Tables Keys Relationships and Normalization

Relational Databases for Designers · 40 min

System Design · Module 3 · Lesson 1 of 4 · 40 min

Tables Keys Relationships and Normalization

Turn booking operations into a schema with identities, relationships and constraints, then distinguish historical facts from accidental duplicates.

Model the identity of the business fact and enforce the invariants that every writing path must obey.

01 The idea

The problem this lesson solves

A cinema screen has a seat named F7, but F7 can be sold again for tomorrow’s show. If your table makes seat_no globally unique, legitimate bookings fail. If it has no appropriate uniqueness rule, two rows may claim to represent the same sellable seat. Schema design begins by deciding what one row means, which facts identify it, and which impossible states the database must reject.

You should recognize API resources and failure responses. SQL examples introduce their own tables and constraints.

Primary keyThe columns identifying one row uniquely.
Foreign keyA constraint requiring a referenced identity to exist, under its configured rules.
NormalizationOrganizing facts to reduce accidental duplication and update anomalies.

02 Worked example

Give a show seat a complete identity

Tables Keys Relationships and Normalization identitiesShowsshow_id PKShow seatsshow_id + seat PKBookingsbooking_id PKCustomerscustomer_id PKFKFKTables Keys Relationships and Normalization compact diagram01Showsshow_id PKFK02Show seatsshow_id + seat PK03Bookingsbooking_id PKFK04Customerscustomer_id PK
Foreign keys point from the referencing table to its parent. A show seat is identified by both show_id and seat_no.
1
List access patternsRead a show’s seat map, claim one seat and list a customer’s bookings.
2
Choose identityUse (show_id,seat_no) for one sellable seat.
3
Add relationshipsReference shows and customers by stable keys rather than copied display names.
4
Add rulesReject invalid state values and duplicate identities at the database boundary.

03 The method

Understand the mechanism

Start with the operations and the meaning of a row

List the operations before drawing tables: display a show’s seat map, place a temporary hold, confirm a booking and list a customer’s bookings. These operations reveal that a physical seat, a seat at a particular show and a completed booking are related but different concepts.

A physical seat belongs to an auditorium. A show assigns a performance to a time and auditorium. A show-seat row represents one sellable occurrence of a physical seat. A booking records a customer’s accepted purchase. Mixing all of these into one row makes identity and updates ambiguous.

For the teaching model, use (show_id,seat_no) to identify a show seat. F7 at show 1 and F7 at show 2 are different rows. Seat labels must also be interpreted within their auditorium; the show’s identity provides that context in this simplified model.

Write a sentence above every proposed table: “one row represents …”. If the sentence contains two independent events or entities, reconsider the model. This small habit catches many accidental many-to-many relationships and repeated-history problems before SQL is written.

Choose keys for stability and business uniqueness

A primary key uniquely identifies a row and cannot contain null values. A natural key uses meaningful business attributes, such as the complete show-seat pair. A surrogate key supplies an independent identifier, such as booking_id. Both can be useful in the same schema.

A surrogate id does not remove the need for business uniqueness. Giving every show-seat row a random id while allowing repeated (show_id,seat_no) pairs still permits duplicate representations of one sellable seat. Keep a unique constraint on the business identity when that is the invariant.

Choose identifiers that remain stable when descriptive facts change. A customer’s name or email can change, may not be unique under the product’s rules, and can be unsuitable as a relationship key. A stable customer_id lets bookings refer to the customer without copying a mutable display field into every reference.

Key scope matters in multi-tenant systems too. If external code values are only unique within a tenant, the relevant identity includes tenant_id. Leaving it out of a lookup or constraint can create cross-tenant collisions or access defects. Identity and authorization remain separate concerns, but both must use the intended scope.

Represent relationships rather than repeating lists

A one-to-many relationship typically puts the parent key on the child: many shows reference one movie, and many bookings reference one customer. A foreign key checks that the referenced parent exists under its configured rules.

A many-to-many relationship needs an association. A booking can contain several show seats, and seats participate in booking history over time under a carefully defined state model. A booking_items table can store the booking reference and complete seat identity, plus facts that belong to that association.

