Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

SQL (PostgreSQL, MySQL, SQLite)  Module 9 – Creating tables and constraints

NOT NULL, UNIQUE, CHECK and DEFAULT

Enforce data rules in PostgreSQL, MySQL and SQLite with NOT NULL, UNIQUE, CHECK and DEFAULT, name your constraints and read the errors each engine reports.

  • Beginner
  • 25 minutes
  • Examples run with sql.js 1.14.2
  • By MySmartCoPilot
Free core, Pro depth: 9 of 25 minutes free
Free: Four rules on one table · The four rules one by one · Rules that involve several columns · 2 more sections. With a pass: 2 in-depth parts and 3 interview answers, about 16 minutes.

What you will learn

  • Protect data with NOT NULL, UNIQUE, CHECK and DEFAULT constraints
  • Name constraints and read their violation messages in each engine
  • Handle NULLs and letter case in unique constraints
  • Find the rows that break a rule before adding it to a table that holds data

Before you start

What you’ll learn · Before you start

What you will learn

  • Protect data with NOT NULL, UNIQUE, CHECK and DEFAULT constraints
  • Name constraints and read their violation messages in each engine
  • Handle NULLs and letter case in unique constraints
  • Find the rows that break a rule before adding it to a table that holds data

Before you start

On this page

A constraint is a rule about the data that the database enforces itself, on every insert and update, whichever program writes the row. Four rules cover most columns. NOT NULL says that a column must hold a value. UNIQUE says that no two rows may share a value. CHECK states a condition that every row must meet, such as a price above zero. DEFAULT is a fill-in rather than a test: the value a column takes when an insert leaves it out. A row that breaks a rule is refused with an error, and the statement changes nothing. A rule in the schema beats a check in application code because it also holds for the second application, the bulk import and the person fixing data by hand late at night. The SQLite examples run with one click, on the SQLite 3.49.1 engine inside sql.js 1.14.2, and each difference in PostgreSQL 18 or MySQL 8.4 is pointed out as it comes up.

Four rules on one table

mart's products with all four rules, and four inserts that break one SQL · four_rules.sql
-- mart's products table with all four rules, then six inserts: two keep every rule, four break one each.
CREATE TABLE products (
  product_id  INTEGER PRIMARY KEY,
  sku         TEXT    NOT NULL UNIQUE,
  name        TEXT    NOT NULL,
  price_paise INTEGER NOT NULL CHECK (price_paise > 0),
  stock       INTEGER NOT NULL DEFAULT 0
                      CONSTRAINT stock_not_negative CHECK (stock >= 0),
  is_active   INTEGER NOT NULL DEFAULT 1 CHECK (is_active IN (0, 1))
);

-- Kept: stock and is_active are left out, so their defaults fill them in.
INSERT INTO products (sku, name, price_paise) VALUES ('RICE-1KG', 'Basmati rice 1 kg', 14999);
INSERT INTO products (sku, name, price_paise, stock) VALUES ('DAL-500G', 'Toor dal 500 g', 8950, 40);

-- Refused: a second RICE-1KG, a missing name, a price of zero, and stock below zero.
INSERT INTO products (sku, name, price_paise) VALUES ('RICE-1KG', 'Basmati rice again', 15999);
INSERT INTO products (sku, name, price_paise) VALUES ('CHAI-250G', NULL, 12000);
INSERT INTO products (sku, name, price_paise) VALUES ('OIL-1L', 'Coconut oil 1 L', 0);
INSERT INTO products (sku, name, price_paise, stock) VALUES ('RAGI-1KG', 'Ragi flour 1 kg', 6800, -2);

SELECT product_id, sku, name, price_paise, stock, is_active FROM products;

Output (exit status 1)

┌────────────┬──────────┬───────────────────┬─────────────┬───────┬───────────┐
│ product_id │   sku    │       name        │ price_paise │ stock │ is_active │
├────────────┼──────────┼───────────────────┼─────────────┼───────┼───────────┤
│ 1          │ RICE-1KG │ Basmati rice 1 kg │ 14999       │ 0     │ 1         │
│ 2          │ DAL-500G │ Toor dal 500 g    │ 8950        │ 40    │ 1         │
└────────────┴──────────┴───────────────────┴─────────────┴───────┴───────────┘

Printed as an error (standard error)

