Database Normalization: 1NF to 3NF
Database normalization is the process of structuring tables so every fact is stored exactly once: 1NF removes repeating groups, 2NF removes columns that depend on only part of the key, and 3NF removes columns that depend on other non-key columns. For most transactional databases, normalizing to 3NF is the right default.
Why Does Normalization Matter?
Duplicated data eventually contradicts itself: if a customer's name is stored in 40 order rows and an update touches only 39 of them, your database now holds two versions of the truth. Normalization prevents three classic anomalies:
- Update anomaly: changing one fact requires editing many rows
- Insert anomaly: you can't record a department until it has an employee
- Delete anomaly: deleting the last order for a customer erases the customer too
What Is First Normal Form (1NF)?
1NF requires that every column holds a single atomic value and every row is unique; no comma-separated lists, no repeating column groups like phone1, phone2, phone3. If you find yourself writing LIKE '%555-0002%' to search a column, that column violates 1NF.
Before 1NF (Bad)
| customer_id | name | phones |
|-------------|-------|---------------------|
| 1 | Alice | 555-0001, 555-0002 |
After 1NF (Good)
| customer_id | name | phone |
|-------------|-------|----------|
| 1 | Alice | 555-0001 |
| 1 | Alice | 555-0002 |
In practice you'd go one step further and split phones into their own table keyed by customer_id, so the customer's name isn't repeated either.
What Is Second Normal Form (2NF)?
2NF requires 1NF plus no partial dependencies: every non-key column must depend on the entire primary key, not just part of it. This only bites when you have a composite key, which is why 2NF violations almost always show up in junction tables like order line items.
Before 2NF (Bad)
-- Primary key is (order_id, product_id)
| order_id | product_id | customer_name | product_name |
|----------|------------|---------------|--------------|
| 1 | 101 | Alice | Widget |
| 1 | 102 | Alice | Gadget |
-- customer_name depends only on order_id, not the full key!
-- product_name depends only on product_id.
After 2NF (Good)
-- Orders table
| order_id | customer_name |
|----------|---------------|
| 1 | Alice |
-- Products table
| product_id | product_name |
|------------|--------------|
| 101 | Widget |
-- Order_items table (pure junction)
| order_id | product_id | quantity |
|----------|------------|----------|
| 1 | 101 | 2 |
What Is Third Normal Form (3NF)?
3NF requires 2NF plus no transitive dependencies: a non-key column may not depend on another non-key column. The tell is a pair of columns that always travel together, like dept_id and dept_name, or zip_code and city.
Before 3NF (Bad)
| employee_id | name | dept_id | dept_name |
|-------------|-------|---------|--------------|
| 1 | Alice | 10 | Engineering |
| 2 | Bob | 10 | Engineering |
-- dept_name depends on dept_id, not on employee_id!
After 3NF (Good)
-- Employees table
| employee_id | name | dept_id |
|-------------|-------|---------|
| 1 | Alice | 10 |
| 2 | Bob | 10 |
-- Departments table
| dept_id | dept_name |
|---------|--------------|
| 10 | Engineering |
Renaming Engineering now means updating one row instead of thousands. Enforce these relationships with foreign keys so the database rejects orphaned references.
What About BCNF?
Boyce-Codd Normal Form is a stricter 3NF: every determinant (any column or set of columns that determines another) must itself be a candidate key. Tables in 3NF are almost always in BCNF too; it only differs in rare cases with multiple overlapping composite candidate keys, so don't lose sleep over it.
How Do You Normalize an Existing Table in SQL?
Extract the repeating attribute into its own table with SELECT DISTINCT, add a foreign key column to the original table, backfill it with an UPDATE ... JOIN, and only then drop the redundant column. Doing the drop last means you can verify the migration before anything is lost.
-- 1. Extract departments from employees
CREATE TABLE departments (
dept_id INT PRIMARY KEY,
dept_name VARCHAR(100) NOT NULL
);
INSERT INTO departments
SELECT DISTINCT dept_id, dept_name FROM employees;
-- 2. Enforce the relationship
ALTER TABLE employees
ADD CONSTRAINT fk_dept
FOREIGN KEY (dept_id) REFERENCES departments(dept_id);
-- 3. Verify, then drop the redundant column
ALTER TABLE employees DROP COLUMN dept_name;
When Should You Denormalize?
Denormalize when read performance measurably matters more than write safety, typically in reporting and analytics workloads where data is loaded in batches and rarely updated in place. Duplicating a column costs you update consistency but saves a join on every query.
- Data warehouses: star schemas intentionally denormalize dimensions for fast aggregation
- Read-heavy hot paths: caching
customer_nameon an orders report table to skip a join executed millions of times a day - Computed aggregates: storing
order_totalinstead of summing line items on every page load - Historical snapshots: copying the product price into the order row is correct, not lazy - the price at time of sale shouldn't change when the catalog does
Before denormalizing an OLTP table, check whether an index or a materialized view solves the problem first; see index strategy for the cheaper fix.
Practical Rules of Thumb
You rarely need to recite the formal definitions; a few habits get you to 3NF naturally and tell you when to stop.
- Normalize to 3NF by default for transactional (OLTP) systems; denormalize deliberately for analytics
- One table per real-world entity: customers, products, orders, order_items
- If a value is repeated verbatim across many rows, it probably wants its own table
- If updating one fact means updating many rows, you're under-normalized
- If every query joins six tables to answer a basic question, you may be over-normalized for your workload
- Snapshot data (price at purchase, address at shipping) belongs in the transaction row on purpose
Pro Tip: Normalize to 3NF for transactional databases. For analytical/reporting databases, denormalization can improve query performance. Know when to break the rules!
โ Back to SQL Tips