How does a foreign key constraint protect a relationship between two tables?
A foreign key declares that the values in one column must correspond to a row that already exists in another table. It is the mechanism that turns a set of independent tables into a relational model, because it stops the database from accepting an order that points at a customer who does not exist.
PRAGMA foreign_keys = ON;
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name VARCHAR(120) NOT NULL
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL REFERENCES customers (id),
amount DECIMAL(10, 2) NOT NULL
);
INSERT INTO customers (id, name) VALUES (1, 'Ada Lovelace');
INSERT INTO orders (id, customer_id, amount) VALUES (1, 1, 120.00);
SELECT o.id, c.name, o.amount
FROM orders AS o
JOIN customers AS c ON c.id = o.customer_id;One row comes back: order 1 belonging to Ada Lovelace. The order can only exist because the customer it points at exists.
The important detail is the first line. SQLite does not enforce foreign keys by default — the pragma has to be turned on for every connection — and that setting is per connection rather than per database, so an application that opens its own connections has to enable it in its connection setup. PostgreSQL, MySQL and Oracle enforce them out of the box. This is a genuinely common bug: a script that inserts rows in the right order works fine locally, and a test suite that never exercises a violation passes, and the problem only appears in production.
What the constraint buys you is that the invariant holds no matter which code path writes the data. Application-level validation can be skipped by a migration, a bulk import or a second service; the foreign key does not care who is writing. It also gives you the option of an index-friendly join in both directions and lets the planner reason about the relationship.
The cost is a lookup on every insert and update of the referencing column, and that is why a foreign key does not automatically create an index on the referencing side in PostgreSQL — the referencing column needs its own index for the constraint check to be cheap. MySQL and SQLite do create one.