Do not put a comma-separated list of seat ids into a column and expect the database to enforce each relationship. The list is difficult to join, validate, update concurrently and constrain. Rows in an association table expose each relationship to ordinary keys and constraints.

Decide deletion behavior explicitly. Deleting a customer should not accidentally erase legally or operationally required booking history through an unexamined cascade. You might restrict deletion, preserve a historical record, or anonymize selected data according to the product’s policy. The schema should express the chosen lifecycle rather than inherit an accidental default.

Normalize facts that must change together

Suppose every booking repeats customer_name and current_email. Changing one customer’s email now requires updating many rows. If only some updates succeed, the database contains conflicting claims about the same current fact. This is an update anomaly.

Normalization organizes facts according to the identities they depend on. Customer attributes belong with the customer; show timing belongs with the show; a line item’s charged price belongs with that purchase event. The goal is not to eliminate every repeated string. It is to avoid storing one current fact in several places without a maintenance rule.

Historical snapshots are different facts. A receipt’s charged price must not change when the current advertised price changes. An order may preserve a delivery address as it was accepted at purchase time. Calling these snapshots “bad duplication” would erase useful history.

Explain the dependency in plain language: this value describes the customer now, or this value records what was charged then. That explanation is more important for a designer than mechanically reciting normal-form names without connecting them to update behavior.

Put local invariants into executable constraints

Use NOT NULL for required values, CHECK for permitted local states or ranges, UNIQUE for the relevant identity, and foreign keys for references. A CHECK restricting state to free, held or booked prevents an application typo from creating a fourth undocumented state.

Constraints protect writes from every client, including maintenance scripts and future services. Relying only on an API’s validation leaves other write paths able to corrupt data. Application validation remains useful for clear errors, but the database is the final guard for invariants it can express.

Understand the boundary. A foreign key can show that customer_id exists; it does not prove the authenticated caller may book for that customer. A row-local CHECK cannot by itself enforce every rule involving several rows, time or external payment state. Those require appropriate transactions, locking, uniqueness patterns or workflow design.

Handle constraint violations as expected outcomes where concurrency can trigger them. Two app instances may both generate the same short code. A unique insert can select the winner; the loser can allocate another code within a bounded policy. A preliminary “is this free?” query is not equivalent to the constraint.

Denormalize only with a maintenance and repair path

After measuring a hot read, you may choose to store a derived count or duplicate a field to avoid repeated work. For example, an owner dashboard might display a link count maintained alongside its underlying links.

Ask how that count changes on create, delete, rollback and retry. If it is updated in the same transaction as the source rows, the transaction adds work and possible contention. If it is updated asynchronously, the product must tolerate lag and the consumer must handle duplicate events. Either way, include a rebuild procedure from authoritative data.

A denormalized value without a repair path becomes difficult to trust after an incident. Reconciliation can compare the projection with source data and correct drift. The design should identify which copy wins when values disagree.

The first schema should make ownership and invariants clear. Optimize specific access patterns after observing their cost. This produces a model you can reason about under concurrent changes rather than a collection of convenient fields that happen to work for the first screen.

Guided tutorial

  1. Create the minimal show-seat table

    Run the PostgreSQL setup below in a disposable practice database. It creates a complete composite key and a constrained state. This table isolates identity; it is not the whole cinema schema. Add a row with show_id=1, seat_no='F7', state='free'.

  2. Challenge the key

    Insert the same triple again and observe the duplicate-key error. Insert (2,'F7','free') and observe that it is permitted because the show differs. Try an unsupported state such as 'maybe' and observe the CHECK failure. Connect each outcome to a requirement rather than memorizing the error text.

  3. Extend the relationships on paper

    Add shows and customers with stable ids, then bookings referencing customers and booking items referencing the appropriate show-seat identity. Mark which table owns current customer email and which row owns charged price. Explain why updating the email should not rewrite historical amounts.

  4. Review one deliberate duplicate

    Propose a cached booking count for a dashboard. Write its update path, permitted staleness and rebuild query. If you cannot explain how to recover the count after a missed update, keep the normalized query until the performance requirement justifies the additional mechanism.