Runtime error near line 17: UNIQUE constraint failed: products.sku
Runtime error near line 18: NOT NULL constraint failed: products.name
Runtime error near line 19: CHECK constraint failed: price_paise > 0
Runtime error near line 20: CHECK constraint failed: stock_not_negative

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < four_rules.sql

Two products went in. The first named only three columns, so DEFAULT filled stock with 0 and is_active with 1. The other four inserts were refused, and each error names the rule and the column: a second RICE-1KG breaks the UNIQUE rule, a missing name breaks NOT NULL, and a price of zero and a stock of −2 break the two CHECK conditions. The last error names the constraint stock_not_negative instead of repeating its condition, because the table gave it that name.

A new row is filled by DEFAULT, then tested by NOT NULL, CHECK and UNIQUE; failing any test makes the statement change nothing.A new rowfrom INSERT or UPDATE1. DEFAULTfills each column the INSERTleft out, such as stock = 02. NOT NULLeach required columnholds a value3. CHECKeach condition istrue or NULL4. UNIQUEno other row holdsthe same valueThe row is storedThe statement failsand changes nothingyesyesyesa NULLfalsea duplicate

The four rules a row meets on its way into the table

Text description of the diagram

The diagram follows a new row, from an INSERT or an UPDATE, from top to bottom.

  1. DEFAULT fills in every column that the INSERT left out, for example a stock of 0.
  2. NOT NULL tests that each required column holds a value. A NULL in such a column fails the statement.
  3. CHECK tests each condition. A condition that is true or NULL passes; a condition that is false fails the statement.
  4. UNIQUE tests that no other row holds the same value in the unique column or columns. A duplicate fails the statement.
  5. A row that passes all four tests is stored. A row that fails any of them makes the whole statement fail, and the statement changes nothing.

When one row breaks several rules at once, SQLite 3.49.1, PostgreSQL 18.6 and MySQL 8.4.11 all report the NOT NULL problem first, then the CHECK, then the duplicate, which is the order shown here.

The four rules one by one

  • NOT NULL refuses a row that leaves the column empty. PostgreSQL and MySQL make every primary key column NOT NULL by themselves; SQLite does so only for an INTEGER PRIMARY KEY, so write it out for any other key.
  • UNIQUE refuses a second row with the same value, and the engine builds an index to find duplicates quickly. It may cover several columns together, such as UNIQUE (series, invoice_no), where each column may repeat but the pair may not. UNIQUE accepts any number of NULLs, because a NULL is never equal to anything, not even another NULL.
  • CHECK tests a condition on the row being written, and the row is refused only when the condition is false. A condition that comes out NULL passes, and a CHECK cannot look at other rows or tables. MySQL enforces CHECK constraints from version 8.0.16; older versions read them and ignored them.
  • DEFAULT gives a value or an expression, such as CURRENT_TIMESTAMP, for a column that the insert does not mention. A default is never used for a NULL that the insert writes on purpose.

Rules that involve several columns

A rule written after a column’s type is a column constraint. A rule that names several columns goes after the last column, as a table constraint: UNIQUE (series, invoice_no), or CONSTRAINT sale_not_above_mrp CHECK (sale_paise <= mrp_paise), which compares two columns of the same row and is therefore allowed everywhere. Both forms are checked in the same way; the table form is simply the only place where a rule can mention more than one column. NOT NULL is the exception, because it always concerns one column: SQLite and MySQL accept it only next to the column, and SQLite reports a table-level NOT NULL as a syntax error. PostgreSQL 18 also accepts CONSTRAINT price_required NOT NULL price_paise among the table constraints, which gives the rule a name of its own. Whatever the form, a CHECK still tests one row at a time, while UNIQUE compares the row with all the others.

Name your constraints

The error each engine reports for the refused rows
Criterion SQLite 3.49PostgreSQL 18MySQL 8.4
NOT NULL NOT NULL constraint failed: products.namenull value in column "name" of relation "products" violates not-null constraintColumn 'name' cannot be null (error 1048)
UNIQUE UNIQUE constraint failed: products.skuduplicate key value violates unique constraint "products_sku_key"Duplicate entry 'RICE-1KG' for key 'products.sku' (error 1062)
CHECK without a name CHECK constraint failed: price_paise > 0new row for relation "products" violates check constraint "products_price_paise_check"Check constraint 'products_chk_1' is violated (error 3819)
CHECK named stock_not_negative CHECK constraint failed: stock_not_negative… violates check constraint "stock_not_negative"Check constraint 'stock_not_negative' is violated (error 3819)

