By WebNest Studio Engineering Team
Database and SQL Tutorial
Database Normalization (1NF, 2NF, 3NF)
Normalization is the process of organizing tables to reduce data redundancy and prevent update, insert, and delete anomalies. It works by progressively splitting data into smaller, well-defined tables connected by foreign keys, following a series of increasingly strict rules called "normal forms."
An anomaly is a symptom of poor design: for example, if a customer's address is repeated on every one of their orders, updating that address means updating many rows — and if even one is missed, the data becomes contradictory. Normalization eliminates this by storing each fact in exactly one place.
Why it matters
When the same fact is stored in several rows, sooner or later the copies disagree: a customer's city is updated in one row and not in another. Normalization organises tables so that each fact is stored once. It is the theory behind good schema design and a guaranteed topic in database interviews.
First Normal Form (1NF)
A table is in 1NF when every column holds a single, atomic value (no lists or repeating groups packed into one field) and each row is uniquely identifiable. A column like phone_numbers = '555-1111, 555-2222' violates 1NF because it stores multiple values in one field, making it impossible to search or index individual numbers properly.
Second Normal Form (2NF)
A table is in 2NF when it is already in 1NF and every non-key column depends on the entire primary key, not just part of it. This rule only matters for tables with a composite primary key; if a column depends on only one part of that composite key, it should be moved to a separate table.
Third Normal Form (3NF)
A table is in 3NF when it is already in 2NF and every non-key column depends only on the primary key — not on another non-key column. If column B's value can be derived from column A (both non-key), such as deriving a department's manager name from a department name stored redundantly, that dependency should be factored into its own table.
A Concrete Before/After Example
Consider a single unnormalized orders table storing order_id, customer_name, customer_email, product_name, product_price. If the same customer places two orders, their name and email are duplicated across rows — an update anomaly (changing an email requires updating multiple rows) and an insert anomaly (you cannot record a customer until they place an order). Normalizing splits this into customers(id, name, email), products(id, name, price), and orders(id, customer_id, product_id, order_date), so each fact about a customer or product is stored exactly once and simply referenced by foreign key.
Example
Un-normalized table (left implied) refactored into 3NF tables
-- Normalized structure after fixing the redundancy described above
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(150) UNIQUE NOT NULL
);
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name VARCHAR(120) NOT NULL,
price DECIMAL(10, 2) NOT NULL
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL REFERENCES customers(id),
product_id INTEGER NOT NULL REFERENCES products(id),
order_date DATE NOT NULL
);
Three tables created; a customer's email now exists in exactly one row regardless of how many orders they place.
Common Mistakes
- Storing comma-separated lists in a single column (e.g., "tags = red,large,sale") instead of a separate related table — this violates 1NF.
- Over-normalizing to the point that simple reports require joining ten or more tables, hurting both readability and performance — some controlled denormalization is a legitimate trade-off, not automatically wrong.
- Leaving a derived or duplicated column (like a customer's full address repeated on every order) that should have been moved to its own referenced table.
- Assuming normalization is only an academic exercise — unnormalized designs cause real update/insert/delete anomalies in production data.
Exercise
orders_flat repeats each customer's name and city on every order. Normalise it: create a customers table holding each customer once, and an orders table that refers to the customer by id. Fill both from the flat table, then count the rows of each.
Expected output: customers, 2, orders, 3 (one per line)
CREATE TABLE orders_flat (order_id INTEGER, customer_name TEXT, customer_city TEXT, product TEXT);
INSERT INTO orders_flat VALUES
(1, 'Asha', 'Pune', 'pen'),
(2, 'Asha', 'Pune', 'book'),
(3, 'Ravi', 'Delhi', 'bag');
-- TODO: create customers (id, name, city) and fill it with each distinct customer
-- TODO: create orders (id, customer_id, product) and fill it from the flat table
SELECT COUNT(*) AS customers FROM customers;
SELECT COUNT(*) AS orders FROM orders;
Show hints
INSERT INTO ... SELECT ... GROUP BY customer_name, customer_citycopies each customer once;MIN(order_id)is a convenient unique id for each.- To fill
orders, join the flat table tocustomerson the name to find each customer's id.
Show solution
CREATE TABLE orders_flat (order_id INTEGER, customer_name TEXT, customer_city TEXT, product TEXT);
INSERT INTO orders_flat VALUES
(1, 'Asha', 'Pune', 'pen'),
(2, 'Asha', 'Pune', 'book'),
(3, 'Ravi', 'Delhi', 'bag');
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
city TEXT
);
INSERT INTO customers (id, name, city)
SELECT MIN(order_id), customer_name, customer_city
FROM orders_flat
GROUP BY customer_name, customer_city;
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL REFERENCES customers(id),
product TEXT
);
INSERT INTO orders (id, customer_id, product)
SELECT f.order_id, c.id, f.product
FROM orders_flat f
JOIN customers c ON c.name = f.customer_name;
SELECT COUNT(*) AS customers FROM customers;
SELECT COUNT(*) AS orders FROM orders;
Quick quiz
1. Which table violates first normal form?
- A. A table with a primary key
- B. A table with a column holding several phone numbers separated by commas
- C. A table with two columns
- D. A table with a foreign key
Show answer
B. 1NF requires each column to hold a single, atomic value.
2. What is the main purpose of normalization?
- A. To make queries shorter
- B. To use fewer tables
- C. To remove redundancy, so that each fact is stored once and cannot become inconsistent
- D. To encrypt the data
Show answer
C. It prevents update, insert and delete anomalies.
3. What does third normal form forbid?
- A. A non-key column that depends on another non-key column
- B. Foreign keys
- C. NULL values
- D. More than three tables
Show answer
A. For example, storing both zip_code and city in an orders table, where city depends on zip_code.
Interview questions
Explain first, second and third normal form.
Show answer
First normal form: every column holds a single value, with no lists or repeating groups, and each row is unique. Second normal form: the table is in 1NF, and every non-key column depends on the whole primary key, not on part of a composite key. Third normal form: the table is in 2NF, and no non-key column depends on another non-key column. A common summary is that every non-key column must depend on the key, the whole key, and nothing but the key.
What is denormalization and when is it justified?
Show answer
Denormalization deliberately stores redundant data, such as a customer's name copied onto each order or a precomputed total, to avoid joins or calculations at read time. It is justified when reads greatly outnumber writes and a measured performance problem cannot be solved with indexes or caching, as in reporting tables. The price is that the copies must be kept in step, so it should be a conscious trade and not a starting point.
Key Points to Remember
- 1NF requires atomic column values and no repeating groups.
- 2NF requires non-key columns to depend on the whole composite primary key, not part of it.
- 3NF requires non-key columns to depend only on the primary key, not on other non-key columns.
- Normalization eliminates redundancy and the update/insert/delete anomalies it causes, at the cost of requiring more joins to reassemble data.
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