Relationships & Keys

beginner primary-key foreign-key composite-key uuid referential-integrity cascade

Keys are the backbone of relational databases. They uniquely identify rows, link tables together, and enforce data integrity. Without keys, a relational database is just a bunch of spreadsheets with no connections.

Primary Keys

A primary key uniquely identifies each row in a table. No two rows can have the same primary key, and it can never be NULL.

Natural Keys vs Surrogate Keys

A natural key uses a real-world value that’s already unique — like an email address or a Social Security Number.

A surrogate key is an artificial value we generate — like an auto-incrementing integer or a UUID.

-- Natural key: using email (it's already unique)
CREATE TABLE users (
    email VARCHAR(255) PRIMARY KEY,
    name VARCHAR(100)
);

-- Surrogate key: auto-incrementing integer
CREATE TABLE users (
    id SERIAL PRIMARY KEY,  -- 1, 2, 3, 4...
    email VARCHAR(255) UNIQUE,
    name VARCHAR(100)
);

-- Surrogate key: UUID
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email VARCHAR(255) UNIQUE,
    name VARCHAR(100)
);

Auto-increment vs UUID — When to Use Which?

Auto-increment (SERIAL)
UUID
Small (4 or 8 bytes)
Large (16 bytes)
Sequential (great for B-tree indexes)
Random (can fragment indexes)
Predictable (security concern in URLs)
Unpredictable (safe in URLs)
Single DB only (conflicts in distributed)
Works in distributed systems

Rule of thumb: Use auto-increment for internal IDs. Use UUIDs when IDs are exposed in URLs or when working with distributed databases.

Foreign Keys

A foreign key is a column that references the primary key of another table. It’s how we create relationships between tables and enforce referential integrity.

Referential integrity means: if a row in the orders table says user_id = 5, there MUST be a user with id = 5 in the users table. The database enforces this automatically.

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100)
);

CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INT NOT NULL,
    total DECIMAL(10,2),
    -- Foreign key constraint
    CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id)
);

-- This WORKS (user 1 exists)
INSERT INTO users (id, name) VALUES (1, 'Manish');
INSERT INTO orders (user_id, total) VALUES (1, 99.99);

-- This FAILS (user 999 doesn't exist)
INSERT INTO orders (user_id, total) VALUES (999, 49.99);
-- ERROR: insert or update violates foreign key constraint "fk_user"

ON DELETE Behavior

What happens when we delete a parent row that has child rows referencing it? We have several options:

-- RESTRICT (default): prevent deletion if references exist
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT
-- "Can't delete this user — they have orders!"

-- CASCADE: delete child rows too
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
-- Delete the user → all their orders are automatically deleted too

-- SET NULL: set the FK to NULL
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
-- Delete the user → orders remain but user_id becomes NULL

-- SET DEFAULT: set the FK to its default value
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET DEFAULT
-- Delete the user → user_id is set to whatever DEFAULT is defined

Similarly, ON UPDATE CASCADE is useful when the referenced key might change:

-- If user's id changes, update all orders that reference it
FOREIGN KEY (user_id) REFERENCES users(id)
    ON DELETE CASCADE
    ON UPDATE CASCADE;

Which ON DELETE to Use?

  • CASCADE — when child data is meaningless without the parent (order items when an order is deleted)
  • RESTRICT — when deletion should be blocked (can’t delete a department if employees are assigned)
  • SET NULL — when the relationship is optional (the comment still exists, but the author is gone)

Composite Keys

A composite key is a primary key made of two or more columns together. Neither column is unique on its own, but the combination is.

-- A student can enroll in many courses
-- A course can have many students
-- The combination (student_id, course_id) is unique

CREATE TABLE enrollments (
    student_id INT REFERENCES students(id),
    course_id INT REFERENCES courses(id),
    enrolled_at TIMESTAMP DEFAULT NOW(),
    grade CHAR(2),
    PRIMARY KEY (student_id, course_id)  -- composite key
);

-- Student 1 can enroll in Course 101 and Course 102
-- But Student 1 can't enroll in Course 101 twice

Junction Tables for Many-to-Many

We can’t directly represent a many-to-many relationship with a foreign key. We need a junction table (also called a bridge table, join table, or linking table).

-- Students ←→ Courses (many-to-many)
-- A student takes many courses. A course has many students.

CREATE TABLE students (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100)
);

CREATE TABLE courses (
    id SERIAL PRIMARY KEY,
    title VARCHAR(200)
);

-- Junction table: the "glue" between them
CREATE TABLE student_courses (
    student_id INT REFERENCES students(id) ON DELETE CASCADE,
    course_id INT REFERENCES courses(id) ON DELETE CASCADE,
    enrolled_at TIMESTAMP DEFAULT NOW(),
    PRIMARY KEY (student_id, course_id)
);

-- Enroll student 1 in courses 1 and 2
INSERT INTO student_courses (student_id, course_id) VALUES (1, 1);
INSERT INTO student_courses (student_id, course_id) VALUES (1, 2);

-- Find all courses for a student
SELECT c.title
FROM courses c
JOIN student_courses sc ON c.id = sc.course_id
WHERE sc.student_id = 1;

Junction tables can also carry extra data about the relationship itself — like when the enrollment happened, what grade was received, etc.

Unique Constraints

Besides the primary key, we often need other columns to be unique:

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) UNIQUE,              -- single column unique
    username VARCHAR(50) UNIQUE,
    UNIQUE (first_name, last_name)          -- composite unique (rare)
);

The difference between a UNIQUE constraint and a PRIMARY KEY:

  • A table can have many UNIQUE constraints but only one PRIMARY KEY
  • UNIQUE columns can be NULL (and multiple NULLs are allowed in most databases)
  • PRIMARY KEY = UNIQUE + NOT NULL

Check Constraints

While we’re talking about constraints, CHECK constraints are worth mentioning:

CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    price DECIMAL(10,2) CHECK (price > 0),           -- must be positive
    discount DECIMAL(3,2) CHECK (discount BETWEEN 0 AND 1),  -- 0-100%
    status VARCHAR(20) CHECK (status IN ('active', 'archived', 'draft'))
);

Interview Tip

When designing a schema in an interview, always start by identifying the relationships and their cardinalities. For 1:N, put the FK on the “many” side. For M:N, create a junction table. Know the ON DELETE options (CASCADE, RESTRICT, SET NULL) and when to use each. And be ready to explain natural vs surrogate keys — most interviewers have a preference and like to debate it.