PostgreSQL setup

CREATE TABLE sd_schema_seats (
  show_id integer NOT NULL,
  seat_no text NOT NULL,
  state text NOT NULL CHECK (state IN ('free','held','booked')),
  PRIMARY KEY (show_id, seat_no)
);

Check your result

Inserting (1, F7, free) twice must fail on the second insert. Inserting (2, F7, free) is valid because the show differs. An unsupported state must fail the CHECK constraint.

04 Cheat sheet

Review the decisions

Use this table to explain the mechanism and its limit without memorizing a product name.

Choice or conceptRememberCheck
Primary keyStable row identityChoose the complete business identity
Foreign keyValid referenceDoes not replace authorization
Normalized current factOne update locationRequires joins for combined views
Historical snapshotValue at event timeMust not follow future edits

05 Where & why

Check the primary references

Use official references for protocol and implementation details. The questions here are teaching prompts.

Primary reference
PostgreSQL table constraints

Read PostgreSQL table constraints

State the implementation version and the workload assumptions when defending your own design.

06 Interview questions

Explain the reasoning out loud

Answer before opening the explanation. The questions progress from vocabulary to decisions and failure behavior.

What does Primary key mean?
The columns identifying one row uniquely.
What does Foreign key mean?
A constraint requiring a referenced identity to exist, under its configured rules.
What does Normalization mean?
Organizing facts to reduce accidental duplication and update anomalies.
Why not use seat_no as the primary key?
The label repeats across shows. The identity must include the show or another key identifying that sellable occurrence.
What is an update anomaly?
Copies of the same current fact can disagree after only some rows are updated. Storing that fact once avoids the inconsistent update paths.
Is every duplicate value denormalization?
No. Two fields can represent different facts, such as current price and charged price at booking time. Their meanings and update rules differ.
What does a foreign key not prove?
It does not prove caller permission or every business rule. It enforces the configured relationship between referenced identities.
When should you denormalize?
When a measured access pattern benefits enough to justify duplicated state. Define its consistency and repair path before relying on the faster read.

07 Practice problems

Practice a new case

Solve the new cases before revealing hints. Complete worked solutions follow the set.

Pick the identity

Easy
Choose a key for student attendance at many class sessions.
Show the hint
Include the event as well as the person.

Name the relation

Easy
Model students enrolled in many courses, each with many students.
Show the hint
The association can carry its own facts.

Correct the key

Medium
Explain why UNIQUE(seat_no) rejects legitimate cinema data.
Show the hint
The label can recur at different shows.

Keep a snapshot

Medium
Decide whether a receipt should use current product price or stored charged price.
Show the hint
A receipt records a past event.

Find an anomaly

Medium
A table repeats customer_email on 50 bookings; the customer changes it and only 20 rows update. Explain the defect.
Show the hint
The copies claim to represent one current fact.

Defend a schema

Hard
Design keys and constraints for a library with multiple physical copies of each book.
Show the hint
Book title and lendable copy are different identities.

Worked solutions

1. Pick the identity

A pair such as (session_id,student_id) identifies one attendance fact. A student id alone permits only one record for all sessions.

2. Name the relation

Use enrollments with student_id and course_id, plus fields such as enrolled_at and status. Apply a suitable uniqueness rule for one active or historical enrollment contract.

3. Correct the key

It wrongly makes F7 globally unique. Scope uniqueness to (show_id,seat_no) or the complete occurrence identity.

4. Keep a snapshot

Store the actual charged price on the line item. A future catalogue change must not alter the receipt’s historical amount.

5. Find an anomaly

The same current email now has conflicting values. Keep current email on the customer row or explicitly label immutable contact snapshots if those are the intended facts.

6. Defend a schema

Model book metadata, physical copies and loans separately. A copy has a stable copy id; loans reference a copy and borrower. Enforce the chosen active-loan uniqueness and retain returned-loan history without making titles globally unique.