skip to content

Keys & Integrity Constraints

How a relational engine enforces data correctness declaratively — keys, uniqueness, referential integrity, and row predicates checked on every write. Interviewers probe this to see whether you push invariants into the database or rely on racy application checks.

part ofRelational database conceptsoverview, primer and where to startread it →
on this pageshow

questions

page 1 of 2

What does a CHECK constraint do in a relational table, and at what point does the database engine evaluate it?

level: juniorimportance: must knowfreq 60%

answer

  1. Per-row predicate, evaluated at write
  2. Rejects only on FALSE; UNKNOWN passes
  3. INSERT + UPDATE-of-that-row; never DELETE
  4. No other rows, no subqueries, no now()
  5. Name it, or the error is useless

basics

~20 s

A CHECK constraint is a boolean condition attached to a table. The engine tests it for every row a statement inserts or updates; if the condition comes out false, the statement fails with a constraint-violation error and the row is not stored.

solid answer

~50 s

A CHECK constraint declares a predicate that every row must satisfy, enforced by the database itself. It is evaluated **per row, at write time**: on INSERT for the new row, and on UPDATE for the new version of each modified row. Rows the statement does not touch are not re-tested, and DELETE never triggers a CHECK. The outcome rule matters: the write is rejected only when the predicate evaluates to **false**. True passes, and so does **unknown**, which is what you get when a NULL feeds the expression. That is why `CHECK (price > 0)` still admits a row with a NULL price. A violation raises an error that aborts the statement; whether the surrounding transaction is left usable depends on the engine and on whether you wrapped the statement in a savepoint. Practically, always name the constraint (`CONSTRAINT ck_orders_qty_positive ...`) so the error identifies which rule fired instead of an auto-generated name.

code

sql · 15 lines
sql
CREATE TABLE order_line (
  id           bigint PRIMARY KEY,
  quantity     integer NOT NULL
                 CONSTRAINT ck_order_line_qty_positive CHECK (quantity > 0),
  unit_price   numeric(12,2) NOT NULL,
  discount_pct numeric(5,2),
  CONSTRAINT ck_order_line_discount_range
    CHECK (discount_pct BETWEEN 0 AND 100)
);

-- rejected: quantity > 0 evaluates to FALSE
INSERT INTO order_line VALUES (1, 0, 10.00, NULL);

-- accepted: discount_pct is NULL, predicate is UNKNOWN, not FALSE
INSERT INTO order_line VALUES (2, 5, 10.00, NULL);

go deeper

for a junior

Be able to state it plainly: a boolean rule on a row, checked on INSERT and UPDATE, error if false. Give one concrete example such as a non-negative amount.

for a middle

Add the three-valued-logic rule (only FALSE rejects), the fact that DELETE never fires it, and why the expression must be deterministic.

for a senior

Talk about naming constraints so errors are actionable, how the violation interacts with the surrounding transaction, and what the optimizer can infer from a declared CHECK.

for a principal

Frame CHECK as the cheapest available place to encode a stable domain invariant, and be explicit about which invariants it cannot express, so the team does not try to bend it into cross-row rules.

