Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

SQL (PostgreSQL, MySQL, SQLite)  Module 5 – Joining tables

Primary keys, foreign keys and relationships

What primary and foreign keys enforce in PostgreSQL, MySQL and SQLite, how to spot one-to-one, one-to-many and many-to-many links, and why data is split up.

  • Beginner
  • 20 minutes
  • Examples run with sql.js 1.14.2
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Identify the primary and foreign keys of a schema, including keys made of two columns
  • Classify a link between two tables as one-to-one, one-to-many or many-to-many
  • Explain why a database stores each fact once and splits data across tables
  • Check which key rules PostgreSQL, MySQL and SQLite enforce, and when

Before you start

What you’ll learn · Before you start

What you will learn

  • Identify the primary and foreign keys of a schema, including keys made of two columns
  • Classify a link between two tables as one-to-one, one-to-many or many-to-many
  • Explain why a database stores each fact once and splits data across tables
  • Check which key rules PostgreSQL, MySQL and SQLite enforce, and when

Before you start

On this page

A shop’s data does not sit in one big table. Customers, orders and products each get a table of their own, and keys link the tables together. A primary key is the column, or the set of columns, whose value picks out exactly one row of its table. A foreign key is a column that holds the primary key of a row in another table: orders.customer_id holds the customer_id of the customer who placed the order. Where the foreign key sits, and whether it has to be unique, decides the relationship between two tables: one-to-many, one-to-one or many-to-many. Every join in this module follows one of these links, so reading them correctly comes first.

To try an example yourself, press Run under it: its mart tables are created from scratch each time by the sql.js 1.14.2 build of SQLite, version 3.49.1, on this page. PostgreSQL 18 and MySQL 8.4 behave differently in a few places that matter, and the lesson says where.

A primary key picks out one row

A primary key makes two promises: no two rows share a key value, and no row goes without one. A table has at most one primary key, made of one column or of several together. Try breaking the first promise:

A second customer with the same key SQL · primary_key.sql
-- customer_id is the primary key: it names exactly one customer.
CREATE TABLE customers (
  customer_id INTEGER PRIMARY KEY,
  name        TEXT    NOT NULL,
  city        TEXT    NOT NULL
);
INSERT INTO customers (customer_id, name, city) VALUES
  (1, 'Asha Kulkarni', 'Pune'),
  (2, 'Vikram Nair',   'Kochi');

-- A second customer 1 is refused, and the table keeps its two rows.
INSERT INTO customers (customer_id, name, city) VALUES (1, 'Asha K.', 'Mumbai');
SELECT * FROM customers;

Output (exit status 1)

┌─────────────┬───────────────┬───────┐
│ customer_id │     name      │ city  │
├─────────────┼───────────────┼───────┤
│ 1           │ Asha Kulkarni │ Pune  │
│ 2           │ Vikram Nair   │ Kochi │
└─────────────┴───────────────┴───────┘

Printed as an error (standard error)

Runtime error near line 12: UNIQUE constraint failed: customers.customer_id

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < primary_key.sql

SQLite refuses the second customer 1 with “UNIQUE constraint failed”, and the table still holds its two rows: a statement that breaks a constraint changes nothing. PostgreSQL reports the same mistake as a duplicate key value that violates the constraint customers_pkey, and MySQL as error 1062, “Duplicate entry ‘1’ for key ‘customers.PRIMARY’”.

A key can span columns. In order_items the key is (order_id, product_id): an order holds many products and a product sits in many orders, so each column repeats on its own, but a given pair appears only once. That pair is what stops the same product from being listed twice in one order.

SQLite lets NULL into some primary keys

Standard SQL never allows NULL in a primary key. SQLite departs from that to stay compatible with programs written for its early versions, which had a bug here:

NULL in a text primary key SQL · null_in_primary_key.sql
-- A text primary key in an ordinary SQLite table: NULL gets in, twice.
CREATE TABLE coupons (
  code        TEXT    PRIMARY KEY,
  discount_pc INTEGER NOT NULL
);
INSERT INTO coupons (code, discount_pc) VALUES ('DIWALI10', 10), (NULL, 15), (NULL, 20);
SELECT count(*) AS coupons, count(code) AS with_a_code FROM coupons;

