Limited Time Offer: 40% off

MySQL data types

MySQL's data types and when to use each one.

Picking the right data type matters. It affects storage size, query performance, and how MySQL validates data on insert. This reference covers the types you will use most, with practical advice on when to choose each one.

Numeric types

MySQL has several integer types, each with a different storage size and range.

TypeStorageSigned rangeUnsigned range
TINYINT1 byte-128 to 1270 to 255
SMALLINT2 bytes-32,768 to 32,7670 to 65,535
MEDIUMINT3 bytes-8,388,608 to 8,388,6070 to 16,777,215
INT / INTEGER4 bytes-2,147,483,648 to 2,147,483,6470 to 4,294,967,295
BIGINT8 bytes-9.2 × 10¹⁸ to 9.2 × 10¹⁸0 to 1.8 × 10¹⁹

INT is the right default for most primary keys and foreign keys. The 2-billion ceiling is plenty for the vast majority of applications. Use BIGINT when you genuinely expect billions of rows, such as event logs, analytics tables, or high-volume transactional systems.

TINYINT and SMALLINT make sense for columns with a known small range, like a rating from 1 to 5 or an age in years. The storage savings are minor, but they add a layer of implicit constraint.

Auto-increment IDs

SQL
CREATE TABLE orders (
  id         INT UNSIGNED NOT NULL AUTO_INCREMENT,
  customer_id INT UNSIGNED NOT NULL,
  total_cents INT NOT NULL,
  PRIMARY KEY (id)
);

Using UNSIGNED on an auto-increment column doubles the available range without any cost. For INT UNSIGNED, that is 4.2 billion rows. If that is not enough, switch to BIGINT UNSIGNED.

Floating-point and decimal types

TypeStorageUse case
FLOAT4 bytesApproximate values, scientific data
DOUBLE8 bytesHigher-precision approximate values
DECIMAL(p, s)VariesExact values: money, measurements

FLOAT and DOUBLE are approximate. They use binary floating-point, which means 0.1 + 0.2 may not equal exactly 0.3. This is fine for sensor readings or scientific calculations where a small rounding error is acceptable.

DECIMAL stores exact values. Use it for currency and anything where rounding errors are unacceptable. DECIMAL(10, 2) stores up to 10 digits total, with 2 after the decimal point, which covers values up to 99,999,999.99.

SQL
CREATE TABLE products (
  id          INT UNSIGNED NOT NULL AUTO_INCREMENT,
  name        VARCHAR(255) NOT NULL,
  price_usd   DECIMAL(10, 2) NOT NULL,
  weight_kg   FLOAT,
  PRIMARY KEY (id)
);

String types

CHAR vs VARCHAR

CHAR(n) is fixed-length. MySQL always stores exactly n characters, padding shorter values with spaces. VARCHAR(n) is variable-length and stores only the characters you insert, plus 1 or 2 bytes for the length prefix.

TypeStorageBest for
CHAR(n)Always n bytesFixed-length codes: country codes, UUIDs, hashes
VARCHAR(n)Actual length + 1–2 bytesMost text: names, emails, titles

CHAR can be slightly faster for columns where every row has the same length, because MySQL can calculate row offsets without reading length prefixes. For most use cases, the difference is not meaningful. Use VARCHAR(255) as your default for short strings.

SQL
CREATE TABLE users (
  id           INT UNSIGNED NOT NULL AUTO_INCREMENT,
  email        VARCHAR(255) NOT NULL,
  country_code CHAR(2) NOT NULL,
  password_hash CHAR(60) NOT NULL,
  PRIMARY KEY (id),
  UNIQUE KEY (email)
);

TEXT types

When a VARCHAR is not large enough, MySQL provides four TEXT types.

TypeMax length
TINYTEXT255 bytes
TEXT65,535 bytes (~64 KB)
MEDIUMTEXT16,777,215 bytes (~16 MB)
LONGTEXT4,294,967,295 bytes (~4 GB)

Use TEXT for long-form content: blog post bodies, comments, descriptions. Use MEDIUMTEXT or LONGTEXT only when you expect content that exceeds 64 KB.

Note that MySQL cannot use a TEXT column as a primary key or in a UNIQUE constraint without specifying a prefix length. You also cannot give a TEXT column a default value.

BLOB types

BLOB types mirror the TEXT types but store binary data: TINYBLOB, BLOB, MEDIUMBLOB, LONGBLOB. In practice, storing files in a database is usually a mistake. Use object storage (S3, GCS, etc.) and store the URL in a VARCHAR column instead.

Date and time types

TypeStorageFormatRange
DATE3 bytesYYYY-MM-DD1000-01-01 to 9999-12-31
TIME3 bytesHH:MM:SS-838:59:59 to 838:59:59
DATETIME8 bytesYYYY-MM-DD HH:MM:SS1000-01-01 to 9999-12-31
TIMESTAMP4 bytesYYYY-MM-DD HH:MM:SS1970-01-01 to 2038-01-19
YEAR1 byteYYYY1901 to 2155

