Relationships
Almost every real schema is a network of tables connected by relationships, and there are exactly three shapes those relationships come in: one-to-one, one-to-many, and many-to-many. Recognizing which shape a relationship has tells you exactly how to implement it — where the foreign key goes, and whether a whole extra table is needed just to represent the relationship itself.
One-to-one
One-to-one means each row in table A corresponds to at most one row in table B, and vice versa. It's implemented by putting a foreign key in one of the two tables and adding a UNIQUE constraint on it — the UNIQUE is what turns an otherwise one-to-many foreign key into strictly one-to-one. A common real example is splitting a large, rarely-accessed set of columns off a users table into a separate profile table.
One-to-one: users and user_profiles
CREATE TABLE users ( user_id SERIAL PRIMARY KEY, email TEXT UNIQUE NOT NULL ); CREATE TABLE user_profiles ( profile_id SERIAL PRIMARY KEY, user_id INTEGER UNIQUE NOT NULL REFERENCES users (user_id), bio TEXT, avatar_url TEXT );
One-to-many
One-to-many means one row in table A can relate to many rows in table B, but each row in B relates back to only one row in A — a customer with many orders, an author with many blog posts. The foreign key lives on the "many" side, pointing back at the "one" side's primary key. This is the relationship the Foreign Keys page's orders/customers example already covers in depth.
One-to-many: customers and orders
CREATE TABLE customers ( customer_id SERIAL PRIMARY KEY, name TEXT NOT NULL ); CREATE TABLE orders ( order_id SERIAL PRIMARY KEY, customer_id INTEGER NOT NULL REFERENCES customers (customer_id), total_amount NUMERIC(10, 2) NOT NULL );
Many-to-many: junction tables
Many-to-many means a row in table A can relate to many rows in table B, and a row in table B can relate to many rows in table A — a product can have many tags, and a tag can apply to many products. Neither table can hold a single foreign key to the other, because either side would need to store a variable-length list of references. The standard solution is a junction table (also called a bridge or association table) sitting between them, holding one row per pairing, with a foreign key to each side.
Many-to-many: products, tags, and a product_tags junction table
CREATE TABLE products ( product_id SERIAL PRIMARY KEY, name TEXT NOT NULL ); CREATE TABLE tags ( tag_id SERIAL PRIMARY KEY, name TEXT UNIQUE NOT NULL ); CREATE TABLE product_tags ( product_id INTEGER NOT NULL REFERENCES products (product_id), tag_id INTEGER NOT NULL REFERENCES tags (tag_id), PRIMARY KEY (product_id, tag_id) ); INSERT INTO product_tags (product_id, tag_id) VALUES (1, 3), (1, 7), (2, 3);
Querying across a junction table just means joining through it — to find every tag on a product, or every product with a given tag, both directions go through product_tags.
Querying a many-to-many relationship through the junction table
-- All tags for product 1 SELECT t.name FROM tags t JOIN product_tags pt ON pt.tag_id = t.tag_id WHERE pt.product_id = 1; -- All products tagged "sale" SELECT p.name FROM products p JOIN product_tags pt ON pt.product_id = p.product_id JOIN tags t ON t.tag_id = pt.tag_id WHERE t.name = 'sale';
Relationship | Implementation |
|---|---|
One-to-one | Foreign key on one side, with a UNIQUE constraint on that foreign key column |
One-to-many | Plain foreign key on the "many" side, pointing at the "one" side's primary key |
Many-to-many | A junction table with a foreign key to each side, usually a composite primary key on both |
One-to-one: foreign key plus UNIQUE on one of the two tables.
One-to-many: plain foreign key on the "many" side.
Many-to-many: a junction table with a foreign key to each side.
A junction table can hold its own columns once the relationship itself needs attributes.