-- With NOT NULL written out, SQLite refuses NULL as PostgreSQL and MySQL do.
CREATE TABLE coupons_checked (
  code        TEXT    NOT NULL PRIMARY KEY,
  discount_pc INTEGER NOT NULL
);
INSERT INTO coupons_checked (code, discount_pc) VALUES (NULL, 15);

Output (exit status 1)

┌─────────┬─────────────┐
│ coupons │ with_a_code │
├─────────┼─────────────┤
│ 3       │ 1           │
└─────────┴─────────────┘

Printed as an error (standard error)

Runtime error near line 14: NOT NULL constraint failed: coupons_checked.code

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < null_in_primary_key.sql

Three coupons went in, and only one of them has a code. In an ordinary SQLite table, a primary key column accepts NULL unless the column is an INTEGER PRIMARY KEY, is declared NOT NULL, or belongs to a STRICT or WITHOUT ROWID table. Uniqueness does not stop the second NULL either, because two NULLs never count as equal. PostgreSQL and MySQL mark every primary key column NOT NULL by themselves. In SQLite, write NOT NULL next to any primary key that is not an INTEGER PRIMARY KEY, as the second table does.

Choosing what the key is

A natural key is a value that people outside the database already use to tell things apart, such as a passport number, the barcode on a product or a user’s login name. A surrogate key carries no meaning at all: the database makes it up, the way it numbers customer_id. Foreign keys copy the key into other tables, so a key that changes has to change in every one of them. People change their email addresses and phone numbers; a number the database made up never needs to change. That is why most schemas give each table a surrogate key and keep natural values unique with a UNIQUE constraint. The schema design module comes back to this choice.

A foreign key points to a row of another table

Writing customer_id INTEGER NOT NULL REFERENCES customers (customer_id) makes orders.customer_id a foreign key. orders is the child table, the one that refers, and customers is the parent table, the one referred to. Three rules follow:

  • Every value in the child column must exist as a key in the parent. NULL is allowed unless the column is NOT NULL, and it means that the row has no parent.
  • A parent row cannot be deleted while child rows point to it, unless the foreign key names an action such as ON DELETE CASCADE. The module on constraints covers those actions.
  • The parent’s columns must be its primary key or carry a UNIQUE constraint, so that a value points to one row and not to several. PostgreSQL rejects any other foreign key with “there is no unique constraint matching given keys”, and MySQL 8.4 refuses it too unless the deprecated setting restrict_fk_on_non_standard_key is switched off.

SQLite adds a rule of its own, and it surprises most people who meet it:

Foreign keys before and after PRAGMA foreign_keys SQL · foreign_keys_off.sql
CREATE TABLE customers (
  customer_id INTEGER PRIMARY KEY,
  name        TEXT    NOT NULL
);
CREATE TABLE orders (
  order_id    INTEGER PRIMARY KEY,
  customer_id INTEGER NOT NULL REFERENCES customers (customer_id),
  status      TEXT    NOT NULL
);
INSERT INTO customers (customer_id, name) VALUES (1, 'Asha Kulkarni'), (2, 'Vikram Nair');

-- A new SQLite connection does not check foreign keys: an order for customer 99 goes in.
PRAGMA foreign_keys;
INSERT INTO orders (order_id, customer_id, status) VALUES (101, 1, 'delivered'), (102, 99, 'placed');

-- Switched on, the checks refuse the same mistake, and a customer who still has orders.
PRAGMA foreign_keys = ON;
INSERT INTO orders (order_id, customer_id, status) VALUES (103, 98, 'placed');
DELETE FROM customers WHERE customer_id = 1;

-- Order 102 got in before the checks were on. foreign_key_check finds it.
SELECT * FROM pragma_foreign_key_check('orders');

Output (exit status 1)

┌──────────────┐
│ foreign_keys │
├──────────────┤
│ 0            │
└──────────────┘
┌────────┬───────┬───────────┬──────┐
│ table  │ rowid │  parent   │ fkid │
├────────┼───────┼───────────┼──────┤
│ orders │ 102   │ customers │ 0    │
└────────┴───────┴───────────┴──────┘

Printed as an error (standard error)

Runtime error near line 18: FOREIGN KEY constraint failed
Runtime error near line 19: FOREIGN KEY constraint failed

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < foreign_keys_off.sql

