MySQL Numeric Types
MySQL provides a comprehensive set of numeric data types covering integers of various sizes, exact decimal numbers, and floating-point approximations. Choosing the right numeric type affects storage size, query performance, and — critically — whether your arithmetic is exact or subject to floating-point rounding.
Integer Types
MySQL has five integer types that differ only in storage size and value range:
Type | Storage | Signed Min | Signed Max | Unsigned Min | Unsigned Max |
|---|---|---|---|---|---|
TINYINT | 1 byte | -128 | 127 | 0 | 255 |
SMALLINT | 2 bytes | -32,768 | 32,767 | 0 | 65,535 |
MEDIUMINT | 3 bytes | -8,388,608 | 8,388,607 | 0 | 16,777,215 |
INT (INTEGER) | 4 bytes | -2,147,483,648 | 2,147,483,647 | 0 | 4,294,967,295 |
BIGINT | 8 bytes | -9,223,372,036,854,775,808 | 9,223,372,036,854,775,807 | 0 | 18,446,744,073,709,551,615 |
-- Integer column declarations CREATE TABLE examples ( -- Signed integers (default) tiny_signed TINYINT, -- -128 to 127 small_signed SMALLINT, -- -32768 to 32767 medium_signed MEDIUMINT, -- -8M to 8M int_signed INT, -- -2B to 2B big_signed BIGINT, -- -9.2 quintillion to 9.2 quintillion -- Unsigned integers (only positive) tiny_unsigned TINYINT UNSIGNED, -- 0 to 255 small_unsigned SMALLINT UNSIGNED, -- 0 to 65535 int_unsigned INT UNSIGNED, -- 0 to 4.29 billion big_unsigned BIGINT UNSIGNED -- 0 to 18.4 quintillion );
When to Use Each Integer Type
TINYINT: Boolean flags, small status codes (0-3), age (0-127), star ratings (1-5), HTTP status category (1-5)
TINYINT UNSIGNED: Age (0-255), small counters, percentages as integers (0-100)
SMALLINT: Port numbers (0-65535), year offsets, small reference table IDs
MEDIUMINT: Medium-scale counters, ZIP codes (stored as numbers), smaller lookup table IDs
INT: Primary keys for most tables (supports up to 4.3 billion rows), order IDs, product IDs
INT UNSIGNED: Primary keys where you want to double the positive range
BIGINT: Social media post IDs, transaction IDs, Unix timestamps in milliseconds, Snowflake IDs
SIGNED vs UNSIGNED
By default, integer columns are signed (can hold negative values). Adding UNSIGNED:
Doubles the maximum positive value (shifts the range from negative+positive to 0+positive)
Prevents storing negative values — MySQL raises an error on insert
Is required for AUTO_INCREMENT primary keys in large tables to get the full 4.29 billion range
Makes sense for inherently non-negative values: IDs, counts, ages, prices in cents
-- Demonstrate UNSIGNED constraint CREATE TABLE counter_demo ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, count INT UNSIGNED DEFAULT 0 ); INSERT INTO counter_demo (count) VALUES (100); -- This will cause an error: column 'count' cannot be negative UPDATE counter_demo SET count = -1 WHERE id = 1; -- Error: Out of range value for column 'count'
The Display Width Syntax (Deprecated)
You may see syntax like INT(11) or TINYINT(1) in older schemas.
The number in parentheses is the display width — it does NOT affect storage or value range.
INT(11) stores exactly the same range as INT(1) or INT.
Display width was only used in combination with the ZEROFILL attribute (now deprecated)
to pad display output with leading zeros. In MySQL 8.0.17+, integer display width is
deprecated and ignored. Write INT, not INT(11).
-- Old style (deprecated, avoid) id INT(11) NOT NULL AUTO_INCREMENT -- Modern style (correct) id INT NOT NULL AUTO_INCREMENT -- TINYINT(1) is a special exception — it is the conventional way to declare -- BOOLEAN columns, and many ORMs use TINYINT(1) to detect boolean columns. is_active TINYINT(1) NOT NULL DEFAULT 1 -- still acceptable for booleans
BOOLEAN / BOOL
MySQL's BOOLEAN type is an alias for TINYINT(1). It stores 0 (false) and 1 (true), but accepts TRUE/FALSE keywords as well:
CREATE TABLE users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(254) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE, -- alias for TINYINT(1)
is_admin BOOLEAN NOT NULL DEFAULT FALSE
);
INSERT INTO users (email, is_active, is_admin) VALUES
('alice@example.com', TRUE, FALSE),
('bob@example.com', 1, 0); -- 1 and 0 work too
-- MySQL stores TRUE as 1 and FALSE as 0
SELECT email, is_active, is_admin FROM users;+--------------------+-----------+----------+ | email | is_active | is_admin | +--------------------+-----------+----------+ | alice@example.com | 1 | 0 | | bob@example.com | 1 | 0 | +--------------------+-----------+----------+
FLOAT and DOUBLE
FLOAT (4 bytes, ~7 significant digits) and DOUBLE (8 bytes, ~15 significant digits) are IEEE 754 floating-point types. They are fast for scientific calculations but imprecise for exact decimal values.
-- Demonstrating floating-point imprecision CREATE TABLE float_demo (val FLOAT, dval DOUBLE); INSERT INTO float_demo VALUES (9.99, 9.99); SELECT val, val * 100, dval, dval * 100 FROM float_demo;
+------+--------------------+------+--------------------+ | val | val * 100 | dval | dval * 100 | +------+--------------------+------+--------------------+ | 9.99 | 999.0000019073486 | 9.99 | 999.00000000000011 | +------+--------------------+------+--------------------+
When FLOAT/DOUBLE Are Appropriate
Scientific measurements where approximate values are acceptable (temperature sensors, GPS coordinates with ~1 meter precision)
Machine learning model weights and embeddings
Statistical calculations where exact decimal representation isn't required
Large-scale scientific datasets where storage efficiency matters more than precision
DECIMAL / NUMERIC
DECIMAL(precision, scale) stores exact decimal numbers. NUMERIC is a synonym.
- Precision: Total number of significant digits (1-65)
- Scale: Digits after the decimal point (0 to precision)
- Storage: Approximately 4 bytes per 9 digits
Examples:
- DECIMAL(10, 2) → stores values like 12345678.99 (8 digits before decimal, 2 after)
- DECIMAL(5, 2) → stores values from -999.99 to 999.99
- DECIMAL(15, 4) → suitable for exchange rates with 4 decimal places
CREATE TABLE financial ( -- Prices: up to $99,999,999.99 price DECIMAL(10, 2) NOT NULL, -- Exchange rates: e.g., 1.234567 exchange_rate DECIMAL(10, 6), -- Tax rates: e.g., 0.08500 (8.5%) tax_rate DECIMAL(5, 5), -- Bitcoin amount: 8 decimal places standard btc_amount DECIMAL(18, 8) ); -- Exact arithmetic INSERT INTO financial (price) VALUES (9.99); SELECT price, price * 100 FROM financial; -- Result: 9.99 | 999.00 -- exact! -- DECIMAL respects its scale on insert INSERT INTO financial (price) VALUES (9.999); -- Stored as: 10.00 (rounded to 2 decimal places) -- In strict mode, this raises a warning
BIT Type
The BIT(n) type stores bit fields — n bits where n is 1 to 64. BIT(1) is effectively a boolean that uses 1 bit of storage.
CREATE TABLE permissions ( user_id INT UNSIGNED, perms BIT(8) -- 8-bit permission mask ); -- Insert using binary literal INSERT INTO permissions VALUES (1, b'10110100'); -- Insert using numeric value (180 = 10110100 in binary) INSERT INTO permissions VALUES (2, 180); -- Check a specific bit (bit 7 = value 128) SELECT user_id, perms + 0 AS perms_int FROM permissions WHERE perms & b'10000000'; -- has the read permission bit set
Overflow Behavior
What happens when you insert a value outside a column's range depends on MySQL's SQL mode.
-- Check current SQL mode SELECT @@sql_mode; -- In STRICT mode (recommended), out-of-range values raise an error: CREATE TABLE test_overflow (n TINYINT UNSIGNED); INSERT INTO test_overflow VALUES (300); -- Error 1264: Out of range value for column 'n' at row 1 -- In non-strict mode, MySQL silently clips to the max: -- 300 gets stored as 255 (max TINYINT UNSIGNED) -- Enable strict mode for the current session SET SESSION sql_mode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION';
STRICT_TRANS_TABLES to sql_mode in your my.cnf. Without it, MySQL silently truncates values that are too large, potentially corrupting data without any error.AUTO_INCREMENT
AUTO_INCREMENT automatically assigns the next sequential integer to a column on INSERT. It is most commonly used for primary keys.
CREATE TABLE orders (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
customer VARCHAR(100) NOT NULL,
total DECIMAL(10, 2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO orders (customer, total) VALUES ('Alice', 99.99);
INSERT INTO orders (customer, total) VALUES ('Bob', 149.50);
SELECT id, customer, total FROM orders;
-- id values: 1, 2
-- Get the last inserted auto_increment value
SELECT LAST_INSERT_ID();
-- Check the next auto_increment value
SHOW CREATE TABLE orders;AUTO_INCREMENT only works on indexed columns (primary key or unique key)
The column must be an integer type (TINYINT through BIGINT, or FLOAT/DOUBLE though this is unusual)
Gaps in AUTO_INCREMENT values are normal — they occur on rollbacks, deletes, and server restarts
The maximum value is determined by the column type — INT UNSIGNED goes to 4,294,967,295
When the maximum is reached, further inserts fail with a duplicate key error
Practical Type Selection Guide
Column purpose | Recommended Type |
|---|---|
Primary key (small tables, under 100M rows) | INT UNSIGNED AUTO_INCREMENT |
Primary key (large tables) | BIGINT UNSIGNED AUTO_INCREMENT |
Boolean / flag | TINYINT(1) or BOOLEAN |
HTTP status code (100-599) | SMALLINT UNSIGNED |
Age (0-150) | TINYINT UNSIGNED |
Year (e.g., publication year) | YEAR or SMALLINT UNSIGNED |
Port number (0-65535) | SMALLINT UNSIGNED |
Quantity / stock count | INT UNSIGNED |
Price / money | DECIMAL(10, 2) or DECIMAL(15, 4) |
Percentage | DECIMAL(5, 2) |
Latitude / longitude | DECIMAL(10, 7) for exact, DOUBLE for approximate |
Scientific measurement | FLOAT or DOUBLE |