C.W.K.
Stream
Lesson 10 of 10 · published

Schema Design Habits

~15 min · schema, design

Level 0Schema Seedling
0 XP0/86 lessons0/10 achievements
0/120 XP to next level120 XP to go0% complete

The blueprint phase

Designing a schema is drawing the blueprint before pouring concrete. A good blueprint saves months of renovation later. A bad one means re-routing plumbing after the residents have moved in.

Six habits that keep schemas honest

  1. Normalise first; denormalise on measured pain. Every entity in its own table; foreign keys connecting them. Denormalisation (computed totals, duplicated names) is a performance optimisation, not a starting point.
  2. snake_case everywhere: users, order_items, created_at. Plural tables, singular columns, table_id for foreign keys. Consistency beats taste.
  3. Always include created_at and updated_at as TIMESTAMPTZ NOT NULL DEFAULT now(). Add a trigger that bumps updated_at on every UPDATE. You will need this for debugging, syncing, and audit. Add it now or regret it later.
  4. NOT NULL by default; allow NULL by exception.
  5. Constraints are documentation — every CHECK, UNIQUE, and FK encodes a rule the application can rely on.
  6. Soft delete instead of hard delete when audit trails matter — add deleted_at TIMESTAMPTZ and exclude rows with non-null deleted_at from queries. (Don't soft-delete by default; pick when it's worth it.)

The auto-bumping updated_at trigger

Worth memorising — you'll write this trigger on most tables.

Code

The 'always have these columns' template·sql
CREATE TABLE things (
    id         INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    -- ... your business columns here ...
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
Auto-bumping updated_at trigger·sql
CREATE OR REPLACE FUNCTION set_updated_at()
RETURNS TRIGGER LANGUAGE plpgsql AS $$
BEGIN
    NEW.updated_at = now();
    RETURN NEW;
END;
$$;

CREATE TRIGGER things_updated_at
BEFORE UPDATE ON things
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
Soft-delete pattern·sql
ALTER TABLE things ADD COLUMN deleted_at TIMESTAMPTZ;
CREATE INDEX things_active_idx ON things (id) WHERE deleted_at IS NULL;

-- Application convention: filter out deleted rows in views / reusable WHEREs.
CREATE VIEW active_things AS
  SELECT * FROM things WHERE deleted_at IS NULL;

External links

Exercise

Sketch the full schema (including types, constraints, indexes, foreign keys, and the standard timestamp/trigger setup) for a 'kanban board' app: boards have lists, lists have cards, cards have labels (many-to-many), and a board has many members (users) with roles.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.