Database Normalization: 1NF to 3NF

โฑ๏ธ 90 sec read ๐Ÿ“Š SQL

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:

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.

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.

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