MySQL Data Types
Master MySQL data types — the three families (numeric, date/time, string) plus JSON and spatial. Covers integer types and UNSIGNED, the DECIMAL vs FLOAT money rule, DATETIME vs TIMESTAMP and the Y2038 problem, VARCHAR/CHAR/TEXT character vs byte limits, ENUM and SET, JSON support, and a comprehensive choose-the-right-type cheat sheet. Flags deprecated syntax (display widths, FLOAT(M,D), YEAR(2)).
A data type tells MySQL what kind of value can live in a column — an integer, a string of up to 100 characters, a date, a fraction with two decimal places, a JSON document. Picking the right type matters because it determines:
- What values are allowed — the database rejects anything that doesn't fit.
- How much storage the column uses — and therefore how big your tables grow.
- How fast queries are — narrower types fit more rows per page; arithmetic on integers is faster than on strings.
- What operations work — you can
SUM()a number, but not aVARCHAR; you canDATE_ADD()a date, but not an integer.
MySQL data types divide into three broad families, plus a couple of specialized types:
| Category | Stores | Examples |
|---|---|---|
| Numeric | Integers and decimals | INT, BIGINT, DECIMAL, FLOAT, BIT |
| Date and Time | Calendar and clock values | DATE, DATETIME, TIMESTAMP, TIME, YEAR |
| String | Text and binary blobs | CHAR, VARCHAR, TEXT, BLOB, ENUM, SET |
| JSON | Structured documents | JSON (since MySQL 5.7) |
| Spatial | Geographic coordinates and shapes | GEOMETRY, POINT, POLYGON |
Integer Types
Five integer sizes, each available as SIGNED (the default — allows negatives) or UNSIGNED (positive only, doubles the maximum):
| Type | Bytes | Signed range | Unsigned range |
|---|---|---|---|
TINYINT | 1 | −128 to 127 | 0 to 255 |
SMALLINT | 2 | −32 768 to 32 767 | 0 to 65 535 |
MEDIUMINT | 3 | −8 388 608 to 8 388 607 | 0 to 16 777 215 |
INT (or INTEGER) | 4 | −2 147 483 648 to 2 147 483 647 | 0 to 4 294 967 295 |
BIGINT | 8 | −9.2 × 1018 to 9.2 × 1018 | 0 to 1.8 × 1019 |
(N) is deprecated in MySQL 8.0.17+. Old documentation says INT(11) means "11-digit-wide integer" — but the (N) never affected what values were stored, only how some clients displayed the values when paired with ZEROFILL. As of 8.0.17 it's deprecated and ignored for integer types. Just write INT, not INT(11).BIGINT — TINYINT UNSIGNED is enough. Smaller types mean less storage, more rows per page, and faster scans on big tables.Fixed-Point and Floating-Point Types
| Type | Storage | Use for |
|---|---|---|
DECIMAL(M, D) | Variable; ~M/2 bytes | Money and any value where exact precision matters. M = total digits (max 65), D = digits after decimal point (max 30). Synonym: NUMERIC. |
FLOAT | 4 bytes | Approximate single-precision floating point — ~7 digits of precision. Use for measurements where small rounding is acceptable. |
DOUBLE | 8 bytes | Approximate double-precision floating point — ~15 digits. Synonym: REAL. |
BIT(M) | (M+7)/8 bytes | Raw bit field, 1–64 bits. Niche use. |
FLOAT(M, D) and DOUBLE(M, D) are deprecated as of MySQL 8.0.17. The (M, D) display modifier is non-standard and never controlled actual precision — just write FLOAT or DOUBLE.DECIMAL, never FLOAT. Floating-point types can't represent simple decimals like 0.1 exactly — small rounding errors compound across calculations. DECIMAL(10, 2) is the standard choice for currency in two-decimal jurisdictions.Boolean
MySQL doesn't have a real boolean type. BOOL and BOOLEAN are synonyms for TINYINT(1) — they store 0 (false) or any non-zero (true). You'll see this surprise newcomers: SELECT TRUE = 1; returns 1.
| Type | Format | Range | Storage |
|---|---|---|---|
DATE | YYYY-MM-DD | 1000-01-01 to 9999-12-31 | 3 bytes |
DATETIME | YYYY-MM-DD HH:MM:SS | 1000-01-01 00:00:00 to 9999-12-31 23:59:59 | 5 bytes (+1–3 for fractional seconds) |
TIMESTAMP | YYYY-MM-DD HH:MM:SS | 1970-01-01 00:00:01 UTC to 2038-01-19 03:14:07 UTC | 4 bytes (+1–3 for fractional seconds) |
TIME | HH:MM:SS | −838:59:59 to 838:59:59 | 3 bytes (+1–3 for fractional seconds) |
YEAR | YYYY | 1901 to 2155 (and 0000) | 1 byte |
Fractional Seconds
Since MySQL 5.6.4, DATETIME, TIMESTAMP, and TIME can store fractional seconds with up to microsecond precision. Specify the precision in parentheses:
CREATE TABLE event_log (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
happened_at DATETIME(6) NOT NULL, -- microseconds
response_ms TIME(3) -- milliseconds
);
DATETIME vs TIMESTAMP — The Big Choice
| Aspect | DATETIME | TIMESTAMP |
|---|---|---|
| Range | Year 1000 to 9999 | 1970-01-01 to 2038-01-19 ⚠️ |
| Storage | 5 bytes | 4 bytes |
| Time zone | Stored as-is — no conversion | Stored as UTC, displayed in session time zone |
| Default value | NULL or explicit | Can auto-update on row update |
TIMESTAMP uses a 32-bit Unix timestamp internally — it overflows on January 19, 2038. If you're storing future dates, audit timestamps for long-lived records, or any value that might land beyond 2038, use DATETIME instead. The original tutorial said "to sometime in 2037" — the actual cutoff is 2038-01-19 03:14:07 UTC.YEAR(2) was removed in MySQL 5.7. Older documentation describes a 2-digit form covering 1970–2069. It's gone — only the 4-digit form remains. Just write YEAR, not YEAR(4) or YEAR(2).DATETIME. The wider range and timezone-stable behavior fit most use cases. Reach for TIMESTAMP only when you specifically want auto-conversion to/from UTC and you're sure your data won't outlive 2038.Variable vs Fixed Length
| Type | Stores | Max length | Storage |
|---|---|---|---|
CHAR(M) | Fixed-length string of M characters (0–255) | 255 chars | Always M × charset bytes — padded with spaces |
VARCHAR(M) | Variable-length string up to M characters | 65 535 chars (subject to row-size limit) | Length + 1 or 2 bytes per row |
VARCHAR. CHAR only beats VARCHAR when every value is exactly the same length (e.g. ISO country codes CHAR(2), fixed-format SKUs). For anything variable-length — names, emails, descriptions — VARCHAR uses storage proportional to actual content.Big Text — TEXT Family
| Type | Max bytes | ~Capacity |
|---|---|---|
TINYTEXT | 255 | Short strings — rare; usually VARCHAR is better |
TEXT | 65 535 | Long descriptions, comments, article bodies |
MEDIUMTEXT | 16 777 215 | Larger documents — ~16 MB |
LONGTEXT | 4 294 967 295 | Very large documents — ~4 GB |
TEXT column can hold 65 535 bytes. With utf8mb4 (the modern default character set, which uses 1–4 bytes per character), that's at most 65 535 characters and as few as ~16 000 if every character is a 4-byte emoji or East-Asian glyph. The original tutorial said "65535 characters" — this is misleading.Binary Strings — BLOB Family
| Type | Max bytes | Uses |
|---|---|---|
TINYBLOB | 255 | Tiny binary blobs |
BLOB | 65 535 | Small binary data — thumbnails, certificates |
MEDIUMBLOB | 16 777 215 | Medium files — images, PDFs (~16 MB max) |
LONGBLOB | 4 294 967 295 | Very large files (~4 GB) |
BINARY(M) / VARBINARY(M) | Fixed/variable up to row limit | Fixed or variable-length binary — UUIDs, hashes |
Enumerated and Set Types
MySQL has two type-safe ways to constrain a column to a fixed set of string values:
| Type | Behavior | Example |
|---|---|---|
ENUM('a','b','c') | Column holds exactly one of the listed values (or NULL) | status ENUM('pending','active','disabled') |
SET('a','b','c') | Column holds any subset (zero or more) of the listed values | permissions SET('read','write','admin') |
CREATE TABLE accounts (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
status ENUM('pending','active','suspended','closed') NOT NULL DEFAULT 'pending',
roles SET('user','editor','admin') NOT NULL DEFAULT 'user'
);
INSERT INTO accounts (status, roles)
VALUES ('active', 'user,editor');
SELECT * FROM accounts;
ALTER TABLE. If your set of allowed values changes often, ENUM becomes a maintenance headache. A separate lookup table with a foreign key is more flexible.Since MySQL 5.7, JSON is a first-class type. It validates that values are well-formed JSON, stores them in an efficient binary format, and exposes a family of JSON_*() functions for querying and modifying nested data.
CREATE TABLE products (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
specs JSON
);
INSERT INTO products (name, specs)
VALUES ('Wireless mouse', '{"color":"black","weight_g":85,"buttons":3}');
-- Query into the JSON
SELECT name, JSON_EXTRACT(specs, '$.color') AS color
FROM products
WHERE JSON_EXTRACT(specs, '$.weight_g') < 100;
For geographic data, MySQL has the OGC-standard spatial types — GEOMETRY (the catch-all), POINT, LINESTRING, POLYGON, and the MULTI* versions. With spatial indexes, you can run "find restaurants within 5 km" queries efficiently. Niche but powerful when you need it.
CREATE TABLE customers (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, -- big integer, no negatives
email VARCHAR(255) NOT NULL UNIQUE, -- variable string
full_name VARCHAR(100) NOT NULL,
country_code CHAR(2) NOT NULL, -- fixed: ISO codes are always 2 chars
age TINYINT UNSIGNED, -- 0-255 covers any human age
balance DECIMAL(12, 2) NOT NULL DEFAULT 0, -- money — always DECIMAL
status ENUM('pending','active','suspended','closed')
NOT NULL DEFAULT 'pending',
preferences JSON, -- flexible per-user settings
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
last_login_at DATETIME(6),
bio TEXT, -- long, variable
avatar MEDIUMBLOB -- binary, ~16 MB max
);
Notice the deliberate type choices: BIGINT UNSIGNED for an ID that needs to count high, CHAR(2) for the always-2-character country code, TINYINT UNSIGNED for age (saves bytes vs INT), DECIMAL for money, JSON for the open-ended preferences blob.
| If you need to store… | Use |
|---|---|
| Whole numbers, < 4 billion | INT (or INT UNSIGNED if always non-negative) |
| Whole numbers up to 18 quintillion (e.g. IDs, file sizes) | BIGINT UNSIGNED |
| Money, currency, anything where rounding matters | DECIMAL(M, D) — never FLOAT |
| Approximate measurements where small error is OK | DOUBLE |
| True/false flag | TINYINT(1) (alias BOOLEAN) |
| Date with no time | DATE |
| Date and time, future-proof | DATETIME — possibly DATETIME(6) for microseconds |
| UTC-aware timestamp, before 2038 | TIMESTAMP |
| Short variable text (names, emails, titles) | VARCHAR(N) — pick a sensible N |
| Always-same-length code (country, SKU) | CHAR(N) |
| Long body text (article, comment) | TEXT or MEDIUMTEXT |
| Fixed list of allowed values | ENUM(...) |
| Variable structure (per-user settings, varying attributes) | JSON |
| Files, images, binary blobs | Object storage, with the URL/key in VARCHAR — fall back to BLOB only if you must |
- MySQL data types fall into three big families — numeric, date/time, and string — plus JSON and spatial for special cases.
- Pick the smallest integer that fits; use
UNSIGNEDwhen values can never be negative — it doubles your range for the same storage. - Money is
DECIMAL, neverFLOAT— floating point can't represent simple decimals exactly. - Default to
DATETIMEfor timestamps; reach forTIMESTAMPonly when UTC conversion is desired and dates won't pass 2038. - Default to
VARCHAR; useCHARonly for fixed-length codes; useTEXTfor long text where row-size limits would otherwise be exceeded. - Watch the deprecation surface: integer
(N)display widths,FLOAT(M,D)/DOUBLE(M,D), andYEAR(2)are all deprecated or removed in modern MySQL — drop them. ENUMfor a small fixed set of values; a separate lookup table when the set changes often;JSONfor genuinely variable structure.