Menu
Try in Playground

SQLite CHECK Constraints: Validate Data at the Table Level

How to use CHECK constraints in SQLite to enforce rules on column values — single-column checks, multi-column checks, named constraints, and the gotchas.

This page includes runnable editors — edit, run, and see output instantly.

A CHECK Constraint Is a Rule Every Row Must Satisfy

A CHECK constraint is a boolean expression you attach to a table. SQLite evaluates it on every INSERT and UPDATE, and if the expression comes back false, the operation fails. It's a way to bake a business rule — "price can't be negative," "status has to be one of these three values" — straight into the schema.

The first two rows go in. The third raises CHECK constraint failed and is rejected — the table never sees it. The constraint enforces the rule for every writer, whether that's your app, a migration script, or someone poking around in the CLI.

Column-Level vs Table-Level

You can write a CHECK in two places. After a column definition (column-level) or after all the columns (table-level). They behave the same; the difference is what reads naturally.

The first booking inserts. The second fails — the end is before the start. Single-column rules read better as column-level; anything that compares two or more columns reads better as table-level.

Restricting Values to a List

A common use is forcing a column to one of a fixed set of values. SQLite has no native enum type, so CHECK ... IN (...) is the idiom:

The third row fails — 'pending' isn't in the allowed list. If you ever need to add a new status, you'll have to rebuild the table (more on that below), so think a little before locking the list down. But for genuinely fixed vocabularies like role names or order states, this is exactly the constraint you want.

Naming Your Constraints

By default, a constraint is anonymous. The error message just says "CHECK constraint failed" with the expression, which is fine when there's one CHECK on the table — and confusing when there are five. Add a name with CONSTRAINT:

Now the failure message includes the constraint name, so you know immediately which rule was violated. Naming costs a few extra characters and pays for itself the first time something breaks in production.

CHECK and NULL: The Gotcha

CHECK passes when the expression is true or NULL. It only fails on an explicit false. That sounds odd until you remember that almost any comparison with NULL evaluates to NULL, not to true or false.

The NULL row goes in cleanly — NULL >= 0 is NULL, not false, so the CHECK doesn't fail. If you actually want to forbid both negative numbers and missing values, combine NOT NULL with the CHECK:

Now the insert fails on the NOT NULL constraint before the CHECK even runs. The two constraints work together: NOT NULL covers absence, CHECK covers shape.

Useful Built-In Functions Inside CHECK

The expression can use most of SQLite's built-in functions. A few that come up a lot:

Three failures: a bad email shape, a too-short username, and a lowercase country code. LIKE handles simple patterns; length(), upper(), lower(), and arithmetic are all fair game. Just keep the expression deterministic — using something like random() or current_timestamp inside CHECK creates rules that can flip between rows, which is rarely what you want.

CHECK vs Trigger

CHECK and triggers can both reject bad data, and beginners often wonder which to reach for. The rule of thumb:

  • CHECK when the rule depends only on the row being written. "This column compared to that column," "this value within a range," "this string matches a pattern."
  • Trigger (specifically a BEFORE INSERT/UPDATE trigger that calls RAISE) when the rule depends on other rows, other tables, or needs to do something more complicated than a single boolean expression.

CHECK is faster, simpler, and visible in the schema — anyone reading CREATE TABLE sees the rule. Reach for a trigger only when CHECK can't express what you need.

You Can't Drop a CHECK With ALTER

This is the one rough edge. SQLite has no ALTER TABLE ... DROP CONSTRAINT. To remove or change a CHECK, you rebuild the table:

BEGIN;

CREATE TABLE products_new (
    id    INTEGER PRIMARY KEY,
    name  TEXT NOT NULL,
    price REAL NOT NULL CHECK (price >= 0 AND price <= 1000000)
);

INSERT INTO products_new SELECT * FROM products;
DROP TABLE products;
ALTER TABLE products_new RENAME TO products;

COMMIT;

Wrap the whole thing in a transaction so a failure midway leaves the database alone. If other tables have foreign keys pointing at the table you're rebuilding, the dance gets longer — disable foreign_keys, rebuild, re-enable, recheck. We'll cover that in the migrations doc later in the curriculum.

Next: UNIQUE Constraints

CHECK validates the shape of values inside a row. The next constraint, UNIQUE, validates relationships across rows — guaranteeing no two rows share the same value in a column or set of columns. That's coming up next.

Frequently Asked Questions

What is a CHECK constraint in SQLite?

A CHECK constraint is a boolean expression attached to a table that every row must satisfy. SQLite evaluates it on every INSERT or UPDATE, and rejects the change if the expression is false. It's the simplest way to enforce a rule like 'price must be positive' without writing application code.

Can a SQLite CHECK constraint reference multiple columns?

Yes — write it as a table-level constraint instead of attaching it to one column. For example, CHECK (start_date <= end_date) declared after the column list can reference both columns. Column-level checks can technically reference other columns too, but table-level reads more clearly when more than one column is involved.

Why does my SQLite CHECK constraint not fire on NULL?

CHECK passes when the expression is true or NULL — it only fails when the expression is explicitly false. So CHECK (age >= 0) accepts a NULL age, because NULL >= 0 is NULL, not false. If you want to forbid NULL too, add a NOT NULL constraint alongside the CHECK.

Can I drop or alter a CHECK constraint in SQLite?

Not directly. SQLite doesn't support ALTER TABLE ... DROP CONSTRAINT. To change a CHECK, you either edit sqlite_schema with PRAGMA writable_schema (advanced, risky) or rebuild the table: create a new table with the desired constraints, copy the data over, drop the old table, and rename. Naming your constraints makes the rebuild script easier to read.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED