Data Types & Schema Design
Picking the right type and the right constraints is most of what makes a schema hard to misuse later.
Intermediate
| Type | Use for |
|---|---|
integer (4 bytes) / bigint (8 bytes) | whole numbers — use bigint for anything that might exceed ~2.1 billion, like a high-volume event-log primary key |
numeric(precision, scale) | exact decimal — always for money; never float, which rounds |
real / double precision | approximate floating point — scientific/measurement data where exactness isn't the point |
-- float rounding, the reason money is never a float:
SELECT 0.1::float + 0.2::float; -- 0.30000000000000004
SELECT 0.1::numeric + 0.2::numeric; -- 0.3, exactly
text and varchar(n) have identical performance — there's no storage or speed penalty to text. Use varchar(n) only when the length limit is a genuine business rule you want the database to enforce; otherwise, text plus a CHECK constraint if needed is more flexible.CREATE TABLE events (
id SERIAL PRIMARY KEY,
happened_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
timestamptz stores the instant in UTC internally and converts to/from the client's timezone on the way in and out; plain timestamp stores whatever local time you gave it with no timezone context at all, which becomes ambiguous the moment your app, database server, or users span more than one timezone. Use timestamptz by default.CREATE TABLE posts (
id SERIAL PRIMARY KEY,
tags TEXT[]
);
INSERT INTO posts (tags) VALUES (ARRAY['sql', 'postgresql', 'tutorial']);
SELECT * FROM posts WHERE 'sql' = ANY(tags);
SELECT * FROM posts WHERE tags @> ARRAY['sql']; -- contains
| Constraint | Enforces |
|---|---|
PRIMARY KEY | unique, not null, one per table — the row's identity |
FOREIGN KEY ... REFERENCES | a column's value must exist in another table's referenced column |
UNIQUE | no duplicate values in this column (or column combination) |
NOT NULL | column can never be NULL |
CHECK (expression) | arbitrary boolean condition, e.g. CHECK (price >= 0) |
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer_id INTEGER NOT NULL REFERENCES customers(id),
amount NUMERIC(10, 2) NOT NULL CHECK (amount >= 0),
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'shipped', 'cancelled'))
);
ON DELETE CASCADE (delete children too) or ON DELETE SET NULL depending on the relationship's meaning.