MySQLData Types

MySQL Data Types Overview

Choosing the right data type for each column is one of the most important decisions in database design. The right type saves storage, improves query performance, enforces data integrity, and makes your code more readable. MySQL has a rich type system covering numbers, strings, dates, spatial data, and JSON.

Data Type Categories

Category

Types

Use for

Numeric

TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT, FLOAT, DOUBLE, DECIMAL, BIT, BOOLEAN

Counts, IDs, prices, measurements, flags

String

CHAR, VARCHAR, TINYTEXT, TEXT, MEDIUMTEXT, LONGTEXT, BINARY, VARBINARY, BLOB types, ENUM, SET

Names, descriptions, codes, binary data, categories

Date and Time

DATE, TIME, DATETIME, TIMESTAMP, YEAR

Dates, times, timestamps, durations

Spatial

GEOMETRY, POINT, LINESTRING, POLYGON, MULTIPOINT, and more

Geographic coordinates, shapes, GIS data

JSON

JSON

Semi-structured data, configuration, flexible attributes

Complete Storage Size Comparison

Type

Storage

Range (Signed)

Range (Unsigned)

TINYINT

1 byte

-128 to 127

0 to 255

SMALLINT

2 bytes

-32,768 to 32,767

0 to 65,535

MEDIUMINT

3 bytes

-8,388,608 to 8,388,607

0 to 16,777,215

INT

4 bytes

-2,147,483,648 to 2,147,483,647

0 to 4,294,967,295

BIGINT

8 bytes

-9.2 × 10^18 to 9.2 × 10^18

0 to 18.4 × 10^18

FLOAT

4 bytes

Approx ±3.4 × 10^38

Same (positive only)

DOUBLE

8 bytes

Approx ±1.8 × 10^308

Same (positive only)

DECIMAL(p,s)

Varies: ceil((p-s)/9)*4 + ceil(s/9)*4 bytes

Exact up to 65 digits

Exact up to 65 digits

CHAR(n)

Fixed: n * bytes_per_char

1–255 chars

n/a

VARCHAR(n)

Actual length + 1-2 bytes overhead

0–65,535 bytes total

n/a

DATE

3 bytes

1000-01-01 to 9999-12-31

n/a

TIME

3 bytes

-838:59:59 to 838:59:59

n/a

DATETIME

8 bytes

1000-01-01 to 9999-12-31 23:59:59

n/a

TIMESTAMP

4 bytes

1970-01-01 UTC to 2038-01-19

n/a

TEXT

2 bytes + content

Up to 65,535 bytes (64 KB)

n/a

MEDIUMTEXT

3 bytes + content

Up to 16,777,215 bytes (16 MB)

n/a

LONGTEXT

4 bytes + content

Up to 4 GB

n/a

JSON

Binary format, similar to LONGBLOB

Up to max_allowed_packet

n/a

Implicit Type Coercion Rules and Gotchas

When MySQL compares or combines values of different types, it silently converts one type to the other. This coercion can produce surprising results and — critically — can prevent index usage.

SQL
-- String to number: leading digits extracted, rest discarded
SELECT '42abc' + 0;      -- 42  (string becomes 42)
SELECT '42abc' = 42;     -- 1   (TRUE — string converts to 42)
SELECT '0abc'  = 0;      -- 1   (TRUE — '0abc' converts to 0)

-- This silently matches unintended rows!
SELECT * FROM users WHERE id = '1 OR 1=1';
-- MySQL converts '1 OR 1=1' to 1 (stops at non-digit)
-- Finds only user id=1 — NOT an injection issue here, but a logic bug

-- Type mismatch prevents index use
-- Bad: index on id (INT) cannot be used with string literal
EXPLAIN SELECT * FROM users WHERE id = '42'G
-- type: ref or ALL depending on version (implicit cast)

-- Good: matches column type exactly, index used efficiently
EXPLAIN SELECT * FROM users WHERE id = 42G
Warning
Always compare columns to values of the same type. Type mismatches cause implicit conversions that silently disable index usage, resulting in full table scans on large tables.
NULL Storage