Step by step:

  • PRAGMA foreign_keys returns 0: a new SQLite connection does not check foreign keys, for compatibility with old programs. The order for customer 99 went in, an order that belongs to nobody.
  • After PRAGMA foreign_keys = ON, the order for customer 98 is refused, and so is deleting Asha, who still has order 101.
  • Switching the checks on does not look at rows that are already there. pragma_foreign_key_check('orders') lists every row that breaks a foreign key: the table, the row’s rowid (here the order_id, 102), the parent table and the number of the foreign key that failed.

The setting belongs to the connection, not to the database file. Every program that opens the file has to switch it on again, and data written by a program that did not can already hold such orphans.

Warning

MySQL 8.4 accepts REFERENCES written after a column, as in the tables above, and then ignores it: no foreign key is created, nothing is checked, and an order for customer 99 goes in without a word. SHOW CREATE TABLE orders shows no CONSTRAINT line. Write the foreign key as a clause of its own, which all three engines accept: FOREIGN KEY (customer_id) REFERENCES customers (customer_id). MySQL then refuses the orphan with error 1452, Cannot add or update a child row: a foreign key constraint fails.

Index the foreign key column

Looking up the children of a parent, to check a delete or to join the two tables, reads the child column. PostgreSQL and SQLite do not index that column for you, and both manuals recommend adding an index. MySQL needs one for every foreign key, and creates it when the table has none. The module on indexes shows what the index changes.

Three shapes of relationship

The diagram draws the three shapes with mart tables, showing only their key columns.

One-to-many, one-to-one with a UNIQUE foreign key, and many-to-many through a junction table, drawn with mart tables.One-to-manyOne-to-oneMany-to-manycustomerscustomer_idINTEGERPKordersorder_idINTEGERPKcustomer_idINTEGERFKordersorder_idINTEGERPKinvoicesinvoice_noTEXTPKorder_idINTEGERFK, UNQordersorder_idINTEGERPKorder_itemsorder_idINTEGERPK, FKproduct_idINTEGERPK, FKquantityINTEGERproductsproduct_idINTEGERPKorders.customer_idinvoices.order_id, UNIQUEorder_items.order_idorder_items.product_id

Where the foreign key goes in each kind of relationship

Text description of the diagram

The diagram has three panels, one under the other. Each shows tables with their key columns, and each line is labelled with the foreign key it stands for. PK marks a primary key column, FK a foreign key column and UNQ a column that must be unique. Two bars at the end of a line mean exactly one, a circle with one bar means zero or one, and a circle with a crow's foot means zero or many.

  1. One-to-many: orders (order_id, PK; customer_id, FK) above customers (customer_id, PK), joined by orders.customer_id. Each order belongs to exactly one customer, and a customer has zero or many orders. The foreign key sits in the table on the many side.
  2. One-to-one: invoices (invoice_no, PK; order_id, FK and UNQ) above orders (order_id, PK), joined by invoices.order_id, which is UNIQUE. Each invoice belongs to exactly one order, and an order has zero or one invoice, because an order_id may appear only once in invoices.
  3. Many-to-many: orders (order_id, PK) at the top, order_items (order_id, PK and FK; product_id, PK and FK; quantity) in the middle and products (product_id, PK) at the bottom. order_items.order_id joins it to orders and order_items.product_id to products. Each order item belongs to exactly one order and one product; an order has zero or many items, and a product appears in zero or many items. The junction table order_items holds one row per pair of an order and a product, so the many-to-many link becomes two one-to-many links.

One-to-many

This is the most common shape. The foreign key sits on the “many” side: one customer has many orders, so orders carries customer_id, while customers stores nothing about orders. NOT NULL on the foreign key says that every order must have a customer; without it, an order could have none.

One-to-one

A foreign key that is also unique can name each parent row only once, so a parent has at most one child:

UNIQUE turns one-to-many into one-to-one SQL · one_to_one.sql
PRAGMA foreign_keys = ON;
CREATE TABLE orders (
  order_id INTEGER PRIMARY KEY,
  status   TEXT    NOT NULL
);
-- UNIQUE on the foreign key: an order can have one invoice at most.
CREATE TABLE invoices (
  invoice_no TEXT    NOT NULL PRIMARY KEY,
  order_id   INTEGER NOT NULL UNIQUE REFERENCES orders (order_id)
);
INSERT INTO orders (order_id, status) VALUES (101, 'delivered'), (102, 'delivered'), (103, 'placed');
INSERT INTO invoices (invoice_no, order_id) VALUES ('MRT-0001', 101), ('MRT-0002', 102);

-- A second invoice for order 101 would make the link one-to-many.
INSERT INTO invoices (invoice_no, order_id) VALUES ('MRT-0003', 101);
SELECT * FROM invoices;

Output (exit status 1)

┌────────────┬──────────┐
│ invoice_no │ order_id │
├────────────┼──────────┤
│ MRT-0001   │ 101      │
│ MRT-0002   │ 102      │
└────────────┴──────────┘

Printed as an error (standard error)

Runtime error near line 15: UNIQUE constraint failed: invoices.order_id

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < one_to_one.sql

The second invoice for order 101 is refused. Tables are split one-to-one when the extra columns are optional and most rows would leave them empty, when they are read far less often than the rest, or when they need different permissions.

Many-to-many

This shape does not fit into either table, because a column holds one value per row: an order cannot list its products in one column, nor a product its orders. A junction table holds one row per pair, with a foreign key to each side. order_items is the junction table of orders and products. Its primary key is the pair, and it carries facts about the pair, such as the quantity.

Reading a relationship

From the data

Counting from both ends of a link shows how it is used:

Counting each link from both ends SQL · link_counts.sql
CREATE TABLE orders (
  order_id    INTEGER PRIMARY KEY,
  customer_id INTEGER NOT NULL,
  status      TEXT    NOT NULL
);
CREATE TABLE order_items (
  order_id   INTEGER NOT NULL,
  product_id INTEGER NOT NULL,
  quantity   INTEGER NOT NULL,
  PRIMARY KEY (order_id, product_id)
);
INSERT INTO orders (order_id, customer_id, status) VALUES
  (101, 1, 'delivered'), (102, 2, 'delivered'), (103, 1, 'placed'), (104, 3, 'cancelled');
INSERT INTO order_items (order_id, product_id, quantity) VALUES
  (101, 1, 2), (101, 2, 1), (102, 4, 1), (103, 3, 2), (103, 5, 1), (104, 2, 3);

-- Customers to orders: how many orders per customer?
SELECT customer_id, count(*) AS orders
FROM orders
GROUP BY customer_id
ORDER BY customer_id;

-- Orders to products, read from both ends of order_items.
SELECT order_id, count(*) AS products
FROM order_items
GROUP BY order_id
ORDER BY order_id;

SELECT product_id, count(*) AS orders
FROM order_items
GROUP BY product_id
ORDER BY product_id;

Output

┌─────────────┬────────┐
│ customer_id │ orders │
├─────────────┼────────┤
│ 1           │ 2      │
│ 2           │ 1      │
│ 3           │ 1      │
└─────────────┴────────┘
┌──────────┬──────────┐
│ order_id │ products │
├──────────┼──────────┤
│ 101      │ 2        │
│ 102      │ 1        │
│ 103      │ 2        │
│ 104      │ 1        │
└──────────┴──────────┘
┌────────────┬────────┐
│ product_id │ orders │
├────────────┼────────┤
│ 1          │ 1      │
│ 2          │ 2      │
│ 3          │ 1      │
│ 4          │ 1      │
│ 5          │ 1      │
└────────────┴────────┘

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < link_counts.sql

Customer 1 has two orders, while each order has exactly one customer_id: one-to-many. Order 101 holds two products and product 2 appears in two orders, so order_items connects many to many. Data only shows what happens to be stored today, though. A count of 1 on both sides does not make a link one-to-one; only a UNIQUE constraint does, and that lives in the schema.

From the catalog

The catalog tells you what the schema allows. For the invoices table of the one-to-one example:

A foreign key covered by a unique index SQL · unique_links.sql
CREATE TABLE orders (
  order_id INTEGER PRIMARY KEY,
  status   TEXT    NOT NULL
);
CREATE TABLE invoices (
  invoice_no TEXT    NOT NULL PRIMARY KEY,
  order_id   INTEGER NOT NULL UNIQUE REFERENCES orders (order_id)
);

