INT vs BIGINT vs SMALLINT
Short answer: SMALLINT is 2 bytes (~±32K), INT is 4 bytes (~±2.1B), BIGINT is 8 bytes (~±9.2 quintillion). Use INT for almost everything; use BIGINT for IDs on tables that might exceed 2 billion rows or for monetary values stored as cents on accounts that might cross billion-dollar lifetime totals; use SMALLINT only for genuinely small ranges where the storage savings matter (rare on modern engines).
Quick comparison #
SMALLINT | INT / INTEGER | BIGINT | |
|---|---|---|---|
| Bytes | 2 | 4 | 8 |
| Range | -32,768 to 32,767 | -2.1B to 2.1B | -9.2 quintillion to 9.2 quintillion |
| Unsigned variant (MySQL) | 0 to 65,535 | 0 to 4.3B | 0 to 1.8e19 |
| Use for | small enumerations, ages, counters where storage matters | the default for IDs, quantities, counts | row IDs on huge tables, microsecond timestamps, monetary amounts in cents |
| Default in modern schemas? | rare | yes | for primary keys on growth-likely tables |
See the storage difference #
When to use which #
INT is the default. Foreign keys, counts, quantities, ages, anything that comfortably fits in ±2.1B. The 4-byte size matches CPU register size on most architectures, which gives it slight performance advantages on bulk operations.
BIGINT — when 2.1 billion is plausibly close:
- Surrogate primary keys on event-style tables (logs, clicks, transactions).
SERIAL(4-byte) on a write-heavy table runs out faster than people expect. - Money stored as integer minor units (cents):
amount_cents BIGINTsurvives any account size;amount_cents INTcaps at $21.4M. - Microsecond/nanosecond timestamps stored as integers.
- Hash-derived IDs that need 64 bits.
SMALLINT — useful for genuinely small enumerations on huge tables where storage matters: a status code (1-10), a year-of-life column (0-150), a country index (200 values). Modern engines store INT efficiently enough that SMALLINT savings are usually negligible. Reach for it deliberately, not by default.
TINYINT / INT2 / INT8 are engine-specific aliases. TINYINT is 1 byte (0-255 unsigned, ±127 signed) on MySQL and SQL Server; doesn't exist in Postgres.
Common mistakes #
INTprimary keys on growth tables: the canonical "we'll fix it before we hit the limit" promise. By the time the table hits 1.5B rows, theALTER TABLEto widen the key takes hours and locks writers. Default toBIGINTfor any table where 2B rows is plausible (events, audit logs, transactions).INTfor cents on monetary columns:INTcaps at $21,474,836.47. Fine for individual transactions, dangerous for cumulative balances or aggregated revenue. UseBIGINTfor cents, or useDECIMALand skip the integer-cents trick. See DECIMAL vs FLOAT.- Mixing
INTandBIGINTin joins: silent type promotion toBIGINTworks but breaks index usage on some older engines. Match the types on FK columns. - Using unsigned
INTto "double the range" on MySQL: works, but only MySQL has unsigned types, so moving the schema to Postgres or Snowflake later requires conversion. Better to useBIGINTfrom the start.
Dialect notes #
- Postgres:
SMALLINT(2 bytes),INTEGER/INT/INT4(4 bytes),BIGINT/INT8(8 bytes).SERIAL,BIGSERIALfor auto-increment. No unsigned types. - MySQL:
TINYINT(1B),SMALLINT(2B),MEDIUMINT(3B — 24-bit),INT(4B),BIGINT(8B). Each has unsigned variant.AUTO_INCREMENTrequires the column to be a key. - SQL Server:
TINYINT(1B, 0-255 only — no signed),SMALLINT,INT,BIGINT.IDENTITY(seed, increment)for auto-increment. - SQLite: dynamic typing, so all integer types are stored as one variable-length
INTEGER. The declared type is hint-only. - Snowflake: all integer types (
TINYINT,SMALLINT,INT,BIGINT) are aliases forNUMBER(38,0). No real storage difference. Auto-increment viaAUTOINCREMENTorIDENTITY. - BigQuery: only
INT64(8 bytes). SynonymsINTEGER,INT,SMALLINT,BIGINTare accepted but all map toINT64. - DuckDB:
TINYINTthroughHUGEINT(16 bytes). Auto-increment viaSEQUENCE.
FAQ #
Should I use INT or BIGINT for a primary key?
BIGINT for any table that might grow large: events, transactions, audit logs, analytics tables. The 4-byte saving from INT is invisible on small tables and irrelevant on large ones. Migrating an INT PK to BIGINT on a billion-row table takes hours of locked writers and a deployment risk you didn't need to take.
When is SMALLINT actually useful?
When you have a column with a genuinely small range (status code 1-10, year-of-life 0-150, country index up to 200) on a huge table where every byte matters. Modern engines store INT efficiently enough that SMALLINT savings are usually negligible. Reach for it deliberately, not by default.
Can I store cents/pence as an integer instead of DECIMAL?
Yes, and many financial systems do. Use BIGINT (not INT) for cents to avoid the $21M cap. The benefit: no rounding errors. The downside: you have to remember the scale (always divide by 100 for display) and reasoning about partial cents (split bills, taxes) becomes harder. DECIMAL(15, 2) is usually simpler.
What's the maximum value for an INT?
Signed: ±2,147,483,647 (about 2.1 billion). Unsigned (MySQL): 0 to ~4.3 billion. BIGINT signed: ±9.2 quintillion. SMALLINT signed: ±32,767. Choose by what fits the column's domain plus a safety margin for growth.
Are there performance differences between INT and BIGINT?
Marginal on modern hardware. CPUs handle both natively. Storage is 4 bytes vs 8 bytes, a 2x difference per row but rarely material vs the rest of the row. Indexing performance is functionally equivalent. Don't pick INT over BIGINT for performance reasons; pick by domain fit.
What about TINYINT?
1-byte integer. MySQL: TINYINT (-128 to 127, or 0-255 unsigned). SQL Server: TINYINT (0-255 only — no signed). Postgres: no TINYINT; use SMALLINT. Useful for boolean-like flags or genuinely small ranges; on small tables the byte savings are negligible.
**Default to BIGINT for primary keys on any table that might grow large.** The 4-byte saving from INT is invisible on small tables and irrelevant on large ones (compared to the rest of the row). The cost of migrating from INT to BIGINT on a billion-row table is hours of locked writers and a deployment risk you didn't need to take. The cost of choosing BIGINT upfront is zero. The asymmetry strongly favors going wide from day one.