Check Constraints Validation
Learn to validate data with check constraints in PostgreSQL. This lesson covers the core concept, step-by-step implementation, hands-on exercises, troubleshooting, and what to study next.
Focus: validate data with check constraints
You’ve built tables, written queries, and maybe even discovered that your database will happily accept a negative price, a future birth date, or an email address that looks like "not-an-email". Without layer‑of‑defense validation, bad data creeps in through application bugs, manual SQL, and overlooked edge cases. In this lesson, you’ll learn to validate data with check constraints in PostgreSQL — the database‑level guard that rejects invalid rows before they ever reach your tables. By the end, you’ll be able to define business rules directly in your schema, keep your data clean, and sleep better at night.
The problem this lesson solves
Every application that writes to a database eventually faces the same threat: invalid data. A stray NULL, a negative quantity, a string that doesn’t match a pattern — each one can break reports, crash downstream systems, or silently corrupt your analytics. You could handle validation in your application’s code, but that’s fragile: a new developer might forget to check, a direct SQL update from a DBA could bypass the ORM, and every microservice that writes to the same table would need its own copy of the rules.
Check constraints solve this by moving validation into the database itself. They are declarative rules that PostgreSQL enforces on every INSERT or UPDATE. If a row violates the rule, the database rejects the change with an error — no matter which client or tool made the attempt. This is your last line of defense for data integrity.
Core concept / mental model
Think of a check constraint as a gatekeeper at the entrance to your table. Before a row is allowed in, the gatekeeper evaluates a Boolean expression. If the expression evaluates to true (or NULL, for reasons we’ll cover later), the row passes. If it evaluates to false, the row is turned away with an error.
In SQL terms, a check constraint is a constraint that you attach to a table, either when you create the table or later with ALTER TABLE. It can reference one or more columns, and you can use any expression that returns a Boolean — comparisons, BETWEEN, IN, LIKE, and even functions.
Key definition: A check constraint is a rule that must hold for every row in a table. It’s evaluated on each write and, if violated, aborts the operation.
Diagram in words:
INSERT/UPDATE
│
▼
┌─────────────┐
│ CHECK rule │→ if TRUE/NULL → row accepted
│ expression │→ if FALSE → ERROR → row rejected
└─────────────┘
How it works step by step
- Decide on a business rule. Example: a
productstable must never have a negative price. - Write a Boolean expression that the data must satisfy, e.g.
price >= 0. - Add the constraint either inline in a
CREATE TABLEor withALTER TABLE ... ADD CONSTRAINT. - PostgreSQL evaluates the expression for each new or updated row.
- If the expression is
false, the statement fails with a clear error message. - If the expression is
true(or evaluates toNULL), the row is accepted.
The NULL nuance is crucial: in SQL, any comparison with NULL yields NULL, not true or false. A check constraint only rejects rows where the expression is false. So if the expression is price >= 0 and price is NULL, the expression is NULL, and the row is accepted. If you want to reject NULL values, add an explicit NOT NULL constraint or combine it in the check.
Hands-on walkthrough
Let’s put this into practice. We’ll create a users table that requires a valid email format and a products table that only allows positive prices.
Example 1: Create a table with a check constraint
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
price NUMERIC(10,2) CHECK (price >= 0)
);
Now try inserting a negative price:
INSERT INTO products (name, price) VALUES ('Broken item', -5.00);
Expected output:
ERROR: new row for relation "products" violates check constraint "products_price_check"
DETAIL: Failing row contains (1, Broken item, -5.00).
The insert is rejected — your data is safe.
Example 2: Add a named constraint later
You can give your constraint a descriptive name for easier debugging:
ALTER TABLE products
ADD CONSTRAINT price_non_negative CHECK (price >= 0);
This is identical to the inline version but keeps the name price_non_negative, which appears in error messages instead of the auto‑generated name.
Example 3: Combine multiple columns
Check constraints can reference multiple columns. For example, ensure an end date is always after a start date:
CREATE TABLE events (
id SERIAL PRIMARY KEY,
starts_at TIMESTAMP NOT NULL,
ends_at TIMESTAMP NOT NULL,
CONSTRAINT valid_period CHECK (ends_at > starts_at)
);
Example 4: Validate email format with a pattern
Use a LIKE or regex pattern. PostgreSQL supports ~ for regex matching:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT NOT NULL,
CONSTRAINT valid_email CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$')
);
This rejects obvious non‑emails like 'not-an-email'.
Compare options / when to choose what
| Validation approach | Where it runs | Pros | Cons |
|---|---|---|---|
| Check constraints | Database | Always enforced, even with direct SQL; easy to add; fast | Not enough for complex rules (e.g., spanning multiple tables) |
| Application validation | App code | Flexible, can give friendly error messages | Bypassable; duplicated across services; can be forgotten |
| Triggers | Database | Can handle complex logic and multiple tables | More overhead and harder to maintain |
| Foreign keys | Database | Enforce referential integrity | Only handle relationships, not column values |
When to choose check constraints: For simple, single‑table business rules — like positive prices, valid dates, status enums — they are the simplest, fastest, and most reliable option. Use triggers for cross‑table validations, and keep application validation as a friendly UX layer, but always back it up with database constraints.
Troubleshooting & edge cases
- Check constraint is ignored for
NULLvalues. If you haveCHECK (price >= 0)and a row hasprice = NULL, it passes becauseNULL >= 0isNULL, notfalse. To rejectNULL, addNOT NULLor useCHECK (price IS NOT NULL AND price >= 0). - Error message is cryptic. PostgreSQL’s default error will tell you the constraint name but not what went wrong. Name your constraints descriptively and consider adding
CONSTRAINT constraint_namein the definition. - You can’t reference other tables in a check constraint. If you need to validate against values in another table, use a trigger or a foreign key instead.
- Adding a check constraint to a large table can take a long time, as PostgreSQL scans all existing rows to verify they satisfy the rule. Consider adding it during off‑peak hours or using
NOT VALIDwith a laterVALIDATE CONSTRAINTto avoid locking the table for too long. - Altering a check constraint isn’t directly supported — you have to drop and re‑add it. For example, to change the condition, use
ALTER TABLE ... DROP CONSTRAINT ...thenADD CONSTRAINT ....
What you learned & what's next
You now know how to validate data with check constraints in PostgreSQL: how to define them, what they do, and their limitations. You learned that they are a powerful tool for enforcing business rules at the database level, protecting your data from invalid entries no matter what client or tool writes to the table — a foundational step toward robust data integrity.
Next step: In the next lesson, you’ll build on this by exploring constraint validation and triggers, where you’ll learn how to enforce more complex cross‑table rules and automate data corrections. This will give you an even more complete toolkit for keeping your PostgreSQL data clean and reliable.
Practice recap
Try this: create a students table with a check that birth_date is before enrollment_date, and test it with a valid and invalid row. Then experiment with NOT VALID on a large table to see how it speeds up the process.
Common mistakes
- Forgetting that
NULLpasses check constraints — useNOT NULLor combine conditions to reject missing values. - Writing a check constraint that references other tables — check constraints can only refer to columns in the same row.
- Relying only on application validation — always add database‑level check constraints as a safety net.
- Adding a check to a huge table without planning — it scans all rows and can lock the table; use
NOT VALIDfor large datasets.
Variations
- Use a
CHECKwith regex (~) for pattern validation, e.g., email or phone formats. - Create a custom domain with its own check constraint to reuse the same validation across multiple tables.
- Use
NOT VALID+VALIDATEto add a check to a large table without a long exclusive lock.
Real-world use cases
- Enforce non‑negative prices and stock levels in an e‑commerce product catalog.
- Validate that event end timestamps are after start timestamps in a scheduling application.
- Ensure user emails match a pattern before being stored in a user accounts table.
Key takeaways
- Check constraints are database‑level guards that reject invalid rows on
INSERTorUPDATE. - A check constraint is a Boolean expression; rows where it evaluates to
falseare rejected. NULLvalues pass checks unless you explicitly reject them — addNOT NULLor combine conditions.- Check constraints can reference multiple columns in the same row but cannot access other tables.
- Name your constraints clearly for easier debugging and use
ALTER TABLEto add them later. - For simple business rules, check constraints are faster and more reliable than application‑only validation.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.