-- Where does invoices point, and which of its columns must be unique?
SELECT "from", "table", "to" FROM pragma_foreign_key_list('invoices');
SELECT name, "unique", origin FROM pragma_index_list('invoices') ORDER BY name;
SELECT name AS column_name FROM pragma_index_info('sqlite_autoindex_invoices_2');

Output

┌──────────┬────────┬──────────┐
│   from   │ table  │    to    │
├──────────┼────────┼──────────┤
│ order_id │ orders │ order_id │
└──────────┴────────┴──────────┘
┌─────────────────────────────┬────────┬────────┐
│            name             │ unique │ origin │
├─────────────────────────────┼────────┼────────┤
│ sqlite_autoindex_invoices_1 │ 1      │ pk     │
│ sqlite_autoindex_invoices_2 │ 1      │ u      │
└─────────────────────────────┴────────┴────────┘
┌─────────────┐
│ column_name │
├─────────────┤
│ order_id    │
└─────────────┘

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < unique_links.sql

pragma_foreign_key_list says that invoices.order_id points to orders.order_id. pragma_index_list lists the table’s indexes: origin is pk for the index of the primary key and u for one that a UNIQUE constraint made. pragma_index_info names the columns of an index, here order_id alone. A foreign key whose columns are exactly the columns of a unique index is a one-to-one link. In PostgreSQL and MySQL the same facts are in information_schema.table_constraints and information_schema.key_column_usage.

Why split data across tables

Splitting looks like extra work until you see what a single wide table does when facts change:

One fact stored on many rows SQL · repeated_facts.sql
-- One wide table: every line of an order repeats who bought it and where they live.
CREATE TABLE order_lines (
  order_id INTEGER,
  customer TEXT,
  city     TEXT,
  product  TEXT,
  quantity INTEGER
);
INSERT INTO order_lines VALUES
  (101, 'Asha Kulkarni', 'Pune',  'Basmati rice 1 kg', 2),
  (101, 'Asha Kulkarni', 'Pune',  'Toor dal 500 g',    1),
  (102, 'Vikram Nair',   'Kochi', 'Coconut oil 1 L',   1),
  (103, 'Asha Kulkarni', 'Pune',  'Masala chai 250 g', 2),
  (103, 'Asha Kulkarni', 'Pune',  'Ragi flour 1 kg',   1);

-- Asha moves to Mumbai. The update reaches the lines of one order and misses the other.
UPDATE order_lines SET city = 'Mumbai' WHERE order_id = 101;

SELECT DISTINCT customer, city
FROM order_lines
ORDER BY customer, city;

Output

┌───────────────┬────────┐
│   customer    │  city  │
├───────────────┼────────┤
│ Asha Kulkarni │ Mumbai │
│ Asha Kulkarni │ Pune   │
│ Vikram Nair   │ Kochi  │
└───────────────┴────────┘

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < repeated_facts.sql

The wide table writes Asha’s city on every line of every order she placed. The update changed it on the lines of order 101 and missed order 103, so the table now says she lives in two cities at once. This is an update anomaly, and it has two relatives:

  • Insertion: a customer who has not ordered yet cannot be stored, because every row is an order line.
  • Deletion: deleting a customer’s only order also deletes the only record of the customer.

In the mart tables a city is written once, in one customers row, and each order stores just the customer_id. Moving Asha to Mumbai is one UPDATE of one row, and a join puts the facts back together when a query needs them. A rule of thumb: when the same fact is written on many rows, it belongs in a table of its own. The design module turns that rule into the normal forms.

How the engines differ

Keys in PostgreSQL, MySQL and SQLite
Criterion PostgreSQL 18MySQL 8.4 (InnoDB)SQLite 3.49
Checks foreign keys AlwaysYes, while foreign_key_checks is 1 (the default)Only after PRAGMA foreign_keys = ON, on each connection
REFERENCES written after a column A foreign keyIgnored: no foreign keyA foreign key
FOREIGN KEY (…) REFERENCES … as its own clause A foreign keyA foreign keyA foreign key
NULL in a primary key column RefusedRefusedAccepted unless INTEGER PRIMARY KEY, NOT NULL, STRICT or WITHOUT ROWID
Index on the foreign key column Not created; add oneCreated when missingNot created; add one
Columns a foreign key may refer to Primary key, UNIQUE or a unique indexPrimary key or a unique keyPrimary key, UNIQUE or a unique index
SQLite Online (SQL Playground) Open your own SQLite database file and inspect its keys with the pragma functions of this lesson, in your browser.

Interview questions

Fresher · What is the difference between a primary key and a UNIQUE constraint?

Both stop two rows from sharing a value. A table has at most one primary key but any number of UNIQUE constraints. Primary key columns can never hold NULL, while a UNIQUE column accepts NULLs, and by default two NULLs do not count as duplicates, so many rows may leave the column empty. The primary key is the row’s identity: a foreign key written without a column list refers to it in PostgreSQL and SQLite, and in MySQL’s InnoDB engine the table’s rows are stored in primary key order. Use the primary key for the identifier that other tables point to, and UNIQUE for other values that must not repeat, such as an email address. One SQLite exception: an ordinary table accepts NULL in a primary key column that is not INTEGER PRIMARY KEY unless it is declared NOT NULL.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A production orders table has a customer_id column but no foreign key. Before adding one, you find 43,977 orders whose customer row no longer exists. How do you add the constraint without a long outage, and what do you do with those orders?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    Users of a new app sign in with their mobile number, and a teammate wants that number to be the primary key of the users table. Do you agree? Defend your answer and say what you would enforce instead.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A primary key is unique and never NULL, at most one per table, and it may span several columns, as (order_id, product_id) does.
  • A foreign key holds a parent’s key, which must be the parent’s primary key or unique; NULL means “no parent” unless the column is NOT NULL.
  • SQLite checks foreign keys only after PRAGMA foreign_keys = ON on each connection, and pragma_foreign_key_check finds rows that slipped in before.
  • MySQL 8.4 ignores REFERENCES written after a column; the separate FOREIGN KEY (…) REFERENCES … clause works in every engine.
  • The foreign key sits on the many side of a one-to-many link; a unique foreign key makes a link one-to-one; a junction table with two foreign keys stores a many-to-many link.
  • Store each fact once: repeated facts drift apart when one copy changes and the others do not.

Exercise

Exercise · Easy · SQL

The primary key of a junction table

A school's database has a junction table, enrolments, between students and courses. Somebody else created it, and before you write queries against it you want to know which columns form its primary key.

Write a query on SQLite's catalog that returns the primary key columns of enrolments, one row per column, in the order they have in the key. Return two columns:

  • column_name: the column's name;
  • key_position: its position in the primary key, counting from 1.

The tests create enrolments in different ways, so read the answer from the catalog instead of writing column names into your query.

Starter code · key_columns.sql

-- Every column of enrolments, with what the catalog says about it.
SELECT name, type, "notnull", pk
FROM pragma_table_info('enrolments');
The sample tests · tests.yaml
# Sample tests: each one creates the tables of a school database, runs your query on it and compares the rows it
# returns, in order.
tests:
  - name: returns both columns of a two-column key, in key order
    ordered: true
    setup: |
      CREATE TABLE students (student_id INTEGER PRIMARY KEY, name TEXT NOT NULL);
      CREATE TABLE courses (course_id INTEGER PRIMARY KEY, title TEXT NOT NULL);
      CREATE TABLE enrolments (
        student_id  INTEGER NOT NULL REFERENCES students (student_id),
        course_id   INTEGER NOT NULL REFERENCES courses (course_id),
        enrolled_on TEXT    NOT NULL,
        PRIMARY KEY (student_id, course_id)
      );
    expected:
      columns: [column_name, key_position]
      rows:
        - ['student_id', 1]
        - ['course_id', 2]
  - name: follows the order of the PRIMARY KEY clause, not the order of the columns
    ordered: true
    setup: |
      CREATE TABLE students (student_id INTEGER PRIMARY KEY, name TEXT NOT NULL);
      CREATE TABLE courses (course_id INTEGER PRIMARY KEY, title TEXT NOT NULL);
      CREATE TABLE enrolments (
        student_id INTEGER NOT NULL REFERENCES students (student_id),
        semester   INTEGER NOT NULL,
        course_id  INTEGER NOT NULL REFERENCES courses (course_id),
        grade      TEXT,
        PRIMARY KEY (course_id, semester, student_id)
      );
    expected:
      columns: [column_name, key_position]
      rows:
        - ['course_id', 1]
        - ['semester', 2]
        - ['student_id', 3]
  - name: returns only the surrogate key when the pair is merely UNIQUE
    ordered: true
    setup: |
      CREATE TABLE students (student_id INTEGER PRIMARY KEY, name TEXT NOT NULL);
      CREATE TABLE courses (course_id INTEGER PRIMARY KEY, title TEXT NOT NULL);
      CREATE TABLE enrolments (
        enrolment_id INTEGER PRIMARY KEY,
        student_id   INTEGER NOT NULL REFERENCES students (student_id),
        course_id    INTEGER NOT NULL REFERENCES courses (course_id),
        UNIQUE (student_id, course_id)
      );
    expected:
      columns: [column_name, key_position]
      rows:
        - ['enrolment_id', 1]