## What a CHECK constraint is A CHECK constraint is a declarative rule stored in the schema: a boolean expression over the columns of a single row, enforced by the database on every write. It sits alongside NOT NULL, keys and foreign keys as one of the integrity constraints the relational model offers, and its purpose is to make a class of bad data *impossible* rather than merely unlikely. Because the rule lives in the catalog, it applies to every writer: your application, a colleague's batch script, a hand-typed statement in a console, a data migration. Nothing can bypass it short of dropping or disabling the constraint. ## When it is evaluated The engine evaluates the expression **once per candidate row version, at the moment that version is written**: - **INSERT**: the new row is tested before it becomes visible. - **UPDATE**: the *post-update* image of each row the statement actually modified is tested. Rows that the WHERE clause did not select are never examined, even if they would fail the predicate. - **DELETE**: nothing is tested — removing a row cannot make the surviving rows violate a per-row rule. Evaluation is immediate by default: it happens as part of the statement, not at COMMIT. So the failing statement is the one that errors, which is exactly what you want for debugging — the error points at the culprit. A consequence people often miss: a CHECK constraint says nothing about rows that were already in the table before the constraint existed, unless the engine validated them when the constraint was created. Existing data and new data are separate concerns. ## True, false, unknown SQL predicates are three-valued: they can be TRUE, FALSE, or UNKNOWN. The rule for CHECK is that a row is rejected **only on FALSE**. UNKNOWN — the value produced whenever NULL participates in a comparison — is treated as acceptable. This is the opposite of a WHERE clause, which keeps only TRUE rows. So `CHECK (discount_pct BETWEEN 0 AND 100)` will happily accept `NULL`. If you want the column populated, that is a separate NOT NULL declaration, or you write the predicate to fail explicitly on NULL. ## Scope: one row, at that instant The expression may reference only columns of the row being written. It cannot look at other rows of the same table, cannot query other tables, and (in standard SQL as implemented by mainstream engines) cannot contain a subquery. That restriction is not arbitrary: to enforce a cross-row predicate correctly the engine would have to re-validate every affected row whenever *any* row changes, and would have to serialize concurrent writers to avoid two transactions each individually satisfying the rule while jointly breaking it. Per-row scope is what makes the constraint cheap and safe under concurrency. ## The expression must be deterministic A CHECK expression should depend on nothing but the row's own values. Engines forbid or strongly discourage non-deterministic inputs — the current time, session settings, sequence values, volatile functions — because the stored data would then satisfy the constraint at write time but violate it later. `CHECK (start_date >= CURRENT_DATE)` looks reasonable and is a trap: yesterday's perfectly legal row becomes a row that could never be re-inserted, and any operation that re-validates the table (a dump/restore, a table rewrite, a constraint revalidation) can suddenly fail. Rules about *now* belong in application logic or in a trigger, not in a CHECK. ## Placement and naming A CHECK can be written inline next to a column or as a separate table-level clause; the only real difference is that a table-level clause can reference several columns of the row. Either way, give it an explicit name. Auto-generated names differ between environments and change when a table is rebuilt, which breaks any code that maps the error back to a user-facing message. A stable name like `ck_orders_qty_positive` is both self-documenting in the schema and usable as a key in error handling. ## What you get for it Beyond correctness, CHECK constraints are documentation that cannot go stale, and query optimizers can use them: knowing that a column is always positive, or always one of four values, lets a planner discard impossible predicates or prune partitions. The cost is a small amount of CPU per written row — a boolean expression evaluated against values already in memory — which is normally negligible next to the I/O of the write itself. ## When to use one Good candidates: value ranges (`amount >= 0`), enumerated domains (`status IN ('NEW','PAID','SHIPPED')`), format sanity (a minimum length, a pattern), and relationships between columns of the same row (`ends_at > starts_at`). Poor candidates: anything requiring another row, another table, the current time, or a rule that product managers change monthly — those either belong to other constraint types or to code.

  • Does a CHECK constraint fire when you DELETE a row, or when you UPDATE a row that the constraint's columns are not part of?
    DELETE never evaluates CHECK constraints — a per-row rule cannot be broken by removing a row. An UPDATE does re-evaluate every CHECK on the table for each row it modifies, even if the statement did not touch the columns named in the predicate, because the engine validates the whole new row image. Rows not matched by the UPDATE's WHERE clause are untouched and untested.
  • Why do engines refuse, or warn against, expressions like CHECK (created_at <= CURRENT_TIMESTAMP)?
    Because the predicate is not a property of the row, it is a property of the row plus the current clock. A row that passed at insert time keeps satisfying the rule only by luck, and any later revalidation — a restore, a table rewrite, re-enabling the constraint — can fail on data the system itself wrote. Constraints must be deterministic functions of the stored values so that 'valid' is a stable property of the data.

saying these in an interview costs you the question

  • Believing a CHECK rejects NULLs — it accepts them, because the predicate is UNKNOWN, not FALSE
  • Thinking the constraint is evaluated at COMMIT rather than during the statement
  • Assuming a newly added CHECK proves the existing rows already comply
  • Writing CURRENT_DATE / random / session settings into the predicate
  • Expecting CHECK to fire on DELETE, or to re-test rows the UPDATE did not match

context

open as a page

If the application already validates every field before saving, why still declare constraints such as NOT NULL, unique keys and foreign keys in the database schema?

level: juniorimportance: must knowfreq 58%

basics

~20 s

Because the application is not the only writer, and its checks are not atomic with the write. Scripts, migrations, other services and future bugs all reach the same tables. Database constraints are the last line of defense that no code path can bypass; app validation exists for fast, friendly feedback.

open as a page

