SQLite Constraints – PRIMARY KEY, FOREIGN KEY, UNIQUE, CHECK, NOT NULL | DbSchema



SQLite constraints and data integrity rules >

Table of Contents

  1. What SQLite constraints do
  2. SQLite constraint types
  3. Create tables with constraints in sqlite3
  4. Foreign keys and PRAGMA foreign_keys
  5. Apply constraints from Python
  6. Manage constraints in DbSchema
  7. Edge cases and limits
  8. Performance and design tips
  9. FAQ
  10. Conclusion
  11. References

SQLite constraints protect data quality by preventing invalid rows from being inserted or updated. Without them, application code has to enforce every rule on its own, which is risky and hard to maintain.

This guide explains the core SQLite constraint types, shows practical sqlite3 and Python examples, covers foreign key caveats, and shows how DbSchema helps manage constraints visually.

What SQLite constraints do

Constraints define what values a table can accept.

They are used to:

  • make required columns truly required
  • prevent duplicates
  • validate ranges and allowed values
  • preserve parent-child relationships
  • set safe defaults for missing values

If you are still designing the table structure, review SQLite CREATE TABLE first.

SQLite constraint types

ConstraintPurposeExample
PRIMARY KEYuniquely identifies each rowcustomer_id INTEGER PRIMARY KEY
FOREIGN KEYpreserves references between tablesFOREIGN KEY(customer_id) REFERENCES customers(customer_id)
UNIQUEdisallows duplicate valuesemail TEXT UNIQUE
NOT NULLrequires a valuename TEXT NOT NULL
DEFAULTfills in an omitted valuestatus TEXT DEFAULT 'new'
CHECKenforces custom rulesCHECK(total_cents >= 0)

Sample schema using several constraints together:

CREATE TABLE customers (
    customer_id INTEGER PRIMARY KEY,
    name        TEXT NOT NULL,
    email       TEXT UNIQUE,
    status      TEXT NOT NULL DEFAULT 'active'
                 CHECK (status IN ('active', 'disabled'))
);

CREATE TABLE orders (
    order_id     INTEGER PRIMARY KEY,
    customer_id  INTEGER NOT NULL,
    total_cents  INTEGER NOT NULL CHECK (total_cents >= 0),
    created_at   TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (customer_id)
        REFERENCES customers(customer_id)
        ON DELETE CASCADE
);

Create tables with constraints in sqlite3

Open the shell:

sqlite3 shop.db

Then create the schema:

PRAGMA foreign_keys = ON;

CREATE TABLE customers (
    customer_id INTEGER PRIMARY KEY,
    name        TEXT NOT NULL,
    email       TEXT UNIQUE,
    status      TEXT NOT NULL DEFAULT 'active'
                 CHECK (status IN ('active', 'disabled'))
);

CREATE TABLE orders (
    order_id     INTEGER PRIMARY KEY,
    customer_id  INTEGER NOT NULL,
    total_cents  INTEGER NOT NULL CHECK (total_cents >= 0),
    FOREIGN KEY (customer_id)
        REFERENCES customers(customer_id)
        ON DELETE CASCADE
);

Test constraint violations

INSERT INTO customers (customer_id, name, email)
VALUES (1, 'Bob', '[email protected]');

INSERT INTO customers (customer_id, name, email)
VALUES (2, 'Alice', '[email protected]');

The second insert fails because of the UNIQUE constraint on email.

Another example:

INSERT INTO orders (order_id, customer_id, total_cents)
VALUES (1, 1, -50);

This fails because of CHECK (total_cents >= 0).

Foreign keys and PRAGMA foreign_keys

Foreign keys are one of the most important SQLite caveats.

SQLite supports foreign key constraints, but they are enforced only when the connection has:

PRAGMA foreign_keys = ON;

Check the current setting:

PRAGMA foreign_keys;

If your app opens multiple connections, make sure each connection enables foreign key enforcement where needed.

This matters directly for:

  • delete cascades
  • preventing orphan rows
  • keeping joins trustworthy

Related follow-up pages:

Apply constraints from Python

Python's sqlite3 module works well for repeatable schema creation and migration scripts.

import sqlite3

schema_sql = """
PRAGMA foreign_keys = ON;

CREATE TABLE IF NOT EXISTS customers (
    customer_id INTEGER PRIMARY KEY,
    name        TEXT NOT NULL,
    email       TEXT UNIQUE
);

CREATE TABLE IF NOT EXISTS orders (
    order_id     INTEGER PRIMARY KEY,
    customer_id  INTEGER NOT NULL,
    total_cents  INTEGER NOT NULL CHECK (total_cents >= 0),
    FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);
"""