InnoDB stores NULL values efficiently. For each row, InnoDB maintains a null bitmap in the row header — 1 bit per nullable column. If a column IS NULL, the bitmap bit is set and no additional storage is used for the value itself.

For a NOT NULL column, there is no null bitmap bit — saving 1 bit per column per row. On a table with 10 nullable columns and 100 million rows, this adds up. More importantly, NOT NULL columns enable certain optimizer shortcuts and are slightly faster to compare.

SQL
-- Declaring NOT NULL enables the optimizer to skip null checks
CREATE TABLE events (
  id         BIGINT   NOT NULL AUTO_INCREMENT,
  user_id    INT      NOT NULL,   -- no null check needed in joins
  event_type VARCHAR(50) NOT NULL,
  payload    JSON,                -- nullable: only allocate when present
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id)
);
The Cost of Wrong Types
  • Storing phone numbers as INT: loses leading zeros (0800... becomes 800...), cannot store international + prefix.

  • Storing prices as FLOAT: rounding errors make 9.99 * 100 = 999.00000190... instead of 999.00.

  • Storing dates as VARCHAR: sorting, date arithmetic, and range queries all break.

  • Storing booleans as VARCHAR('true'/'false'): 4-5 bytes instead of 1 bit; slow comparisons.

  • Using INT instead of BIGINT for high-traffic tables: hits the 2.1B row ceiling unexpectedly.

Use DECIMAL for Money, Never FLOAT

SQL
-- This is wrong — FLOAT has rounding errors
CREATE TABLE prices_wrong (price FLOAT);
INSERT INTO prices_wrong VALUES (9.99);
SELECT price * 100 FROM prices_wrong;
-- Result: 999.0000019073486 -- NOT 999.00!

-- This is correct
CREATE TABLE prices_correct (price DECIMAL(10, 2));
INSERT INTO prices_correct VALUES (9.99);
SELECT price * 100 FROM prices_correct;
-- Result: 999.00 -- exact!
Warning
FLOAT and DOUBLE are IEEE 754 floating-point types with inherent binary representation errors. Never store currency, financial calculations, or any value where exact decimal precision matters in a FLOAT or DOUBLE column.
CAST() and CONVERT() with All Format Options

SQL
-- CAST(value AS type) -- SQL standard
SELECT CAST('2024-01-15'   AS DATE);
SELECT CAST(42.7           AS UNSIGNED);    -- 42 (truncates, not rounds)
SELECT CAST('123abc'       AS UNSIGNED);    -- 123 (stops at non-digit)
SELECT CAST(NOW()          AS DATE);        -- strips time portion
SELECT CAST('{"a":1}'     AS JSON);        -- validates and parses

-- All CAST target types:
-- SIGNED              INT with sign
-- UNSIGNED            INT without sign
-- DECIMAL(p,s)        Exact decimal
-- FLOAT               4-byte float
-- DOUBLE              8-byte float
-- DATE                Date only
-- DATETIME            Date and time
-- TIME                Time only
-- YEAR                Year only
-- CHAR[(n)]           Character string
-- BINARY[(n)]         Binary string
-- JSON                JSON document (8.0+)

-- CONVERT(value, type) -- MySQL extension, functionally same as CAST
SELECT CONVERT('42',    UNSIGNED);
SELECT CONVERT(NOW(),   DATE);

-- Two-argument CONVERT: character set conversion
SELECT CONVERT('hello' USING utf8mb4);
SELECT CONVERT(latin1_col USING utf8mb4) FROM legacy_table;
Generated Columns

Generated columns compute their value automatically from an expression involving other columns. They come in two flavours: VIRTUAL (computed on read, not stored) and STORED (computed on write, stored on disk). Both can be indexed, which enables fast queries on derived values.