DATETIME vs TIMESTAMP

This is the most important distinction in the date/time group.

TIMESTAMP stores values in UTC and converts them to the current session time zone on retrieval. It also has a smaller range (up to 2038) and costs half the storage of DATETIME.

DATETIME stores values exactly as you give them, with no time zone conversion. What you put in is what you get out.

For most applications, DATETIME is the better default. You avoid silent conversions when the session time zone changes, and you are not limited to dates before 2038. Handle time zone logic in your application layer where the rules are explicit.

Use TIMESTAMP when you want automatic tracking of row creation or update times:

SQL
CREATE TABLE articles (
  id           INT UNSIGNED NOT NULL AUTO_INCREMENT,
  title        VARCHAR(255) NOT NULL,
  body         MEDIUMTEXT NOT NULL,
  published_at DATETIME,
  created_at   TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at   TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (id)
);

Here published_at is a DATETIME because it is a user-controlled value that should not shift with time zones. The created_at and updated_at columns use TIMESTAMP for automatic tracking.

Boolean

MySQL has no native boolean type. It maps BOOLEAN and BOOL to TINYINT(1). The values TRUE and FALSE are aliases for 1 and 0.

SQL
CREATE TABLE feature_flags (
  id         INT UNSIGNED NOT NULL AUTO_INCREMENT,
  flag_name  VARCHAR(100) NOT NULL,
  is_enabled TINYINT(1) NOT NULL DEFAULT 0,
  PRIMARY KEY (id)
);

-- Both of these work
INSERT INTO feature_flags (flag_name, is_enabled) VALUES ('dark_mode', TRUE);
INSERT INTO feature_flags (flag_name, is_enabled) VALUES ('beta_ui', 1);

BIT(1) is an alternative that stores a single bit and only accepts 0 or 1. It has stricter validation but less readable query output and inconsistent handling across client libraries. TINYINT(1) is more portable.

JSON type

MySQL 5.7 added a native JSON type. Unlike storing JSON in a TEXT column, the JSON type validates the document on insert and stores it in an optimized binary format that allows efficient reads of individual keys.

SQL
CREATE TABLE events (
  id         BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  event_type VARCHAR(100) NOT NULL,
  payload    JSON NOT NULL,
  occurred_at DATETIME NOT NULL,
  PRIMARY KEY (id)
);

INSERT INTO events (event_type, payload, occurred_at)
VALUES ('user.signup', '{"user_id": 42, "plan": "pro", "referral": null}', NOW());

-- Extract a value
SELECT payload->>'$.plan' AS plan FROM events WHERE id = 1;

You can index specific paths within a JSON column using generated columns:

SQL
ALTER TABLE events
  ADD COLUMN user_id BIGINT GENERATED ALWAYS AS (payload->>'$.user_id') STORED,
  ADD INDEX idx_user_id (user_id);

Use JSON when your data is genuinely schemaless or when different rows need different attributes. Avoid it as a substitute for proper schema design. If every row has the same five keys, those should be real columns.

ENUM and SET

ENUM restricts a column to a specific list of string values. MySQL stores the value as a small integer internally, so storage is efficient.

SQL
CREATE TABLE tickets (
  id       INT UNSIGNED NOT NULL AUTO_INCREMENT,
  status   ENUM('open', 'in_progress', 'resolved', 'closed') NOT NULL DEFAULT 'open',
  priority ENUM('low', 'medium', 'high') NOT NULL DEFAULT 'medium',
  PRIMARY KEY (id)
);

SET is similar but allows a column to hold multiple values from the list at once, stored as a bitmask.

SQL
CREATE TABLE notifications (
  id       INT UNSIGNED NOT NULL AUTO_INCREMENT,
  channels SET('email', 'sms', 'push') NOT NULL DEFAULT 'email',
  PRIMARY KEY (id)
);

The trap with both types: adding a new value requires an ALTER TABLE, which can be slow and lock the table on older MySQL versions (before 8.0 online DDL improvements). If the list of values changes often, a separate lookup table with a foreign key is more flexible.

Choosing the right type

SituationRecommended type
Primary key, expected < 2 billion rowsINT UNSIGNED AUTO_INCREMENT
Primary key, expected billions of rowsBIGINT UNSIGNED AUTO_INCREMENT
Money / exact decimalsDECIMAL(p, s)
Short text (names, emails, titles)VARCHAR(255)
Long text (articles, comments)TEXT or MEDIUMTEXT
Fixed-length codes (country codes, hashes)CHAR(n)
Dates without timesDATE
Timestamps for audit fieldsTIMESTAMP DEFAULT CURRENT_TIMESTAMP
All other date/time valuesDATETIME
Boolean flagsTINYINT(1)
Schemaless or variable attributesJSON
Column with a small fixed set of valuesENUM

The guiding principle: use the smallest type that covers your data without artificial constraints. This keeps storage efficient, keeps indexes small, and lets MySQL validate your data at the schema level rather than in application code.