try:
    with sqlite3.connect("shop.db") as connection:
        connection.executescript(schema_sql)
        connection.execute(
            "INSERT INTO orders(order_id, customer_id, total_cents) VALUES (?, ?, ?)",
            (1, 999, 2500)
        )
except sqlite3.IntegrityError as error:
    print("Constraint violation:", error)

This is a practical pattern for tests and setup scripts because it gives you a clear exception when the data violates a rule.

Manage constraints in DbSchema

DbSchema helps when you want to review constraint logic without digging through raw DDL every time.

Typical workflow:

  1. Open the SQLite database or model in DbSchema.
  2. Double-click a table in the diagram.
  3. Configure column rules such as NOT NULL, DEFAULT, and UNIQUE.
  4. Add foreign keys and check constraints visually.
  5. Generate or synchronize the SQL changes.
  6. Use the SQL Editor to test inserts and verify constraint behavior.

Useful docs:

DbSchema is especially helpful when multiple people need to understand how the rules fit together across tables and relationships.

Edge cases and limits

UNIQUE and NULL

SQLite allows multiple NULL values in a UNIQUE column because NULL is treated as unknown, not equal to another NULL.

CHECK constraints

CHECK is great for ranges and allowed-value lists, but it is not a full replacement for application logic. Keep the expressions simple and easy to understand.

Altering constraints later

SQLite can rename tables and add columns, but many constraint changes still require the classic pattern:

  1. create a new table
  2. copy the data
  3. drop or rename the old table
  4. rename the new table

INTEGER PRIMARY KEY behavior

INTEGER PRIMARY KEY is special in SQLite because it maps to the rowid. That can be useful for performance and identity generation.

Performance and design tips

Add constraints as early as possible

It is easier to stop bad data from entering than to clean it later.

Index foreign key columns used in joins

Constraints preserve integrity, but indexes preserve speed. If you join on customer_id often, add an index on the child table column.

Use CHECK for business rules that belong in the database

Examples include:

  • non-negative totals
  • allowed status lists
  • minimum quantities

Test edge cases explicitly

Try bad inserts in development so you know the exact failure message before users hit it.

FAQ

Why is my foreign key not enforced in SQLite?

Most often because PRAGMA foreign_keys = ON; was not enabled on that connection.

Can a UNIQUE column contain multiple NULL values?

Yes. SQLite treats NULL values as distinct for uniqueness checks.

Can I add or remove constraints later with ALTER TABLE?

Sometimes, but many changes still require rebuilding the table and copying the data.

What is the difference between PRIMARY KEY and UNIQUE?

Both prevent duplicates, but PRIMARY KEY identifies the main row identity for the table, while UNIQUE is usually for additional business keys such as email or external code.

Conclusion

SQLite constraints are the first line of defense for clean data. PRIMARY KEY, FOREIGN KEY, UNIQUE, NOT NULL, DEFAULT, and CHECK each solve a different integrity problem, and most real tables need several of them together.

You can define and test them in sqlite3, automate them from Python, and manage them visually in DbSchema when you need better schema visibility, SQL generation, and documentation.

References

  1. SQLite documentation
  2. SQLite foreign key documentation
  3. DbSchema Documentation

DbSchema Database Design & Management

Visual Design with ER Diagrams
DbSchema ER Diagram Features Overview
Visual Design & Schema Diagram

✓ Create and manage your database schema visually through a user-friendly graphical interface.

✓ Easily arrange tables, columns, and foreign keys to simplify complex database structures, ensuring clarity and accessibility.

GIT & Collaboration
Version Control & Collaboration

✓ Manage schema changes through version control with built-in Git integration, ensuring every update is tracked and backed up.

✓ Collaborate efficiently with your team to maintain data integrity and streamline your workflow for accurate, consistent results.

Data Explorer & Query Builder
Relational Data & Query Builder

✓ Seamlessly navigate and visually explore your database, inspecting tables and their relationships.

✓ Build complex SQL queries using an intuitive drag-and-drop interface, providing instant results for quick, actionable insights.

Interactive Documentation & Reporting
HTML5 Documentation & Reporting

✓ Generate HTML5 documentation that provides an interactive view of your database schema.

✓ Include comments for columns, use tags for better organization, and create visually reports.