SQL
-- VIRTUAL column: computed on SELECT, not stored
CREATE TABLE order_items (
  id         INT            NOT NULL AUTO_INCREMENT,
  qty        INT            NOT NULL,
  unit_price DECIMAL(10,2)  NOT NULL,
  -- VIRTUAL: no storage used, computed each time it is read
  line_total DECIMAL(12,2)  AS (qty * unit_price) VIRTUAL,
  PRIMARY KEY (id)
);

-- STORED column: computed on INSERT/UPDATE, stored on disk
CREATE TABLE articles (
  id       INT       NOT NULL AUTO_INCREMENT,
  title    VARCHAR(500) NOT NULL,
  body     MEDIUMTEXT,
  -- STORED: indexable, takes disk space
  word_count INT UNSIGNED AS (
    IF(body IS NULL, 0,
      CHAR_LENGTH(body) - CHAR_LENGTH(REPLACE(body, ' ', '')) + 1)
  ) STORED,
  PRIMARY KEY (id),
  INDEX idx_word_count (word_count)  -- can index STORED generated columns
);

-- Query using the generated column
SELECT title, word_count FROM articles WHERE word_count > 1000;
Note
VIRTUAL generated columns cannot be indexed in MySQL (you can only index STORED generated columns). Use STORED when you need to filter or sort by the derived value, VIRTUAL when you only need it in SELECT output.
JSON vs TEXT for Structured Data

The JSON type (MySQL 5.7.8+) stores JSON documents in an efficient binary format that:

  • Validates JSON on insert (rejects malformed JSON)
  • Supports path extraction with the -> and ->> operators
  • Allows indexing via generated columns on specific JSON paths
  • Is more compact than equivalent TEXT storage for the same document

Use TEXT only for unstructured free-form content that you never need to parse.

SQL
-- JSON column: validated, path-accessible
CREATE TABLE user_preferences (
  user_id   INT PRIMARY KEY,
  settings  JSON NOT NULL
);

INSERT INTO user_preferences VALUES
  (1, '{"theme":"dark","lang":"en","notifications":true}');

-- Extract with -> (returns JSON value, strings are quoted)
SELECT settings->'$.theme' FROM user_preferences WHERE user_id = 1;
-- Result: "dark"

-- Extract with ->> (returns unquoted string)
SELECT settings->>'$.theme' FROM user_preferences WHERE user_id = 1;
-- Result: dark

-- Index a specific JSON path via generated column
ALTER TABLE user_preferences
  ADD COLUMN theme_virtual VARCHAR(20)
    AS (settings->>'$.theme') VIRTUAL,
  ADD INDEX idx_theme (theme_virtual);

SELECT user_id FROM user_preferences WHERE theme_virtual = 'dark';
Type Selection Quick Reference

Data

Recommended Type

Reason

Auto-increment PK (small)

INT UNSIGNED AUTO_INCREMENT

Up to 4.3 billion rows, 4 bytes

Auto-increment PK (large)

BIGINT UNSIGNED AUTO_INCREMENT

Up to 18.4 × 10^18 rows, 8 bytes

UUID / GUID

BINARY(16) or CHAR(36)

BINARY saves 20 bytes per row

Boolean / flag

TINYINT(1) or BOOLEAN

MySQL has no true boolean

Money / currency

DECIMAL(15, 2)

Exact; never FLOAT/DOUBLE

Email address

VARCHAR(254)

RFC 5321 max is 254 chars

URL

VARCHAR(2048)

Covers most real-world URLs

Password hash (bcrypt)

CHAR(60)

bcrypt output is always 60 chars

ISO country code

CHAR(2)

Always exactly 2 chars

Long text / article body

MEDIUMTEXT or LONGTEXT

TEXT capped at 64KB

Timestamp with auto-update

TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP

Tracks row changes, UTC storage

Date without time

DATE

3 bytes, no time component

Flexible attributes

JSON

Validated, path-accessible, indexable

Fixed category list

ENUM or TINYINT UNSIGNED

ENUM stored as 1-2 bytes internally