A column in one table references the key of another table under a foreign key constraint. What exactly does that constraint guarantee about the stored data, and what happens if the referencing column holds NULL?

level: juniorimportance: must knowfreq 78%

basics

~20 s

It guarantees every non-NULL value in the referencing (child) column exists in the referenced (parent) key, so there are no orphan rows. A NULL referencing value points at nothing, so the check is skipped and the row is accepted.

open as a page

A table column is declared NOT NULL with a DEFAULT value. Explain the difference between an INSERT that omits that column entirely and one that explicitly supplies NULL for it.

level: juniorimportance: must knowfreq 62%

basics

~20 s

Omitting the column makes the engine apply the DEFAULT, so the insert succeeds. Explicitly writing NULL is a real value being supplied — the DEFAULT is not consulted, and the NOT NULL constraint rejects the row.

open as a page

What does declaring a primary key on a table actually guarantee, and how does a relational engine enforce that guarantee?

level: juniorimportance: must knowfreq 82%

basics

~20 s

A primary key guarantees every row has a value in those columns (no NULLs) and no two rows share the same combination. The engine enforces it by marking the columns NOT NULL and maintaining a unique index it probes on every insert and key update.

open as a page

A foreign key can declare what should happen when the referenced parent row is deleted or when its key value is updated. What behaviours are available and what does each one do?

level: juniorimportance: must knowfreq 64%

basics

~20 s

Five: CASCADE deletes or rewrites the child rows, SET NULL blanks the child's key columns, SET DEFAULT sets them to the column default, and RESTRICT or NO ACTION reject the operation so orphans never appear. ON DELETE and ON UPDATE are declared separately; the default when unspecified is NO ACTION.

open as a page

A nullable column carries a UNIQUE constraint. How many rows holding NULL in that column can the table contain, and why?

level: juniorimportance: must knowfreq 60%

basics

~20 s

In standard SQL and most engines, unlimited — NULL means 'unknown', two NULLs never compare equal, so no duplicate is ever detected. Microsoft SQL Server is the well-known exception: it permits only one NULL. A PRIMARY KEY forbids NULL entirely.

open as a page

When you INSERT a row into a child table that has a foreign key referencing a parent table, what extra work does the database do before that statement can succeed, and when does that extra work become a throughput bottleneck?

level: juniorimportance: must knowfreq 55%

basics

~20 s

For every inserted row the engine looks up the referenced key in the parent's unique index to prove the parent exists, and takes a light lock on that parent row so it cannot be deleted or re-keyed before commit. It costs an extra index probe plus contention on hot parent rows.

open as a page

A table has CONSTRAINT ck_price CHECK (price > 0) and an INSERT supplies NULL for price. Does the row go in? Explain the logic the engine applies.

level: middleimportance: must knowfreq 55%

basics

~20 s

Yes, the row is accepted. NULL > 0 evaluates to UNKNOWN, not FALSE, and a CHECK constraint rejects a row only when its predicate is FALSE. To forbid the NULL you need NOT NULL on the column, or a predicate that handles NULL explicitly.

open as a page

A signup flow runs a SELECT for the submitted email address and, when no row comes back, INSERTs the new account. Under concurrent traffic it occasionally creates duplicate accounts for the same address. Explain why, and what actually fixes it.

level: middleimportance: must knowfreq 68%

basics

~20 s

The SELECT and the INSERT are separate operations with a gap between them, and a read of a row that does not exist locks nothing. Two requests can both find no row and both insert. The fix is a unique constraint on the email column, with the application handling the resulting violation.

open as a page

What does it mean for a database constraint to be declared DEFERRABLE INITIALLY DEFERRED, and how does that change when the constraint is checked compared with the default behaviour?

level: middleimportance: must knowfreq 45%

basics

~20 s

By default a constraint is checked at the end of every statement. DEFERRABLE INITIALLY DEFERRED postpones checking to COMMIT, so data may be temporarily inconsistent inside the transaction. A violation then fails the COMMIT and aborts the whole transaction.

open as a page

You run ALTER TABLE ... ADD CONSTRAINT to add a foreign key to a table holding 500 million rows in production, and the application immediately starts timing out. Explain what the database is doing and why the impact is so wide.

level: middleimportance: must knowfreq 45%

basics

~20 s

Adding a constraint takes a strong table lock and then scans every existing row to prove they all satisfy it, holding the lock for the whole scan. On a huge table that is minutes to hours, and queued queries pile up behind the lock.

