awesome-everything RU
↑ Back to the climb

Databases

Constraints, keys, and Postgres data types

Crux The five constraint kinds in depth, surrogate vs natural keys, SQL''''s gap from relational algebra, and how picking the narrowest Postgres type is itself a constraint.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at middle altitude — in the sky
◷ 16 min

A team stores money as REAL. After a year of accumulated transactions they find cents-level discrepancies they cannot explain. The type was the bug — float arithmetic loses precision. The fix is a schema change that touches every row.

Codd’s relational model vs SQL

Edgar Codd’s 1970 paper formalised relations as sets of tuples drawn from typed domains, and defined a closed algebra of operations (selection, projection, join, union, intersection, difference) — closed because each operation takes relations as input and produces a relation as output. SQL is a non-strict implementation of that algebra; it adds NULL (which Codd disliked), ORDER (rows are conceptually unordered), and duplicate rows (relations have no duplicates). Knowing the gap explains the rough edges:

  • NULL = NULL is NULL, not true — three-valued logic.
  • ORDER BY is required to guarantee row order — the engine may return any order without it.
  • DISTINCT exists because the engine must keep duplicates unless you ask.

Treat the gap as “SQL = relational algebra plus practical compromises” and the surprises stop being surprising.

The five constraint kinds

ConstraintWhat it enforcesKey detail
PRIMARY KEYUnique non-null identifier per rowOne per table; implicitly creates a unique B-tree index
UNIQUENo duplicate values in this column setMultiple NULLs allowed (standard SQL); opt into UNIQUE NULLS NOT DISTINCT (SQL:2023 / Postgres 15+) to block that
NOT NULLColumn always has a valuePer-column; the first line of data quality
FOREIGN KEYColumn references an existing PK/UNIQUE key in another tableON DELETE / ON UPDATE clauses: NO ACTION, RESTRICT, CASCADE, SET NULL, SET DEFAULT
CHECKArbitrary boolean expression on each row at write timeCHECK (amount >= 0), CHECK (status IN (‘open’,‘closed’)) — can reference other columns of the same row

Surrogate vs natural keys

A natural key is data that already exists in the business domain (user email, product SKU, order number). A surrogate key is database-generated, opaque, and meaningful only inside the database (BIGSERIAL, UUID).

Production default in 2026: surrogate key as the primary key, plus a UNIQUE NOT NULL constraint on the business-natural key. Why: natural keys change (a customer changes their email), and a primary key change cascades through every foreign key referencing it — operationally expensive and often impossible at scale. Surrogate keys never change.

The exception: pure join tables (favourites: user_id, item_id) often use the composite of foreign keys as the PK — the relationship itself is the identity, no surrogate needed.

UUID vs BIGSERIAL. UUIDs are globally unique (good for distributed inserts, multi-region, offline-first clients) but bigger (16 bytes vs 8) and worse for index locality (random UUIDv4 fragments B-trees). UUIDv7 (time-ordered, RFC 9562) fixes the locality issue and is the modern default where UUID is wanted. BIGSERIAL is smaller, sequential, and cache-friendly — pick it when globally-unique IDs are not needed.

Postgres data types: pick the narrowest that fits

Postgres has the richest type system of any mainstream database. The type is the first line of constraint — a correctly-typed column catches 80% of bad data before any CHECK constraint runs.

CategoryProduction defaultsAvoid
IntegersBIGINT (8B) for IDs; INTEGER (4B) when domain is bounded < ~2BSMALLINT unless you know domain is < 32,767
StringsTEXT (no length limit, no padding)CHAR(n) — pads to length, trailing-space surprises; VARCHAR(n) adds a check but no storage benefit
MoneyNUMERIC(p,s) or BIGINT cents — exact arithmeticREAL or DOUBLE PRECISION — IEEE 754 loses cents
TimeTIMESTAMPTZ (stores UTC, displays in session timezone)TIMESTAMP (no zone) — a footgun; DATE for date-only
IDsUUID native type (16 bytes)UUID as TEXT — wastes bytes, loses type enforcement
BooleansBOOLEANSMALLINT or TEXT for booleans — semantically wrong
Semi-structuredJSONB (binary, indexable)JSON (text only, not indexable)
Key and type numbers
Codd's paper
1970
BIGSERIAL per index entry
8 bytes
UUID per index entry
16 bytes
UUIDv7 vs UUIDv4 index locality
ordered vs random
FK constraint check overhead
~5-50 μs / row
JSONB GIN index size vs B-tree
~5-20x larger
Typical column storage overhead
~1-4 bytes / column
Composite PK index entry size
~24-48 bytes

Design a minimal e-commerce schema (users, products, orders)

1/3
Quiz

A team stores money as REAL (single-precision float) and notices that a year of accumulated transactions has cents-level discrepancies. The fix?

Quiz

Which is the strongest argument for using a surrogate primary key (BIGSERIAL or UUID) over a natural key (email)?

Recall before you leave
  1. 01
    Why is NULL = NULL not TRUE in SQL, and what does it equal?
  2. 02
    Name the ON DELETE options for a foreign key and when you use each.
  3. 03
    What is the production default for storing currency in Postgres, and why not REAL?
Recap

SQL is relational algebra plus practical compromises: NULL, ordering, and duplicates. The five constraint kinds (PRIMARY KEY, FOREIGN KEY, NOT NULL, UNIQUE, CHECK) encode business rules the engine refuses to break. The production default for primary keys is a surrogate (BIGSERIAL or UUIDv7) plus a UNIQUE NOT NULL on the business natural key — natural keys change, surrogate keys never do. Postgres types are the first line of constraint: NUMERIC for money, TIMESTAMPTZ for timestamps, TEXT for strings, JSONB (not JSON) for semi-structured data. Lesson 3 covers normalization — the discipline for removing redundancy from a schema.

Connected lessons
appears again in164
Continue the climb ↑Normal forms, denormalization, and why schemas stick
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.