Hook: A CHECK constraint is the database version of a bouncer with a rule card: it stops bad rows before they ever get a seat.
Question: What is a CHECK constraint in SQL?
Answer: A CHECK constraint is a rule on a table that rejects rows when a column value, or a combination of column values, does not meet a condition. For example, it can enforce that price is non-negative, or that discount is not bigger than subtotal. It is enforced by the database on INSERT and UPDATE, so bad data is blocked at the storage layer.
Interview-Ready Answer: I use a CHECK constraint when I want the database to protect a business rule, like age must be at least 18 or discount must not exceed subtotal. It evaluates each row when data is inserted or updated, and it rejects the row if the condition is false. One detail I like to mention is that NULL can still pass a CHECK in many databases unless I also add NOT NULL, because NULL makes the expression unknown instead of false.
Detailed Explanation: A CHECK constraint is a boolean rule attached to a table. A boolean expression is a condition that evaluates to true or false, such as price >= 0 or status IN ('draft', 'active'). You can attach it to one column or to several columns together.
INSERT or UPDATE.NOT NULL rejects it.This is why CHECK is a data-quality guardrail, not a query filter. It does not help your SELECT speed; it only protects writes.
quantity > 0, rating BETWEEN 1 AND 5status IN ('pending', 'paid', 'shipped')discount_cents <= subtotal_cents| Rule | What it protects | Best use |
|---|---|---|
| CHECK | Custom condition | Business rules |
| NOT NULL | Missing values | Required fields |
| UNIQUE | No duplicates | Identifiers |
| FOREIGN KEY | Valid references | Relationships |
For a simple predicate, CHECK is usually O(1) per row because it just evaluates a small expression. For n inserted rows, the total cost is O(n). There is no index lookup for CHECK itself, so it is usually cheap, but a complex expression or function call can add CPU cost during bulk loads. In real systems, millions of rows can still be fine, but if your predicate does expensive string parsing or custom functions, the write path will slow down.
CHECK (age >= 18) does not forbid NULL by itself in many engines.Memory tip while whiteboarding: say, CHECK guards the door on the way in; it does not patrol the room after the guest is inside.
Real-World Example: Imagine a checkout service in an e-commerce app. The application calculates subtotal_cents, discount_cents, and final_cents, then writes the order to the database. A CHECK constraint like discount_cents <= subtotal_cents and subtotal_cents >= 0 makes sure a buggy client cannot save a negative subtotal or a discount larger than the order total.
What goes wrong without it: a frontend bug or a broken integration sends discount_cents = 5000 for a 3000-cent order, and the database happily stores a negative final total. The symptom may not be an immediate crash; instead, finance reports drift, refund logic gets confused, and support sees customers complaining about strange receipts. In logs, you might see no obvious SQL error because the bad row was accepted, which is exactly why database-level rules matter.
In production, CHECK constraints are especially useful for protecting money fields, inventory counts, and status values. They reduce the chance that one bad service, one malformed import, or one overlooked UI path corrupts the source of truth.
-- SQLite-compatible demo of CHECK constraints
-- The script shows both acceptance and rejection paths.
PRAGMA foreign_keys = ON;
DROP TABLE IF EXISTS members;
DROP TABLE IF EXISTS order_lines;
-- A simple CHECK on one column.
-- NULL passes here because CHECK rejects FALSE, not UNKNOWN.
CREATE TABLE members (
member_id INTEGER PRIMARY KEY,
age INTEGER CHECK (age >= 18)
);
INSERT INTO members (member_id, age) VALUES (1, 21);
INSERT INTO members (member_id, age) VALUES (2, NULL);
-- This row would violate the CHECK, so we use OR IGNORE to keep the script runnable.
INSERT OR IGNORE INTO members (member_id, age) VALUES (3, 16);
SELECT 'members' AS section, member_id, age
FROM members
ORDER BY member_id;
-- A table-level CHECK that uses multiple columns together.
CREATE TABLE order_lines (
line_id INTEGER PRIMARY KEY,
quantity INTEGER NOT NULL CHECK (quantity > 0),
unit_price_cents INTEGER NOT NULL CHECK (unit_price_cents >= 0),
discount_cents INTEGER NOT NULL DEFAULT 0,
CHECK (discount_cents <= quantity * unit_price_cents)
);
INSERT INTO order_lines (line_id, quantity, unit_price_cents, discount_cents)
VALUES (1, 2, 500, 200);
-- Rejected: discount is larger than the full line amount.
INSERT OR IGNORE INTO order_lines (line_id, quantity, unit_price_cents, discount_cents)
VALUES (2, 1, 300, 400);
-- Rejected: quantity is not positive.
INSERT OR IGNORE INTO order_lines (line_id, quantity, unit_price_cents, discount_cents)
VALUES (3, 0, 300, 0);
SELECT 'order_lines' AS section, line_id, quantity, unit_price_cents, discount_cents
FROM order_lines
ORDER BY line_id;Follow-up & Tricky Questions:
NOT NULL.NOT NULL: CHECK (age >= 18) does not necessarily block NULL. Add NOT NULL when the column must exist.Memory Hook: Think of CHECK as a bouncer at the door with a simple rule sheet. If the row breaks the rule, it never enters the table. If the value is NULL, the bouncer may shrug unless NOT NULL is also on the list.
products table with price_cents >= 0 and status IN (...).end_date >= start_date.NOT NULL matters.