open as a page

Describe what a relational engine actually does at write time to enforce a foreign key: what work happens when a child row is inserted or its referencing columns are updated, and what happens when the parent row is deleted or its key value changes.

level: middleimportance: must knowfreq 66%

basics

~20 s

Child INSERT/UPDATE triggers a lookup in the parent's unique index for the referenced value; if absent, the statement fails. Parent DELETE or key UPDATE triggers a reverse search of the child table for referencing rows; if any exist, the operation is refused. Both run inside the writing transaction.

open as a page

Why do experienced practitioners insist that a row's primary key value must never change, and how does that argument shape the choice between a natural business key and a surrogate key?

level: middleimportance: must knowfreq 62%

basics

~20 s

The key value is copied everywhere — into child foreign keys, caches, URLs, exports, other systems — so changing it means rewriting all of those atomically or breaking references. Natural business values change more often than expected, so most teams use a meaningless surrogate key and enforce the business value with a separate unique constraint.

open as a page

In a relational database, what is the practical difference between declaring UNIQUE as a table constraint and simply creating a unique index on the same columns?

level: middleimportance: must knowfreq 55%

basics

~20 s

Both enforce the same rule, and engines normally implement the constraint with a unique index anyway. The constraint is a named logical object in the catalog that other features can point at (foreign keys, upsert targets, tooling); a bare index is only a physical structure.

open as a page

PostgreSQL lets you add a constraint with the NOT VALID clause and run VALIDATE CONSTRAINT later. Walk through the two steps — what each one locks and checks, and exactly what guarantee you have in the window between them.

level: seniorimportance: must knowfreq 38%

basics

~20 s

ADD CONSTRAINT ... NOT VALID takes a brief strong lock and no scan; from then on all new and modified rows are enforced, but existing rows are unchecked. VALIDATE CONSTRAINT then scans the table under a weak lock that allows concurrent reads and writes.

open as a page

Engines automatically index the referenced key on the parent side of a foreign key but usually leave the referencing columns on the child side unindexed. What goes wrong in production because of that, and how do you decide which of those columns to index?

level: seniorimportance: must knowfreq 60%

basics

~20 s

Parent DELETEs and referenced-key UPDATEs must search the child table for referencing rows; with no index that is a full scan per parent row, and in engines that lock the scanned children it also causes lock escalation and deadlocks. Index child referencing columns on any table whose parent gets deleted, updated, or joined on.

open as a page

You need to make an existing, heavily-used column NOT NULL on a table with hundreds of millions of rows, some of which currently hold NULL. How do you get there without a long outage?

level: seniorimportance: must knowfreq 50%

basics

~20 s

Backfill the NULLs in batches first, stop new NULLs from arriving (application change or a CHECK ... IS NOT NULL added as NOT VALID), validate that check with a non-blocking scan, then promote to NOT NULL — engines can use the proven check to skip the full re-scan under the blocking lock.

open as a page

In an engine that stores table rows clustered by the primary key — InnoDB, for example — how does the choice of primary key affect physical storage, secondary index lookups, and insert throughput?

level: seniorimportance: must knowfreq 56%

basics

~20 s

There the table is the primary key's B-tree: leaf pages hold whole rows in key order. Secondary indexes store the primary key as the row pointer, so wide keys bloat them and lookups cost two descents. Random keys scatter inserts across pages; monotonic keys append but concentrate contention on the rightmost page.

open as a page

Every foreign key in a schema is declared ON DELETE CASCADE, and someone deletes one parent row with millions of descendants. What actually happens inside that transaction, and what are the operational risks?

level: seniorimportance: must knowfreq 52%

basics

~20 s

The engine finds and deletes every descendant level by level in one transaction: real row deletes with index maintenance, undo and write-ahead logging, locks held until commit, and triggers firing. Risks are long lock waits and deadlocks, huge rollback/undo growth, replication lag, and a full scan per parent if the child foreign key column is unindexed.

open as a page

You must load 200 million rows into a table that already has several secondary indexes, foreign keys and CHECK constraints. How would you sequence the load so it finishes in hours rather than days, and what has to be true at the end before the data can be trusted?

level: seniorimportance: must knowfreq 45%

basics

~20 s

Load into an empty table or a staging partition with indexes and foreign keys removed, use the bulk-load API in large batches, then rebuild indexes with sort-based builds and re-add constraints so they are validated by one set-based scan each. Afterwards verify every constraint is in a validated, not merely enabled, state.

