Key Takeaways
- MySQL's 30+ data types span 4 families: numeric, string, date/time, and JSON. Picking the wrong one wastes storage and introduces data errors that compound over time.
- Never use
FLOATorDOUBLEfor money. Both use IEEE 754 binary floating-point, which can't represent most decimal fractions exactly. UseDECIMAL(10,2)instead. TIMESTAMPauto-converts to UTC and has a hard 2038-01-19 ceiling.DATETIMEstores wall-clock time with no conversion and supports dates through year 9999.INT UNSIGNEDcovers ~4.3 billion rows. Switch toBIGINT UNSIGNEDfor event logs or high-volume tables before you hit that limit.
Numeric Types
MySQL's numeric types split into two groups: exact types (INT, BIGINT, DECIMAL) and approximate types (FLOAT, DOUBLE). The distinction matters most for financial data. The MySQL 8.0 Reference Manual documents that FLOAT and DOUBLE use IEEE 754 binary representation, which can't precisely store most decimal fractions — making them unsafe for any monetary column.
| Type | Storage | Range (signed) | Use for |
|---|---|---|---|
TINYINT |
1 byte | -128 to 127 | Booleans, small status flags |
SMALLINT |
2 bytes | -32,768 to 32,767 | Small counters, age, year |
INT |
4 bytes | -2.1B to 2.1B | General-purpose IDs and counts |
BIGINT |
8 bytes | ±9.2 quintillion | High-volume tables, snowflake IDs |
DECIMAL(p,s) |
Variable | Exact | Money, measurements requiring precision |
FLOAT / DOUBLE |
4 / 8 bytes | Approximate | Scientific values where small errors are acceptable |
Key rules:
- Use
TINYINT(1)for booleans — MySQL's conventional boolean representation. - Use
INT UNSIGNEDfor auto-increment primary keys (doubles the positive range to ~4.3 billion). - Use
BIGINT UNSIGNEDfor tables that may grow very large. - Never use
FLOATorDOUBLEfor monetary values. Floating-point imprecision causes rounding errors in financial calculations. UseDECIMAL(10, 2)instead.
MySQL stores FLOAT and DOUBLE using the IEEE 754 binary floating-point standard, which cannot precisely represent most decimal fractions (MySQL 8.0 Reference Manual). For monetary calculations, DECIMAL(10,2) stores exact values and prevents the rounding errors that floating-point types introduce in financial totals.
String Types
VARCHAR covers most string storage needs. It's variable-width: MySQL allocates only what the content requires, plus 1-2 bytes of length overhead. CHAR, by contrast, always occupies exactly n bytes — a 10-character value in a CHAR(50) column still consumes all 50 bytes. That fixed width makes CHAR marginally faster to index, which is why the MySQL 8.0 Reference Manual recommends it for fixed-length data like ISO country codes (CHAR(2)) or MD5 hashes (CHAR(32)).
| Type | Max size | Use for |
|---|---|---|
CHAR(n) |
255 chars | Fixed-length strings: country codes, hashes, status enums |
VARCHAR(n) |
65,535 bytes | Variable-length strings: names, emails, URLs, titles |
TEXT |
65,535 bytes | Long content: descriptions, comments, HTML |
MEDIUMTEXT |
16 MB | Large documents, article bodies |
LONGTEXT |
4 GB | Very large content (logs, serialised data) |
Key rules:
- Use
VARCHAR(n)for most string columns. Setnto a realistic maximum:VARCHAR(255)for names and emails,VARCHAR(2048)for URLs. - Use
CHAR(n)only when the value is always the same length. It's marginally faster to index thanVARCHAR. - Don't put
TEXTcolumns inWHEREclauses without a full-text index. It forces a full table scan regardless of how specific your filter is. - Avoid
ENUM. It's inflexible to alter and opaque to external tools. Use aVARCHARwith aCHECKconstraint, or a separate lookup table instead.
VARCHAR and CHAR differ primarily in storage behavior: CHAR(n) always occupies exactly n bytes, padded with spaces, while VARCHAR(n) uses 1 byte of overhead for values up to 255 characters plus actual content length (MySQL 8.0 Reference Manual). For columns where every value is exactly the same length, CHAR is marginally more efficient to index than VARCHAR.
Date and Time Types
The TIMESTAMP versus DATETIME decision has real production consequences. TIMESTAMP stores every value in UTC and reconverts to the session timezone on retrieval, making it automatic for audit columns like created_at. Its hard upper limit is 2038-01-19 — the point where a 32-bit Unix timestamp overflows. Any application storing future dates past that boundary must use DATETIME, which supports dates through year 9999 (MySQL 8.0 Reference Manual).
| Type | Range | Use for |
|---|---|---|
DATE |
1000-01-01 to 9999-12-31 | Dates without time: birthdays, deadlines |
DATETIME |
1000-01-01 to 9999-12-31 | Timestamps stored in application timezone |
TIMESTAMP |
1970-01-01 to 2038-01-19 | Audit timestamps stored in UTC, auto-converted on retrieval |
TIME |
-838:59:59 to 838:59:59 | Durations, time-of-day without date |
YEAR |
1901 to 2155 | Year-only values |
Key rules:
- Use
TIMESTAMPforcreated_atandupdated_ataudit columns. It auto-converts to UTC on storage and back to the session timezone on retrieval. - Use
DATETIMEwhen you need to store a specific wall-clock time without timezone conversion, e.g., a scheduled event that should fire at 9am regardless of timezone. TIMESTAMPhas a 2038 ceiling. For future dates beyond that, useDATETIME.
MySQL's TIMESTAMP type stores values internally as UTC and converts to the current session timezone on retrieval (MySQL 8.0 Reference Manual). Its upper limit is 2038-01-19 03:14:07 UTC, the 32-bit signed integer overflow of Unix time. Applications storing reservation or event dates beyond 2038 must use DATETIME, which has no such ceiling.
JSON Type
MySQL 5.7 introduced native JSON storage; MySQL 8.0 expanded the operator set considerably. A JSON column validates syntax on insert and rejects malformed data outright, preventing the silent corruption that storing JSON in a TEXT column allows. Path expressions use dot notation: data->'$.key' is shorthand for JSON_EXTRACT(data, '$.key'), and the ->> operator returns an unquoted scalar value directly.
-- Read a single JSON path value
SELECT config->'$.theme' FROM user_settings WHERE user_id = 1;
-- Unquoted scalar (MySQL 5.7.13+)
SELECT config->>'$.theme' FROM user_settings WHERE user_id = 1;
-- Filter rows by JSON value
SELECT * FROM users WHERE preferences->>'$.notifications' = 'true';
Use JSON for truly variable or schema-less data: feature flags, user preferences, integration payloads. Don't use it to avoid modelling your data properly. If the same key appears in every row, it should be a regular column. JSON columns can't be indexed directly — only generated columns derived from JSON paths can be indexed — so querying inside JSON at scale requires careful planning. See our database schema examples for how to mix JSON columns with conventional structure.
MySQL's native JSON type validates document syntax at insert time and enables path-based queries using the -> and ->> operators, introduced in MySQL 5.7 and expanded in 8.0 (MySQL 8.0 Reference Manual). Unlike TEXT storage, a JSON column rejects malformed input and permits selective path extraction without parsing the full document in application code.
What Are the Most Common MySQL Data Type Mistakes?
Three type selection errors show up repeatedly in production schemas. They're easy to avoid at design time and expensive to fix once a table has millions of rows.
1. FLOAT or DOUBLE for monetary values. This is the most damaging one. A column defined as FLOAT can't store 0.1 exactly — it stores the nearest IEEE 754 approximation. Sum enough rows and the total drifts from the correct value. Use DECIMAL(10, 2) for any price, fee, or financial amount. No exceptions. The database normalization guide covers how this fits into a well-structured financial schema.
2. TEXT columns in WHERE clauses without a FULLTEXT index. A query like WHERE body LIKE '%keyword%' against a TEXT column does a full table scan every time. On a table with 50,000 rows that's slow; on a table with 5 million rows it's a timeout. Add a FULLTEXT index, store a searchable excerpt in a VARCHAR column, or move search to a dedicated tool.
3. ENUM instead of a lookup table. ENUM enforces allowed values at the database level, which sounds useful. The problem is ALTER TABLE. Adding a new ENUM value in MySQL 5.x rewrites the entire table — a blocking operation on a live database. In MySQL 8.0 this is instant for appended values, but the opaque storage still confuses ORMs and external tooling. A VARCHAR(50) with a CHECK constraint, or a foreign key to a status_types lookup table, is far more maintainable. Your future self will thank you when the business adds a new status at 11pm on a Friday.
Quick Reference: Common Column Patterns
- Primary key:
INT UNSIGNED NOT NULL AUTO_INCREMENT - Large-table primary key:
BIGINT UNSIGNED NOT NULL AUTO_INCREMENT - UUID / GUID:
CHAR(36) NOT NULL(orBINARY(16)withUUID_TO_BIN()in MySQL 8.0+ for storage efficiency) - Email address:
VARCHAR(255) NOT NULL UNIQUE - Password hash:
VARCHAR(255) NOT NULL - URL slug:
VARCHAR(255) NOT NULL - Price / monetary value:
DECIMAL(10, 2) NOT NULL - Boolean flag:
TINYINT(1) NOT NULL DEFAULT 0 - Created/updated timestamps:
TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP - Long-form text:
TEXT - Foreign key: Same type as the referenced primary key, e.g.
INT UNSIGNED NOT NULL
Video Resource
Frequently Asked Questions
What MySQL data type should I use for storing money?
Use DECIMAL(p, s) — for example DECIMAL(10, 2) for two decimal places. Never use FLOAT or DOUBLE for money. Both use IEEE 754 binary floating-point, which cannot represent most decimal fractions exactly, causing rounding errors that accumulate in financial totals.
What is the difference between DATETIME and TIMESTAMP in MySQL?
TIMESTAMP stores values in UTC and automatically converts to the session timezone on retrieval, making it ideal for created_at and updated_at audit columns. DATETIME stores the literal wall-clock time without timezone conversion and has a wider date range (up to year 9999 vs TIMESTAMP's 2038-01-19 limit).
What MySQL type should I use for boolean columns?
MySQL has no native boolean type. The convention is TINYINT(1), which stores 0 (false) or 1 (true). ORMs like Laravel and Rails treat TINYINT(1) as a boolean automatically. MySQL 8.0+ also accepts BOOLEAN as a synonym for TINYINT(1).
When should I use VARCHAR vs TEXT in MySQL?
Use VARCHAR(n) for short strings where you know the maximum length: names, emails, URLs, slugs. Use TEXT for long-form content such as article bodies, descriptions, or HTML where the length is unpredictable. Avoid using TEXT columns in WHERE clauses without a full-text index, as it forces a full table scan.
What MySQL data type should I use for a primary key?
INT UNSIGNED NOT NULL AUTO_INCREMENT is the standard choice for most tables, supporting up to approximately 4.3 billion rows. Use BIGINT UNSIGNED NOT NULL AUTO_INCREMENT for tables expected to grow very large, such as event logs or high-volume transactional tables.
Why should I avoid ENUM in MySQL?
ENUM stores allowed values as a one- or two-byte integer mapped to a list of strings. Adding a new value requires an ALTER TABLE that rewrites the entire table in older MySQL versions, causing downtime on large tables. ENUM values are also opaque to ORMs and external tools. A VARCHAR column with a CHECK constraint, or a separate lookup table, is more maintainable and easier to extend.
What MySQL data type should I use for a UUID?
CHAR(36) stores the standard hyphenated UUID string. For storage efficiency, MySQL 8.0+ provides UUID_TO_BIN() and BIN_TO_UUID() to convert a UUID into BINARY(16), halving the storage compared to CHAR(36) and improving index performance. Use CHAR(36) for readability; use BINARY(16) for high-volume tables where index size matters.