MySQL Data Types MySQL · Data Types

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 a VARCHAR; you can DATE_ADD() a date, but not an integer.

MySQL data types divide into three broad families, plus a couple of specialized types:

CategoryStoresExamples
NumericIntegers and decimalsINT, BIGINT, DECIMAL, FLOAT, BIT
Date and TimeCalendar and clock valuesDATE, DATETIME, TIMESTAMP, TIME, YEAR
StringText and binary blobsCHAR, VARCHAR, TEXT, BLOB, ENUM, SET
JSONStructured documentsJSON (since MySQL 5.7)
SpatialGeographic coordinates and shapesGEOMETRY, POINT, POLYGON

Integer Types

Five integer sizes, each available as SIGNED (the default — allows negatives) or UNSIGNED (positive only, doubles the maximum):

TypeBytesSigned rangeUnsigned range
TINYINT1−128 to 1270 to 255
SMALLINT2−32 768 to 32 7670 to 65 535
MEDIUMINT3−8 388 608 to 8 388 6070 to 16 777 215
INT (or INTEGER)4−2 147 483 648 to 2 147 483 6470 to 4 294 967 295
BIGINT8−9.2 × 1018 to 9.2 × 10180 to 1.8 × 1019
⚠️ The display-width (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).
💡 Pick the smallest integer type that fits. A column holding ages (0–120) doesn't need BIGINTTINYINT UNSIGNED is enough. Smaller types mean less storage, more rows per page, and faster scans on big tables.

Fixed-Point and Floating-Point Types

TypeStorageUse for
DECIMAL(M, D)Variable; ~M/2 bytesMoney and any value where exact precision matters. M = total digits (max 65), D = digits after decimal point (max 30). Synonym: NUMERIC.
FLOAT4 bytesApproximate single-precision floating point — ~7 digits of precision. Use for measurements where small rounding is acceptable.
DOUBLE8 bytesApproximate double-precision floating point — ~15 digits. Synonym: REAL.
BIT(M)(M+7)/8 bytesRaw 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.
💡 Money should always be 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.

TypeFormatRangeStorage
DATEYYYY-MM-DD1000-01-01 to 9999-12-313 bytes
DATETIMEYYYY-MM-DD HH:MM:SS1000-01-01 00:00:00 to 9999-12-31 23:59:595 bytes (+1–3 for fractional seconds)
TIMESTAMPYYYY-MM-DD HH:MM:SS1970-01-01 00:00:01 UTC to 2038-01-19 03:14:07 UTC4 bytes (+1–3 for fractional seconds)
TIMEHH:MM:SS−838:59:59 to 838:59:593 bytes (+1–3 for fractional seconds)
YEARYYYY1901 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:

MySQL — Fractional-second precision
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

AspectDATETIMETIMESTAMP
RangeYear 1000 to 99991970-01-01 to 2038-01-19 ⚠️
Storage5 bytes4 bytes
Time zoneStored as-is — no conversionStored as UTC, displayed in session time zone
Default valueNULL or explicitCan auto-update on row update
⚠️ The Year 2038 problem. 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).
💡 Default to 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

TypeStoresMax lengthStorage
CHAR(M)Fixed-length string of M characters (0–255)255 charsAlways M × charset bytes — padded with spaces
VARCHAR(M)Variable-length string up to M characters65 535 chars (subject to row-size limit)Length + 1 or 2 bytes per row
📌 The original "VARCHAR up to 255" is from MySQL 4.x. Since MySQL 5.0.3, VARCHAR can hold up to 65 535 characters per column — though the entire row is still limited to 65 535 bytes, so wide tables with multiple long VARCHARs may need TEXT instead.
💡 Default to 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

TypeMax bytes~Capacity
TINYTEXT255Short strings — rare; usually VARCHAR is better
TEXT65 535Long descriptions, comments, article bodies
MEDIUMTEXT16 777 215Larger documents — ~16 MB
LONGTEXT4 294 967 295Very large documents — ~4 GB
⚠️ TEXT lengths are in bytes, not characters. A 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

TypeMax bytesUses
TINYBLOB255Tiny binary blobs
BLOB65 535Small binary data — thumbnails, certificates
MEDIUMBLOB16 777 215Medium files — images, PDFs (~16 MB max)
LONGBLOB4 294 967 295Very large files (~4 GB)
BINARY(M) / VARBINARY(M)Fixed/variable up to row limitFixed or variable-length binary — UUIDs, hashes
📌 BLOB vs TEXT: Both can hold large data. BLOB stores raw bytes (case-sensitive, no character set); TEXT stores characters (case-insensitive in default collations, has a character set). Use BLOB for files and binary data; use TEXT for human-readable text.
💡 Don't store huge files in the database. Storing image or video bytes in BLOB/LONGBLOB columns works but is rarely the right call — the database becomes huge, backups are slow, and you can't serve content directly. Most teams put the file in object storage (S3, GCS) and store just the URL or key in the database.

Enumerated and Set Types

MySQL has two type-safe ways to constrain a column to a fixed set of string values:

TypeBehaviorExample
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 valuespermissions SET('read','write','admin')
MySQL — ENUM and SET in action
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;
OUTPUT
mysql — accounts
mysql>CREATE TABLE accounts ( ... );
Query OK, 0 rows affected (0.04 sec)
mysql>INSERT INTO accounts (status, roles) VALUES ('active', 'user,editor');
Query OK, 1 row affected (0.01 sec)
mysql>SELECT * FROM accounts;
+----+--------+-------------+
| id | status | roles |
+----+--------+-------------+
| 1 | active | user,editor |
+----+--------+-------------+
1 row in set (0.00 sec)
⚠️ ENUM gotcha — adding values requires 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.

MySQL — JSON in action
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;
OUTPUT
mysql — products
mysql>SELECT name, JSON_EXTRACT(specs, '$.color') AS color FROM products WHERE ...;
+----------------+---------+
| name | color |
+----------------+---------+
| Wireless mouse | "black" |
+----------------+---------+
1 row in set (0.00 sec)
💡 Use JSON for genuinely variable structure — product attributes that differ across categories, user-provided form fields, audit logs of API requests. Don't use it as a substitute for normal columns; querying and indexing JSON is slower than querying real columns.

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.

Example — A Practical Mix of Types
MySQL — Putting types to work
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
);
OUTPUT
mysql — schema
mysql>CREATE TABLE customers ( ... );
Query OK, 0 rows affected (0.06 sec)
mysql>DESCRIBE customers;
+---------------+-------------------------------------------------------+------+-----+-------------------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------------+-------------------------------------------------------+------+-----+-------------------+-------+
| id | bigint unsigned | NO | PRI | NULL | auto_increment |
| email | varchar(255) | NO | UNI | NULL | |
| full_name | varchar(100) | NO | | NULL | |
| country_code | char(2) | NO | | NULL | |
| age | tinyint unsigned | YES | | NULL | |
| balance | decimal(12,2) | NO | | 0.00 | |
| status | enum('pending','active','suspended','closed') | NO | | pending | |
| preferences | json | YES | | NULL | |
| created_at | datetime(6) | NO | | CURRENT_TIMESTAMP(6) | DEFAULT_GENERATED |
| last_login_at | datetime(6) | YES | | NULL | |
| bio | text | YES | | NULL | |
| avatar | mediumblob | YES | | NULL | |
+---------------+-------------------------------------------------------+------+-----+-------------------+-------+
12 rows in set (0.00 sec)

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 billionINT (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 mattersDECIMAL(M, D) — never FLOAT
Approximate measurements where small error is OKDOUBLE
True/false flagTINYINT(1) (alias BOOLEAN)
Date with no timeDATE
Date and time, future-proofDATETIME — possibly DATETIME(6) for microseconds
UTC-aware timestamp, before 2038TIMESTAMP
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 valuesENUM(...)
Variable structure (per-user settings, varying attributes)JSON
Files, images, binary blobsObject 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 UNSIGNED when values can never be negative — it doubles your range for the same storage.
  • Money is DECIMAL, never FLOAT — floating point can't represent simple decimals exactly.
  • Default to DATETIME for timestamps; reach for TIMESTAMP only when UTC conversion is desired and dates won't pass 2038.
  • Default to VARCHAR; use CHAR only for fixed-length codes; use TEXT for long text where row-size limits would otherwise be exceeded.
  • Watch the deprecation surface: integer (N) display widths, FLOAT(M,D) / DOUBLE(M,D), and YEAR(2) are all deprecated or removed in modern MySQL — drop them.
  • ENUM for a small fixed set of values; a separate lookup table when the set changes often; JSON for genuinely variable structure.