A hint

Run the starter query and look at the pk column of pragma_table_info('enrolments'): it says whether each column is part of the primary key, and where in the key it stands.

The sample tests run on this device, in your browser (sql.js): nothing is sent to mysmartcopilot.com. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 Which of these pairs of tables need a junction table between them?

    Choose every answer that is right.

    Show the answer to question 1

    Answer:

    • Films and actors
    • Books and their authors, when a book can have several authors
    • Students and the courses they take, when a student takes several courses

    A junction table is needed when both sides can have many of the other: a student takes many courses and a course has many students, a book has several authors who write several books, and a film has many actors who act in many films. Orders and customers are one-to-many (the foreign key goes in orders), orders and invoices are one-to-one (a unique foreign key in invoices), and employees and departments are one-to-many (the foreign key goes in employees).

  2. Question 2 of 6 A customer places many orders, and each order has one customer. Where does the foreign key go?

    Choose one answer.

    Show the answer to question 2

    Answer: In orders, as a customer_id column

    The foreign key sits on the many side. Each order names its one customer in orders.customer_id. A column in customers could hold only one order per customer, and a junction table is only needed when both sides can have many.

  3. Question 3 of 6 In an ordinary SQLite table, which column definition lets two rows store NULL in the primary key?

    Choose one answer.

    Show the answer to question 3

    Answer: code TEXT PRIMARY KEY

    SQLite keeps an old bug for compatibility: a primary key column accepts NULL unless it is an INTEGER PRIMARY KEY, is declared NOT NULL, or belongs to a STRICT or WITHOUT ROWID table. NULLs never count as equal, so the second NULL is not a duplicate either. An INTEGER PRIMARY KEY turns a NULL into a new row number instead.

  4. Question 4 of 6 In MySQL 8.4 a table was created with customer_id INT NOT NULL REFERENCES customers (customer_id). An order for a customer who does not exist was inserted without an error. Why?

    Choose one answer.

    Show the answer to question 4

    Answer: MySQL parses REFERENCES written after a column and ignores it, so no foreign key exists

    MySQL 8.4 accepts an inline REFERENCES clause as a comment and creates no constraint; SHOW CREATE TABLE shows no foreign key. Written as its own clause, FOREIGN KEY (customer_id) REFERENCES customers (customer_id), the key is created and checked: foreign_key_checks is 1 by default.

  5. Question 5 of 6 invoices has a foreign key order_id that refers to orders. What turns the link into a one-to-one link?

    Choose one answer.

    Show the answer to question 5

    Answer: A UNIQUE constraint on invoices.order_id

    Unique values mean that each order can be named by at most one invoice. NOT NULL only says that every invoice must belong to an order, which a one-to-many link also allows. orders.order_id is the primary key and is already indexed.

  6. Question 6 of 6 A SQLite file already holds orders for customers who do not exist. A program now runs PRAGMA foreign_keys = ON. What happens to those orders?

    Choose one answer.

    Show the answer to question 6

    Answer: Nothing; they stay, and pragma_foreign_key_check lists them

    Switching the checks on affects only the statements that run afterwards. Rows that are already there are not examined; SELECT * FROM pragma_foreign_key_check returns one row for each of them, so you can fix or remove them.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.