SQL Data Types Explained (2026)
A finance table stores prices as FLOAT. Nine months and a few million rows later, the total revenue report is off by $340 — not from a bug in the logic, but from tiny rounding errors compounding across every row. Data types aren't a formality you skip while designing a table; they decide how a column stores, compares, sorts, and computes. This guide covers numeric types, string types, date/time types, booleans, implicit conversion traps, and how to actually choose the right one.
Quick Answer: What Do SQL Data Types Actually Control?
A column's data type controls what values it can store, how much space it uses, how it compares and sorts against other values, and what operations are valid on it. The four families to know: numeric (INTEGER, DECIMAL, FLOAT), string (VARCHAR, TEXT, CHAR), date/time (DATE, TIMESTAMP), and boolean. Picking the wrong one doesn't usually error immediately — it shows up later as a rounding error, a dropped leading zero, or a comparison that silently returns the wrong rows.
The $340 Rounding Error
A finance table stores price as FLOAT instead of DECIMAL. Individually, each price looks fine — $19.99 shows up as $19.99. But floating-point numbers store an approximation of most decimal values in binary, not an exact value, and small rounding differences accumulate every time prices are summed across millions of rows. Nine months later, a quarterly revenue report is off by $340 against the accounting system, and nobody can find a bug in the logic — because there isn't one. The type itself was wrong from the start.
Data types feel like a formality during table design — something to click through quickly and move on. In practice, the type you pick for a column decides, months in advance, whether a report will be trustworthy, whether a leading zero survives, and whether a date behaves consistently across time zones.
Numeric Types: INTEGER, DECIMAL, and FLOAT
Numeric types split into two fundamentally different families: exact and approximate.
| Type | Stores | Precision | Use for |
|---|---|---|---|
| INTEGER / BIGINT | Whole numbers | Exact | Counts, IDs, quantities |
| DECIMAL / NUMERIC | Fixed-point decimals | Exact | Money, precise measurements |
| FLOAT / DOUBLE | Floating-point decimals | Approximate | Scientific data, not money |
-- correct: exact, no accumulated rounding error CREATE TABLE orders ( order_id INTEGER, price DECIMAL(10,2) ); -- risky at scale: FLOAT is an approximation CREATE TABLE orders ( order_id INTEGER, price FLOAT );
DECIMAL(10,2) means up to 10 total digits, 2 of them after the decimal point — exactly right for currency. Never use FLOAT or DOUBLE for money, inventory counts that must reconcile exactly, or anything else where "close enough" isn't good enough.
String Types: VARCHAR, TEXT, and CHAR
Text storage has three common options, and the differences are smaller than most beginners expect.
| Type | Length | Use for |
|---|---|---|
| CHAR(n) | Fixed, padded with spaces | Fixed-width codes, e.g. country codes |
| VARCHAR(n) | Variable, up to a limit | Names, emails, most text with a sane max |
| TEXT | Variable, effectively unlimited | Descriptions, comments, long free text |
In PostgreSQL specifically, performance between VARCHAR and TEXT is nearly identical — the practical reason to pick VARCHAR(n) over TEXT is to have the database enforce a maximum length as a constraint, not for any storage or speed advantage. CHAR is rare outside fixed-width codes like a 2-letter country code or a 3-letter currency code, since it pads shorter values with trailing spaces to reach its fixed length.
Date and Time Types
Getting date/time types wrong is one of the most common sources of subtle bugs in applications used across regions.
CREATE TABLE orders ( order_date DATE, -- calendar date only, no time created_at TIMESTAMP, -- date + time, no time zone updated_at TIMESTAMPTZ -- date + time, time-zone aware );
- DATE stores only a calendar date — correct for a birthday or a due date where time-of-day is irrelevant.
- TIMESTAMP stores date and time but has no concept of time zone — the same stored value can mean different real moments depending on how the application interprets it.
- TIMESTAMP WITH TIME ZONE (
TIMESTAMPTZin PostgreSQL) normalizes incoming values to a consistent reference internally, which is the safer default for any system used across more than one time zone.
For any application with users in more than one region, default to a time-zone-aware timestamp type from day one. Migrating a table from naive TIMESTAMP to TIMESTAMPTZ after the data already has ambiguous values is a genuinely painful cleanup — much cheaper to choose correctly upfront.
Boolean: Not Always a Native Type
True/false flags feel like the simplest possible data type, but support for a dedicated boolean type is inconsistent across databases.
| Database | Boolean support |
|---|---|
| PostgreSQL | Native BOOLEAN (true / false / null) |
| MySQL | BOOLEAN is an alias for TINYINT(1) |
| BigQuery | Native BOOL type |
In MySQL, a column declared BOOLEAN actually stores as TINYINT(1) under the hood — it accepts 0 and 1 and behaves close to a boolean in practice, but tools and drivers occasionally surface it as a small integer rather than true/false. Worth knowing before you're debugging why a boolean-looking column shows up as 1 instead of true in some client.
Implicit Conversion: When SQL Guesses for You
Comparing values of different types doesn't always error — many databases silently convert one side to match the other, called implicit conversion. It can work in your favor or quietly produce a wrong result.
-- risky: relies on the database silently converting types SELECT * FROM orders WHERE order_id = '1024'; -- explicit: cast deliberately instead of hoping SELECT * FROM orders WHERE order_id = CAST('1024' AS INTEGER); -- PostgreSQL shorthand cast syntax SELECT * FROM orders WHERE order_id = '1024'::INTEGER;
The specific rules for what converts to what, and whether a mismatch errors or silently coerces, differ across database systems and even across versions of the same one. Rather than memorizing every rule, cast explicitly with CAST() or the :: shorthand whenever you're comparing values that didn't originate as the same type — it removes the guesswork entirely.
Choosing the Right Type: A Practical Checklist
Is it money or must it reconcile exactly?
DECIMAL or NUMERIC, never FLOAT or DOUBLE.
Could it have a meaningful leading zero?
Phone numbers, zip codes, account numbers — store as VARCHAR or CHAR, not a numeric type.
Does time zone matter?
Use a time-zone-aware timestamp for anything spanning regions; plain DATE when only the calendar date matters.
Is there a real maximum length?
VARCHAR(n) with an enforced limit for names and emails; TEXT for open-ended content like comments.
Mistakes That Give Away a Beginner
- Storing money as FLOAT. The $340 rounding error from this article's opening — invisible at small scale, expensive once it compounds.
- Storing phone numbers or zip codes as INTEGER. A leading zero has no meaning in a number and gets silently dropped.
- Using naive TIMESTAMP for a multi-region application. Works fine until users in two time zones compare the same stored value and get different real moments.
- Comparing a string column to a number without casting and trusting whatever the database's implicit conversion rules happen to do.
- Assuming BOOLEAN behaves identically everywhere. It's a real type in PostgreSQL and BigQuery, but an alias for TINYINT(1) in MySQL.
Practice: Pick the Right Type for Each Column
For a new employees table, decide the correct data type and briefly justify it for each of these:
annual_salary
A currency value that must reconcile exactly across payroll reports.
employee_id_number
A company ID that sometimes starts with a zero, e.g. "00452."
last_login
A timestamp for a system used by employees across three countries.
If your answers were DECIMAL, VARCHAR, and TIMESTAMPTZ — in that order — you've got the core decision logic down.
Frequently Asked Questions
Should I use FLOAT or DECIMAL for money in SQL?
Use DECIMAL or NUMERIC for money, never FLOAT or DOUBLE. Floating-point types store an approximation of most decimal values, and repeated addition of amounts like 0.10 can accumulate visible rounding errors. DECIMAL stores an exact value and is the standard choice for currency.
What is the difference between VARCHAR and TEXT in SQL?
VARCHAR(n) stores variable-length text up to a defined limit, while TEXT typically stores variable-length text with no practical limit. In PostgreSQL, performance between them is nearly identical, so VARCHAR is mainly useful when you want the database to enforce a maximum length. In older MySQL versions, TEXT columns had storage differences worth checking against current documentation.
What is the difference between DATE and TIMESTAMP in SQL?
DATE stores only a calendar date with no time component. TIMESTAMP stores both a date and a time, down to fractional seconds. TIMESTAMP WITH TIME ZONE additionally stores time zone awareness, converting values to a consistent reference internally, which matters for any application used across regions.
Why did my SQL query return unexpected results comparing a number to text?
This usually happens because of implicit type conversion, where the database silently converts one side of a comparison to match the other's type. Behavior varies by database and can produce unexpected matches or errors. Casting explicitly with CAST() or :: avoids relying on implicit conversion rules.
Is BOOLEAN a real data type in SQL?
It depends on the database. PostgreSQL has a native BOOLEAN type storing true, false, or null. MySQL does not have a true boolean type; BOOLEAN is an alias for TINYINT(1), storing 0 and 1. This difference matters when writing SQL meant to run on multiple database systems.
Why does a leading zero disappear when I store a phone number as INTEGER?
Integer types store numeric values, and a leading zero has no mathematical meaning in a number, so it is dropped. Identifiers like phone numbers, zip codes, and account numbers that may contain leading zeros should be stored as VARCHAR or CHAR, not as a numeric type.
Conclusion: The Fundamentals Tier Is Complete
Data types decide how a column behaves long before anyone notices — they're the quiet foundation under everything covered so far in this cluster. Use DECIMAL for money, keep identifiers with leading zeros as text, pick a time-zone-aware timestamp when regions are involved, and cast explicitly instead of trusting implicit conversion. Get these four decisions right at table design time, and entire categories of bugs simply never happen.
That closes out the fundamentals tier: SELECT, WHERE, ORDER BY/LIMIT, and data types. The next article starts the intermediate tier with GROUP BY and HAVING — turning individual rows into aggregated summaries.
Next Up: SQL GROUP BY and HAVING
Fundamentals are done. GROUP BY is where SQL starts summarizing data instead of just returning individual rows.
▶ Read the GROUP BY & HAVING GuideKhalid Hussain
Founder of Review Publically. MSc holder and Google Advanced Data Analytics certified. Teaches SQL and Python data analysis with a focus on current, correct, production-ready syntax rather than outdated conventions.
Related Articles