open as a page

How do you use a CHECK constraint to enforce a rule that spans several columns of the same row — for example that an end date is never earlier than a start date, or that a discounted price never exceeds the list price?

level: middleimportance: should knowfreq 40%

basics

~20 s

Write the constraint at table level rather than beside a single column, so the expression can reference every column of the row: CONSTRAINT ck_period_order CHECK (ends_at >= starts_at). It is evaluated against the whole new row image on every INSERT and UPDATE of that row.

open as a page

Two tables reference each other with foreign keys — an employee row must point at a department, and each department row must point at its manager employee. Neither table can be populated first. How do you insert the very first pair of rows, and what are the portable alternatives?

level: middleimportance: should knowfreq 38%

basics

~20 s

Declare at least one of the two foreign keys DEFERRABLE INITIALLY DEFERRED, then insert both rows in one transaction; the checks run at COMMIT when both parents exist. The portable alternative is a nullable foreign key: insert with NULL, then UPDATE it.

open as a page

A table has a unique constraint on (list_id, position). A user reorders two items, so the application issues one UPDATE that exchanges their position values. Why can that statement fail, and what are the ways to make the reorder work?

level: middleimportance: should knowfreq 40%

basics

~20 s

Many engines validate uniqueness row by row as the statement runs, so the moment the first row takes the other's value there is a duplicate and it errors. Fixes: a deferrable unique constraint checked at COMMIT, a temporary sentinel value, or a non-unique ordering key.

open as a page

Adding a column with a non-null DEFAULT to a very large table used to rewrite every row, but modern engines can do it as a near-instant metadata-only change. Explain the mechanism that makes that possible and where it stops working.

level: middleimportance: should knowfreq 44%

basics

~20 s

The engine stores the default in the catalog as the value that existing rows are deemed to have, and materialises it on read for any row physically missing the column. New and updated rows store it for real. It stops being instant when the default is volatile or the change forces a row-format rewrite.

open as a page

When would you define a table's primary key over several columns rather than one surrogate column, and what does the ordering of columns in that key affect?

level: middleimportance: should knowfreq 58%

basics

~20 s

Use a composite key when identity is genuinely a combination — link tables, or a child numbered within a parent, or a tenant-scoped id. Column order matters because the key's index is ordered left to right, so only leading-column prefixes get index seeks, and rows cluster by the leading column.

open as a page

Two referential actions a foreign key can declare are RESTRICT and NO ACTION. Both reject an operation that would leave rows referencing a missing parent — so what is the practical difference between them?

level: middleimportance: should knowfreq 40%

basics

~20 s

Timing. RESTRICT checks immediately, as part of the row operation, before other actions or triggers run. NO ACTION defers the check to the end of the statement — or to commit if the constraint is deferrable and deferred — so anything that removes or repoints the children first lets the operation succeed.

open as a page

When is ON DELETE SET NULL the appropriate referential action for a foreign key, what must be true of the schema for it to work, and how does ON DELETE SET DEFAULT compare?

level: middleimportance: should knowfreq 36%

basics

~20 s

Use SET NULL when the reference is optional and the child should outlive its parent — an article losing its category. It requires the child's foreign key columns to be nullable, so it cannot be used on NOT NULL columns or on columns that form part of the child's primary key. SET DEFAULT instead sets the column to its default, which must itself match an existing parent row.

open as a page

Email addresses are stored with the casing the user typed, but the product treats [email protected] and [email protected] as the same account. How do you make the database enforce that uniqueness?

level: middleimportance: should knowfreq 40%

basics

~20 s

Enforce uniqueness on a normalized form, not the raw value: a unique index on an expression such as lower(email), or a stored generated column holding the lowercased value with a unique constraint on it, or a case-insensitive collation on the column plus a plain unique constraint.

open as a page

A table declares a two-column unique constraint over (tenant_id, external_ref). Two rows are inserted, both with tenant_id 7 and external_ref NULL. Does the constraint reject the second row?

level: middleimportance: should knowfreq 40%

basics

~20 s

No, under standard semantics. A composite unique key only rejects a row when every constrained column compares equal and none of them is NULL. A single NULL anywhere in the key makes the comparison unknown, so both rows are accepted. SQL Server rejects the second.

open as a page

showing 1–30 of 48