By WebNest Studio Engineering Team
Database Design Tutorial
Enforcing Data Integrity in Database Design
Data integrity means the database itself refuses to store data that violates the rules of the business, rather than relying entirely on application code to check those rules before every insert or update. Application-level validation is useful for giving users friendly error messages, but it is not a substitute for database-level constraints, because application code can have bugs, can be bypassed by a second application or a direct database script, and can simply be forgotten in one code path out of many.
Designing for data integrity means treating NOT NULL, foreign keys, UNIQUE, and CHECK constraints as first-class design decisions made table by table and column by column — not an afterthought bolted on after the schema is "working."
Why it matters
Bad data is easy to let in and very hard to find and repair afterwards. A price of zero, a product with no name or a row pointing to a category that does not exist will each cause a failure somewhere far from where it entered. Rules written into the schema stop bad data at the door, whichever program is writing.
NOT NULL: Making Required Fields Actually Required
Every column should be explicitly decided as required or optional; NOT NULL is the mechanism that turns "this should always have a value" from a comment or a hope into an enforced guarantee. A column left nullable by default, without a decision, is the single most common source of "why is this field empty in production" bugs — because eventually some code path, some import script, or some manual fix will skip setting it.
Foreign Keys: Preventing Orphaned and Invalid References
A foreign key constraint guarantees that a value like orders.customer_id can never point to a customer that does not exist. Without this constraint enforced at the database level, a bug or a race condition in application code can insert an order referencing a deleted or nonexistent customer, and that broken reference then corrupts every report or join that assumes the relationship is valid.
UNIQUE and CHECK: Guarding Business Rules Directly in the Schema
UNIQUE constraints prevent duplicate values where the business requires exactly one (one email per customer account, one SKU per product). CHECK constraints enforce rules about the value itself, independent of other rows — a quantity must be greater than zero, a discount percentage must be between 0 and 100, a status column must be one of a fixed set of values. These guardrails catch bad data at the moment it is written, which is far cheaper than discovering and cleaning it up after it has spread through reports and downstream systems.
- NOT NULL — a value must always be present.
- FOREIGN KEY — a value must reference an existing row in another table.
- UNIQUE — no two rows may share the same value (or combination of values).
- CHECK — a value must satisfy a specific condition (range, set of allowed values, sign).
Example
A products table with layered integrity guardrails
CREATE TABLE products (
id BIGINT PRIMARY KEY,
sku VARCHAR(50) NOT NULL UNIQUE,
name VARCHAR(150) NOT NULL,
unit_price DECIMAL(10, 2) NOT NULL CHECK (unit_price >= 0),
stock_qty INT NOT NULL DEFAULT 0 CHECK (stock_qty >= 0),
status VARCHAR(20) NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'discontinued', 'out_of_stock')),
category_id BIGINT NOT NULL REFERENCES product_categories(id)
);
-- These inserts are rejected by the database itself, before any
-- application code even runs:
-- INSERT INTO products (id, sku, name, unit_price, category_id)
-- VALUES (1, NULL, 'Widget', 9.99, 1); -- NOT NULL violation on sku
-- INSERT INTO products (id, sku, name, unit_price, category_id)
-- VALUES (2, 'SKU-1', 'Widget', -5.00, 1); -- CHECK violation on unit_price
-- INSERT INTO products (id, sku, name, unit_price, category_id)
-- VALUES (3, 'SKU-1', 'Widget', 9.99, 999); -- FK violation, category 999 doesn't exist
Each commented insert fails with a distinct, specific database error
(NOT NULL violation, CHECK constraint violation, foreign key violation)
rather than silently storing invalid data. Valid rows must have a
non-null unique SKU, a non-negative price, non-negative stock, a status
from the fixed allowed set, and a category that actually exists.
Common Mistakes
- Relying solely on application-level validation and leaving the corresponding database columns nullable or unconstrained, so a second application, script, or bug can bypass the rule entirely.
- Adding a foreign key column without an actual FOREIGN KEY constraint, which allows orphaned or invalid references to accumulate silently.
- Using a free-text VARCHAR column for a value that should be restricted to a fixed set of options, instead of a CHECK constraint (or enum type), allowing typos like "actve" to sit undetected in production data.
- Adding constraints only after a data quality problem has already occurred in production, instead of designing them in from the first migration.
Exercise
Create a products table with four guards: name is required; sku is unique; price must be greater than zero; and category_id must refer to an existing category. Of the four inserts below, only the first is valid. Count the rows that were accepted.
Expected output: total then 1
CREATE TABLE categories (id INTEGER PRIMARY KEY, name TEXT NOT NULL);
INSERT INTO categories (id, name) VALUES (1, 'Stationery');
-- TODO: products (id, name, sku, price, category_id) with the four guards
INSERT INTO products (id, name, sku, price, category_id) VALUES (1, 'Pen', 'PEN-1', 12, 1);
INSERT INTO products (id, name, sku, price, category_id) VALUES (2, 'Pencil', 'PEN-1', 5, 1);
INSERT INTO products (id, name, sku, price, category_id) VALUES (3, 'Ruler', 'RUL-1', 0, 1);
INSERT INTO products (id, name, sku, price, category_id) VALUES (4, 'Bag', 'BAG-1', 450, 9);
SELECT COUNT(*) AS total FROM products;
Show hints
- The four guards are
NOT NULL,UNIQUE,CHECK (price > 0)andREFERENCES categories(id). - The second insert repeats a sku, the third has a price of zero and the fourth names a category that does not exist.
Show solution
CREATE TABLE categories (id INTEGER PRIMARY KEY, name TEXT NOT NULL);
INSERT INTO categories (id, name) VALUES (1, 'Stationery');
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
sku TEXT NOT NULL UNIQUE,
price INTEGER NOT NULL CHECK (price > 0),
category_id INTEGER NOT NULL REFERENCES categories(id)
);
INSERT INTO products (id, name, sku, price, category_id) VALUES (1, 'Pen', 'PEN-1', 12, 1);
INSERT INTO products (id, name, sku, price, category_id) VALUES (2, 'Pencil', 'PEN-1', 5, 1);
INSERT INTO products (id, name, sku, price, category_id) VALUES (3, 'Ruler', 'RUL-1', 0, 1);
INSERT INTO products (id, name, sku, price, category_id) VALUES (4, 'Bag', 'BAG-1', 450, 9);
SELECT COUNT(*) AS total FROM products;
Quick quiz
1. What is an orphaned row?
- A. A row with a NULL primary key
- B. A child row whose foreign key points to a parent that no longer exists
- C. A row with no columns
- D. A duplicate row
Show answer
B. A foreign key constraint makes orphaned rows impossible.
2. Why should a column that is always required be declared NOT NULL?
- A. It makes the column unique
- B. It encrypts the column
- C. The database then rejects any row that omits it, whichever program inserts the row
- D. It creates an index
Show answer
C. Without it, a single buggy code path can leave gaps that break later queries.
3. Which constraint best expresses "a discount must be between 0 and 100"?
- A. CHECK (discount BETWEEN 0 AND 100)
- B. UNIQUE (discount)
- C. DEFAULT 100
- D. FOREIGN KEY (discount)
Show answer
A. A CHECK constraint holds a condition that every row must satisfy.
Interview questions
What are the types of data integrity?
Show answer
Entity integrity: every row is uniquely identifiable, enforced by the primary key. Referential integrity: relationships between tables stay valid, enforced by foreign keys. Domain integrity: each column holds only acceptable values, enforced by data types, NOT NULL, CHECK and defaults. User-defined integrity covers business rules beyond these, enforced with constraints, triggers or application logic.
Should validation live in the application or in the database?
Show answer
In both, for different reasons. The application validates to give users immediate, friendly feedback. The database enforces the rules that must always hold, because applications have bugs and are not the only writers: scripts, imports and other services bypass them. Rules about the shape and consistency of the data belong in the schema; complex business workflows usually stay in the application.
Key Points to Remember
- Database-level constraints (NOT NULL, FOREIGN KEY, UNIQUE, CHECK) enforce rules that application code alone cannot guarantee, since app code can have bugs or be bypassed.
- Every column should have an explicit required/optional decision expressed as NOT NULL or nullable, not left nullable by default.
- Foreign keys prevent orphaned references; UNIQUE prevents duplicates; CHECK enforces valid ranges and allowed value sets.
- Catching bad data at write time with constraints is far cheaper than discovering and cleaning it after it has spread downstream.
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Runs your SQL on SQLite in your browser. Standard SQL works; a few features that exist only in PostgreSQL or MySQL do not. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
These examples need a project environment on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering