Database Normalization Explained: 1NF, 2NF, and 3NF in Plain English
Normalization is the discipline of structuring tables to minimize duplicate data and avoid update anomalies. It’s also the concept that quietly justifies almost every schema decision made throughout this course — why Trip referenced Driver instead of repeating the driver’s name on every row, why phone numbers got their own table. Here’s what the first three normal forms actually mean, without the textbook jargon, and a worked example showing the actual bugs an unnormalized table produces.
The Three Rules
| Form | Rule, in plain language |
|---|---|
| 1NF | Every column holds one atomic value — no comma-separated lists crammed into a cell |
| 2NF | Every non-key column depends on the whole primary key, not just part of it (only matters when the key has multiple columns) |
| 3NF | Every non-key column depends only on the key — not on another non-key column |
1NF in Practice
-- VIOLATES 1NF: multiple phone numbers crammed into one column
-- phone_numbers = '555-1234, 555-5678' ❌
-- FIXED: one row per phone number in a related table
CREATE TABLE dbo.ContactPhone (
phone_id INT IDENTITY(1,1) PRIMARY KEY,
staff_id INT NOT NULL REFERENCES dbo.Staff(staff_id),
phone VARCHAR(20) NOT NULL
);
The comma-separated version isn’t just stylistically ugly — it’s functionally broken. You can’t easily search “who has this phone number,” can’t enforce a phone number is only associated with one person, and any query trying to count phone numbers per employee needs fragile string-splitting logic instead of a simple COUNT(*) ... GROUP BY.
2NF: A Worked Example With a Composite Key
-- VIOLATES 2NF: composite key is (order_id, product_id), but product_name
-- depends ONLY on product_id, not on the full key
CREATE TABLE dbo.OrderLine_Bad (
order_id INT,
product_id INT,
product_name NVARCHAR(100), -- ❌ repeated on every order line for this product
quantity INT,
PRIMARY KEY (order_id, product_id)
);
-- FIXED: product_name moves to its own table, keyed by product_id alone
CREATE TABLE dbo.Product (
product_id INT PRIMARY KEY,
product_name NVARCHAR(100) NOT NULL
);
CREATE TABLE dbo.OrderLine (
order_id INT,
product_id INT REFERENCES dbo.Product(product_id),
quantity INT NOT NULL,
PRIMARY KEY (order_id, product_id)
);
In the “bad” version, if a product gets renamed, you must update every single order line that ever referenced it — miss one, and your data now disagrees with itself about the product’s name. That’s the specific failure 2NF prevents: a partial dependency (product_name depending on only part of the composite key) causing update anomalies.
3NF: Transitive Dependencies
-- VIOLATES 3NF: department_name depends on department_id, not directly on staff_id (the key)
CREATE TABLE dbo.Staff_Bad (
staff_id INT PRIMARY KEY,
full_name NVARCHAR(100),
department_id INT,
department_name NVARCHAR(50) -- ❌ depends on department_id, a NON-key column
);
-- FIXED: department_name lives only in Department, referenced by FK
CREATE TABLE dbo.Department (department_id INT PRIMARY KEY, department_name NVARCHAR(50) NOT NULL);
CREATE TABLE dbo.Staff_Good (
staff_id INT PRIMARY KEY,
full_name NVARCHAR(100) NOT NULL,
department_id INT NOT NULL REFERENCES dbo.Department(department_id)
);
Same failure mode as 2NF, one step removed: department_name “transitively” depends on the key through department_id, rather than directly. Rename a department, and every staff row in the “bad” table needs updating in lockstep, or the data silently contradicts itself.
Normalized vs Denormalized, Visualized
Real schemas often deliberately break strict normalization for performance reasons — called denormalization. A reporting table might intentionally store department_name alongside staff data to avoid a JOIN on every single dashboard query, accepting the update-anomaly risk as a worthwhile tradeoff because that data changes rarely and is read constantly. Know the rules well enough to break them on purpose, with a clear reason, not by accident because you didn’t recognize the dependency in the first place.
Enjoyed this?
Subscribe to get every new SQL Server lesson as soon as it’s published, and share it with a developer who’d find it useful.
📡 Subscribe via RSS |
Share on X |
Share on LinkedIn |
Share on Facebook
Want the full structured course with quizzes, projects, and 10+ exercises per chapter? Check out SQL Server Fundamentals, coming soon on this site.