A constraint without a name gets one made up by the engine: products_price_paise_check in PostgreSQL, products_chk_1 in MySQL, which numbers its checks in order, and none at all in SQLite, which prints the condition instead. Writing CONSTRAINT stock_not_negative CHECK (stock >= 0) puts your name into every error message and log line, and it is also the name that ALTER TABLE … DROP CONSTRAINT needs when the rule changes later. Name a constraint after the rule rather than the column, so that the message says what went wrong.

Your turn

Exercise · Easy · SQL

Rules for a product catalogue

A shop loads new products from a supplier's sheet, the table candidates (n, sku, name, stock), which each test fills in. Some rows of the sheet are wrong. Add constraints to the CREATE TABLE products statement so that the table itself refuses them:

  • sku must be present, and no two products may share one;
  • name must be present;
  • stock must be present and never below zero (zero is allowed);
  • is_active must be 0 or 1, and a product that does not mention it is active (1).

Leave the lines below the CREATE TABLE as they are. INSERT OR IGNORE skips every row that breaks a rule and keeps the rest, and the last line returns the products in the order of sku.

Starter code · products.sql

-- Add constraints to the columns below. Change nothing after the CREATE TABLE statement.
CREATE TABLE products (
  sku       TEXT,
  name      TEXT,
  stock     INTEGER,
  is_active INTEGER
);

-- Every row of the supplier's sheet is tried; rows that break a rule are skipped.
INSERT OR IGNORE INTO products (sku, name, stock) SELECT sku, name, stock FROM candidates ORDER BY n;
SELECT sku, name, stock, is_active FROM products ORDER BY sku;
The sample tests · tests.yaml
# Sample tests: each one fills the table candidates, runs your script on it and compares the rows it returns, in
# order.
tests:
  - name: refuses a negative stock and a missing name, and marks the rest active
    ordered: true
    setup: |
      CREATE TABLE candidates (n INTEGER PRIMARY KEY, sku TEXT, name TEXT, stock INTEGER);
      INSERT INTO candidates VALUES
        (1, 'RICE-1KG', 'Basmati rice 1 kg', 12),
        (2, 'DAL-500G', 'Toor dal 500 g', -4),
        (3, 'CHAI-250G', NULL, 30),
        (4, 'RAGI-1KG', 'Ragi flour 1 kg', 7);
    expected:
      columns: [sku, name, stock, is_active]
      rows:
        - ['RAGI-1KG', 'Ragi flour 1 kg', 7, 1]
        - ['RICE-1KG', 'Basmati rice 1 kg', 12, 1]
  - name: keeps the first of two rows with the same sku, and refuses a row without one
    ordered: true
    setup: |
      CREATE TABLE candidates (n INTEGER PRIMARY KEY, sku TEXT, name TEXT, stock INTEGER);
      INSERT INTO candidates VALUES
        (1, 'OIL-1L', 'Coconut oil 1 L', 9),
        (2, 'OIL-1L', 'Coconut oil, duplicate row', 3),
        (3, NULL, 'Jaggery 1 kg', 15),
        (4, 'SALT-1KG', 'Rock salt 1 kg', NULL);
    expected:
      columns: [sku, name, stock, is_active]
      rows:
        - ['OIL-1L', 'Coconut oil 1 L', 9, 1]
  - name: accepts a stock of exactly zero
    ordered: true
    setup: |
      CREATE TABLE candidates (n INTEGER PRIMARY KEY, sku TEXT, name TEXT, stock INTEGER);
      INSERT INTO candidates VALUES
        (1, 'GHEE-500ML', 'Cow ghee 500 ml', 0),
        (2, 'POHA-500G', 'Thick poha 500 g', -1);
    expected:
      columns: [sku, name, stock, is_active]
      rows:
        - ['GHEE-500ML', 'Cow ghee 500 ml', 0, 1]
A hint

Each rule is a constraint written after the column's type: NOT NULL for a value that must be present, UNIQUE for one that no two rows may share, CHECK (…) for a condition, and DEFAULT … for the value used when the INSERT leaves the column out. A column can carry several of them, one after another.

The sample tests run on this device, in your browser (sql.js): nothing is sent to mysmartcopilot.com. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

