MySQLNumeric Types

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

SQL
-- 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

Tip
For primary keys, use INT UNSIGNED for tables that will stay under 4 billion rows, and BIGINT UNSIGNED for anything expected to grow beyond that. Don't use BIGINT everywhere by default — it doubles the storage cost per column and is slower to compare than INT.
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

SQL
-- 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'
Warning
Be careful with arithmetic on UNSIGNED columns in MySQL. If the result would be negative (e.g., a - b where a less than b), MySQL wraps around to a large positive number in strict mode off, or raises an error in strict mode. Enable SQL strict mode in production to catch these issues.
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).

SQL
-- 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:

SQL
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.

SQL
-- 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 |
+------+--------------------+------+--------------------+
Warning
Never use FLOAT or DOUBLE for financial data, prices, or any quantity where exact decimal representation matters. The rounding errors shown above are a fundamental limitation of binary floating-point representation — they cannot be avoided by using DOUBLE instead of FLOAT.
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

SQL
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
Note
DECIMAL columns have a higher CPU cost for arithmetic than FLOAT because MySQL must perform decimal arithmetic (not binary floating-point). This is almost always negligible — prefer exactness over micro-optimizing arithmetic speed.
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.

SQL
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
Tip
For permission systems, BIT fields are compact but hard to read. Consider using a separate permissions table with INT flags, or better yet a proper roles/permissions table with foreign keys. Reserve BIT for compact binary flag storage where the bit meanings are well-documented.
Overflow Behavior

What happens when you insert a value outside a column's range depends on MySQL's SQL mode.

SQL
-- 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';
Warning
Always run MySQL in strict mode in production. Add 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.

SQL
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