What do CHECK constraints enforce, and what can they not do?
A CHECK constraint evaluates a boolean expression on each row as it is written, and rejects the row when the expression is definitively false. That makes it the tool for rules that are about a single row's own values: a price that must not be negative, a discount that must be between 0 and 100, a status that must be one of an allowed set.
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name VARCHAR(120) NOT NULL,
price_cents INTEGER NOT NULL,
discount_pct INTEGER,
CONSTRAINT chk_price CHECK (price_cents >= 0),
CONSTRAINT chk_discount CHECK (discount_pct BETWEEN 0 AND 100)
);
INSERT INTO products (id, name, price_cents, discount_pct) VALUES
(1, 'Mechanical Keyboard', 7950, 10),
(2, 'USB-C Hub', 3200, NULL);
SELECT id, name, discount_pct FROM products ORDER BY id;Two rows come back. The second product's discount is NULL and it was accepted — that is the behaviour to understand, and it is covered below.
CHECK can compare a column against another column in the same row, so it can express rules like 'the end date must be after the start date' and 'the discount must not exceed the price'. It can reference expressions and function calls. What it cannot do is reference another table — a CHECK cannot ask whether a customer exists, because that is a foreign key's job, and it cannot look at other rows, so it cannot express 'the total must not exceed the sum of its lines'. Anything involving another row needs a trigger, a foreign key, or an assertion in the application.
CHECK constraints are also not a substitute for the type system. They are evaluated by the engine on every write, which is a real cost on a hot table, so it is worth keeping the expressions simple and worth not stacking a dozen of them on the same column when one would do.