3 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 3 A table has discount_pct INTEGER CHECK (discount_pct BETWEEN 0 AND 100). Which insert does it refuse?

    Choose one answer.

    Show the answer to question 1

    Answer: A discount of 150

    A CHECK refuses a row only when its condition is false. 150 BETWEEN 0 AND 100 is false. BETWEEN includes both ends, so 0 and 100 pass, and for NULL the condition is NULL, which passes too. Add NOT NULL when a value must be present.

  2. Question 2 of 3 Which of these are true of a UNIQUE constraint on one column, by default, in PostgreSQL, MySQL and SQLite?

    Choose every answer that is right.

    Show the answer to question 2

    Answer:

    • The engine keeps an index to find duplicates
    • The same rule can cover several columns together, as a table constraint
    • Several rows may hold NULL in the column

    UNIQUE treats NULLs as distinct, so many rows may hold NULL; each engine enforces the rule with an index; and a table constraint such as UNIQUE (series, invoice_no) covers a pair of columns. The rule holds for every program that writes to the table, which is the point of putting it in the schema.

  3. Question 3 of 3 A column is declared newsletter INTEGER DEFAULT 1, without NOT NULL. An INSERT names the column and writes NULL into it. What does the row hold?

    Choose one answer.

    Show the answer to question 3

    Answer: NULL

    A default is used only when the insert leaves the column out. A NULL written on purpose is stored as NULL, as the subscriber vikram@mail.in showed. With NOT NULL on the column, the same insert would fail instead.

SQLite Online (SQL Playground) Paste a CREATE TABLE with your constraints, then try inserts that should fail, all in your browser.

UNIQUE in depth: NULLs, letter case and deleted rows Pro: opens with a Pro, Premium or Ultimate pass

Most duplicates that reach production slip past a UNIQUE constraint that is in place, through NULLs, through letter case or through rows that are marked deleted. This part reproduces each gap and closes it in all three engines.

In this part:

  • Two NULLs are never duplicates, reproduced
  • Letter case and accents: the duplicates that get through
  • Deleted rows that block a new sign-up
  • One UPDATE that collides with itself

4 runnable examples (SQL) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

CHECK and DEFAULT in depth: NULLs, limits and rules for tables with data Pro: opens with a Pro, Premium or Ultimate pass

A CHECK fails only on false, it sees one row at a time, and a DEFAULT steps in only for columns left out. This part shows where each of those facts lets bad data in, and how to add a rule to a table that already holds rows.

In this part:

  • A CHECK that lets NULL through, reproduced
  • What a CHECK cannot see
  • DEFAULT steps in only for columns left out
  • Adding a rule to a table that already has rows, with numbers

3 runnable examples (SQL) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher to mid · Warm-up

    Can a column with a UNIQUE constraint hold NULL in more than one row, and does the answer change between PostgreSQL, MySQL and SQLite?

    Yes, in all three by default. UNIQUE refuses a second row with an equal value, and NULL is never equal to anything, not even another NULL, so any number of rows may leave the column empty. That suits optional values such as a phone number, where many customers have none but no two may share one. When at most one NULL should be allowed, PostgreSQL 15 and later accept UNIQUE NULLS NOT DISTINCT. SQLite and MySQL have no such clause, but a unique index on an expression such as coalesce(phone, '') does the same job, as long as the empty string cannot occur as a real value. A primary key is different: its columns are NOT NULL, so the question never arises there.

  2. Mid to senior

    A PostgreSQL users table has email text UNIQUE. Support finds people with two accounts whose addresses differ only in letter case, such as Asha@Mail.in and asha@mail.in. Why did the constraint allow it, and how do you fix the existing data and the schema without blocking sign-ups for long?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Fresher to mid

    A coupons table has CHECK (discount_pct BETWEEN 0 AND 100), yet an import loaded thousands of coupons with no discount at all, and the billing job then failed on them. How did they pass the constraint, and how do you make the rule hold for old and new rows on a busy PostgreSQL table?

    Model answer, Pro. First step and follow-ups

  4. Mid to senior

    A MySQL customers table declares name VARCHAR(100) NOT NULL, yet 3,000 rows have an empty name, all loaded by a nightly job that sends multi-row INSERTs. The job never failed. How is that possible, and what do you change?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • NOT NULL, UNIQUE and CHECK are tested on every row that is written; DEFAULT fills only the columns an insert leaves out.
  • A broken rule fails the whole statement, which then changes nothing.
  • UNIQUE accepts any number of NULLs, and a CHECK passes when its condition is NULL.
  • Name constraints after their rule, so that every error message explains itself.
  • MySQL enforces CHECK constraints only from 8.0.16; SQLite and PostgreSQL always have.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.