Normalization — 1NF to BCNF
In simple words
If the same fact is saved in many places — say Alice's city copied into every one of her orders — then when she moves you have to fix every copy, and if you miss one the data contradicts itself. Normalization means arranging your tables so each fact is saved in exactly one place, and other tables just point to it. It is done in steps (called normal forms: 1NF, 2NF, 3NF and so on), each step removing one particular kind of repetition.
What is Normalization?
Normalization is the process of organizing a database to reduce data redundancy and eliminate data anomalies by applying a series of rules called Normal Forms. Data Redundancy means storing same data in more than one physical and logical locations. Data Anomaly means unexpected pattern in data like storing all data in one place.
Un-normalized DB:
Same data stored in multiple places → update one → others go stale → data inconsistency
Normalized DB:
Each piece of data stored exactly once → update one place → consistent everywhere
The Problem — Data Anomalies
Before learning normal forms, understand WHY normalization exists.
Start with this poorly designed table that stores everything in one place:
Table: orders_raw (everything crammed in)
┌──────────┬─────────┬───────────┬──────────────────┬─────────┬──────────┬──────────────┬────────────┐
│ order_id │ user_id │ user_name │ user_email │ city │ prod_id │ product_name │ prod_price │
├──────────┼─────────┼───────────┼──────────────────┼─────────┼──────────┼──────────────┼────────────┤
│ 1001 │ 1 │ Alice │ alice@email.com │ Mumbai │ P01 │ Shoes │ 500 │
│ 1002 │ 1 │ Alice │ alice@email.com │ Mumbai │ P02 │ T-shirt │ 200 │
│ 1003 │ 2 │ Bob │ bob@email.com │ Delhi │ P01 │ Shoes │ 500 │
│ 1004 │ 2 │ Bob │ bob@email.com │ Delhi │ P03 │ Watch │ 1500 │
└──────────┴─────────┴───────────┴──────────────────┴─────────┴──────────┴──────────────┴────────────┘
This has three types of problems:
1. Update Anomaly
Alice moves from Mumbai to Delhi. You must update EVERY row where user_id = 1. Miss one → Alice lives in two cities simultaneously.
Update bob@email.com to bob@gmail.com
→ Must change 2 rows
→ If one row is missed → same person has two different emails
2. Insertion Anomaly
You want to add a new product (Bag, ₹800) to the catalog. But there is no order for it yet — you CANNOT insert it because order_id cannot be NULL (it's part of the key).
Cannot add product without an order → data about products is trapped inside orders
3. Deletion Anomaly
Carol (user_id = 3) cancels her only order. You delete that row → Carol's user information is GONE from the database entirely.
Deleting an order deletes user data too → unintended data loss
Functional Dependency — The Core Concept
A functional dependency X → Y means: "knowing X, you can always determine Y."
user_id → user_name (knowing user_id tells you the user's name)
user_id → email (knowing user_id tells you the email)
product_id → product_name (knowing product_id tells you the product name)
order_id → order_date (knowing order_id tells you the date)
order_id → user_id (knowing order_id tells you which user placed it)
But:
user_id does NOT → product_name (knowing a user doesn't tell you a product)
Types of Functional Dependencies
Full dependency: X → Y where Y depends on ALL of X
(order_id, product_id) → quantity
quantity needs BOTH order_id AND product_id
Partial dependency: X → Y where Y depends on PART of X
(order_id, product_id) → product_name
product_name only needs product_id, not order_id
This is a partial dependency — violation of 2NF
Transitive dependency: X → Z where X → Y and Y → Z
user_id → city_id → city_name
city_name depends on city_id, not directly on user_id
This is a transitive dependency — violation of 3NF
1NF — First Normal Form
Rule
- Every column must hold atomic (indivisible) values — no lists, no sets, no arrays
- Every row must be unique (have a primary key)
- No repeating groups of columns
❌ Violates 1NF
Table: orders_bad
┌──────────┬─────────┬──────────────────────────────────┐
│ order_id │ user_id │ products │
├──────────┼─────────┼──────────────────────────────────┤
│ 1001 │ 1 │ Shoes, T-shirt │ ← multiple values in one cell
│ 1002 │ 2 │ Watch │
└──────────┴─────────┴──────────────────────────────────┘
Also bad — repeating columns:
┌──────────┬───────────┬──────────────┬──────────────┐
│ order_id │ product_1 │ product_2 │ product_3 │
├──────────┼───────────┼──────────────┼──────────────┤
│ 1001 │ Shoes │ T-shirt │ NULL │
└──────────┴───────────┴──────────────┴──────────────┘
✅ Converted to 1NF
Split multi-valued cells into separate rows:
┌──────────┬─────────┬──────────┐
│ order_id │ user_id │ prod_id │
├──────────┼─────────┼──────────┤
│ 1001 │ 1 │ P01 │
│ 1001 │ 1 │ P02 │
│ 1002 │ 2 │ P03 │
└──────────┴─────────┴──────────┘
PK = (order_id, prod_id)
1NF achieved: each cell holds one atomic value, each row is unique.
2NF — Second Normal Form
Rule
- Must be in 1NF
- No partial dependencies — every non-key column must depend on the ENTIRE primary key, not just part of it
2NF only applies when the primary key is composite (multiple columns).
Current table after 1NF (with more columns):
Table: order_products (PK = order_id + prod_id)
┌──────────┬─────────┬──────────┬──────────────┬──────────┬─────────────────┐
│ order_id │ prod_id │ quantity │ product_name │ price │ order_date │
├──────────┼─────────┼──────────┼──────────────┼──────────┼─────────────────┤
│ 1001 │ P01 │ 1 │ Shoes │ 500 │ 2024-01-10 │
│ 1001 │ P02 │ 2 │ T-shirt │ 200 │ 2024-01-10 │
│ 1002 │ P01 │ 1 │ Shoes │ 500 │ 2024-01-11 │
└──────────┴─────────┴──────────┴──────────────┴──────────┴─────────────────┘
Functional dependencies:
(order_id, prod_id) → quantity ✅ full dependency — needs both
prod_id → product_name ❌ partial — only needs prod_id
prod_id → price ❌ partial — only needs prod_id
order_id → order_date ❌ partial — only needs order_id
❌ Partial Dependencies (violates 2NF)
product_name and price depend only on prod_id — not on the full PK
order_date depends only on order_id — not on the full PK
Result:
- "Shoes" stored in EVERY row that includes P01 → update anomaly
- Change Shoes price → update 100 rows → miss one → inconsistent data
✅ Converted to 2NF
Split tables so each column depends on its ENTIRE key:
Table: order_items (PK = order_id + prod_id) Table: orders (PK = order_id)
┌──────────┬─────────┬──────────┐ ┌──────────┬────────────┐
│ order_id │ prod_id │ quantity │ │ order_id │ order_date │
├──────────┼─────────┼──────────┤ ├──────────┼────────────┤
│ 1001 │ P01 │ 1 │ │ 1001 │ 2024-01-10 │
│ 1001 │ P02 │ 2 │ │ 1002 │ 2024-01-11 │
│ 1002 │ P01 │ 1 │ └──────────┴────────────┘
└──────────┴─────────┴──────────┘
Table: products (PK = prod_id)
┌─────────┬──────────────┬───────┐
│ prod_id │ product_name │ price │
├─────────┼──────────────┼───────┤
│ P01 │ Shoes │ 500 │
│ P02 │ T-shirt │ 200 │
└─────────┴──────────────┴───────┘
2NF achieved: every non-key column depends on the ENTIRE primary key.
3NF — Third Normal Form
Rule
- Must be in 2NF
- No transitive dependencies — non-key columns must NOT depend on other non-key columns
Current orders table (extended with user data):
Table: orders (PK = order_id)
┌──────────┬─────────┬────────────┬───────────┬────────────────┬─────────┐
│ order_id │ user_id │ order_date │ user_name │ user_email │ city │
├──────────┼─────────┼────────────┼───────────┼────────────────┼─────────┤
│ 1001 │ 1 │ 2024-01-10 │ Alice │ alice@email.com│ Mumbai │
│ 1002 │ 1 │ 2024-01-11 │ Alice │ alice@email.com│ Mumbai │
│ 1003 │ 2 │ 2024-01-12 │ Bob │ bob@email.com │ Delhi │
└──────────┴─────────┴────────────┴───────────┴────────────────┴─────────┘
Functional dependencies:
order_id → user_id ✅ direct
order_id → order_date ✅ direct
order_id → user_name ❌ TRANSITIVE: order_id → user_id → user_name
order_id → user_email ❌ TRANSITIVE: order_id → user_id → user_email
order_id → city ❌ TRANSITIVE: order_id → user_id → city
❌ Transitive Dependencies (violates 3NF)
user_name, user_email, city all depend on user_id — not directly on order_id
Result:
- Alice appears in every row for her orders → update anomaly
- Change Alice's city → update multiple rows in orders table
- Delete Alice's last order → lose Alice's contact info
✅ Converted to 3NF
Table: orders (PK = order_id) Table: users (PK = user_id)
┌──────────┬─────────┬────────────┐ ┌─────────┬───────────┬────────────────┬─────────┐
│ order_id │ user_id │ order_date │ │ user_id │ user_name │ user_email │ city │
├──────────┼─────────┼────────────┤ ├─────────┼───────────┼────────────────┼─────────┤
│ 1001 │ 1 │ 2024-01-10 │ │ 1 │ Alice │ alice@email.com│ Mumbai │
│ 1002 │ 1 │ 2024-01-11 │ │ 2 │ Bob │ bob@email.com │ Delhi │
│ 1003 │ 2 │ 2024-01-12 │ └─────────┴───────────┴────────────────┴─────────┘
└──────────┴─────────┴────────────┘
3NF achieved: non-key columns depend ONLY on the primary key — nothing else.
BCNF — Boyce-Codd Normal Form
Rule
- Must be in 3NF
- For every functional dependency X → Y, X must be a superkey (X must uniquely identify rows — it must be a candidate key)
BCNF is a stricter version of 3NF. Most tables in 3NF are also in BCNF. Violations only occur with overlapping composite candidate keys.
BCNF Violation Example
Table: course_teacher (a student can take a course from only one teacher,
a teacher teaches only one course,
but a course can be taught by multiple teachers)
┌──────────┬─────────┬──────────────┐
│ student │ course │ teacher │
├──────────┼─────────┼──────────────┤
│ Alice │ Math │ Prof. Kumar │
│ Alice │ Physics │ Prof. Sharma │
│ Bob │ Math │ Prof. Patel │
│ Carol │ Math │ Prof. Kumar │
└──────────┴─────────┴──────────────┘
Candidate keys:
(student, course) → uniquely identifies a row ✅
(student, teacher) → uniquely identifies a row ✅
Functional dependency:
teacher → course (each teacher teaches only one course)
But: teacher is NOT a superkey (teacher alone doesn't identify a row)
→ BCNF violated
✅ Converted to BCNF
Table: student_teacher Table: teacher_course
┌──────────┬──────────────┐ ┌──────────────┬─────────┐
│ student │ teacher │ │ teacher │ course │
├──────────┼──────────────┤ ├──────────────┼─────────┤
│ Alice │ Prof. Kumar │ │ Prof. Kumar │ Math │
│ Alice │ Prof. Sharma │ │ Prof. Sharma │ Physics │
│ Bob │ Prof. Patel │ │ Prof. Patel │ Math │
│ Carol │ Prof. Kumar │ └──────────────┴─────────┘
└──────────┴──────────────┘
Normal Forms — Visual Summary
Raw Table (Un-normalized)
↓
┌─────────────────────────────────────────────────────────────┐
│ 1NF — Atomic values, no repeating groups, has primary key │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 2NF — 1NF + no partial dependencies │
│ (non-key columns depend on FULL composite PK) │
│ Only relevant when PK is composite │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 3NF — 2NF + no transitive dependencies │
│ (non-key columns depend ONLY on PK, not on other │
│ non-key columns) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ BCNF — 3NF + every determinant is a superkey │
│ Stricter than 3NF, eliminates subtle redundancies │
└─────────────────────────────────────────────────────────────┘
Quick Check — Violation Identifier
Is any cell storing multiple values? → violates 1NF
Does a non-key column depend on PART of a
composite PK? → violates 2NF
Does a non-key column depend on another
non-key column? → violates 3NF
Does a functional dependency exist where
the left side is not a superkey? → violates BCNF
The Forms Nest — Both Directions
Each form is literally "the previous form + one more rule", so:
SATISFYING a higher form guarantees the lower ones:
BCNF ✅ → 3NF ✅ → 2NF ✅ → 1NF ✅
SATISFYING a lower form guarantees NOTHING above it:
1NF ✅ → says nothing about 2NF / 3NF / BCNF
VIOLATING a lower form breaks every form above it:
1NF ❌ → 2NF ❌, 3NF ❌, BCNF ❌
3NF ❌ → BCNF ❌ (but it could still be 2NF ✅ while failing 3NF)
So when you spot a multi-valued cell, you don't need to check anything else — the table fails 1NF and therefore every form. Work bottom-up: fix 1NF, then 2NF, then 3NF.
Denormalization — Going Backwards (Important for System Design)
Normalization is ideal for data integrity. But at scale, normalized tables require many JOINs → JOINs across millions of rows → slow queries.
Denormalization intentionally introduces redundancy to improve read performance.
When to Denormalize
Normalized (3NF):
To get a user's order with product names:
users JOIN orders JOIN order_items JOIN products
4-table JOIN → expensive at 100M rows
Denormalized:
orders_summary table stores:
order_id, user_name, user_email, product_names (JSON array), total_amount
→ single table read, no joins → fast
→ but user_name is duplicated → update Alice's name → must update orders_summary too
Strategies
1. Store computed columns
Add total_amount to orders table (sum of order_items)
Update it when items are added/removed
→ fast reads, but write complexity increases
2. Duplicate frequently joined columns
Add user_name and user_email to orders table
→ fast reads, but must keep in sync with users table
3. Pre-aggregated tables (summary tables)
daily_revenue table: (date, total_revenue, total_orders)
Updated by a background job daily
→ dashboard reads from this instead of aggregating orders in real time
4. Read replicas + materialized views
Not denormalization per se, but achieves the same goal
→ normalize the source, denormalize the read model
The Tradeoff
Normalization → write-optimized, consistent, storage-efficient, slow complex reads
Denormalization → read-optimized, redundant, inconsistent risk, fast reads
System Design Context
Real-World Normalization Decisions
Flipkart product catalog:
Products table (normalized):
products → categories (FK)
products → brands (FK)
products → product_attributes (1:N for flexible attributes)
For search/display: denormalized Elasticsearch index stores everything flat
product_id, name, brand_name, category_name, price, attributes...
→ fast search, no joins at search time
→ synced from normalized DB via CDC (Change Data Capture)
Twitter/X feed:
Normalized: tweets table, users table (JOIN to get username + avatar)
Denormalized: tweet_feed stores user_handle + avatar_url inline
→ reading 100 tweets doesn't need 100 JOINs to users table
→ downside: if user changes username, all their cached tweets show old name
→ Twitter accepts eventual consistency for username display
Interview Decision Framework
Use normalized (3NF) when:
- Data integrity is critical (financial, medical)
- Write-heavy workload
- Small-medium scale (< 10M rows)
- Complex queries that vary (reporting, analytics)
Denormalize when:
- Read-heavy, read patterns are known and fixed
- Scale requires removing JOINs (100M+ rows)
- Latency requirements are strict (< 10ms per query)
- The data is derived/computed (can always be recalculated)
How This Connects
- Relational Model - Tables, Keys, Relationships — normalization decides which tables the relational model should have; candidate keys and functional dependencies are the raw material.
- SQL - Joins, Aggregations, Subqueries — every table you split is a JOIN you now have to write; normalization is why JOINs exist.
- Indexing - B-Tree & Hash Indexes — more tables means more foreign-key columns that need indexes to keep those JOINs fast.
- ACID Properties — the anomalies normalization prevents are consistency (C) problems; denormalization trades some of that guarantee for speed.
- Module 4 — Data Architecture / Storage Engines — denormalized read models, materialized views, search indexes synced by CDC (Change Data Capture) are normalization run in reverse for scale.
- NoSQL Types — document stores deliberately denormalize (embed related data) so a read is one lookup, not a join.
In the Interview
When to say it. Any "design the schema" prompt, any "what's wrong with this table?", and whenever you propose denormalization you should be able to say what normal form you're breaking and why it's worth it.
The ~20-second version:
"Normalization removes redundancy so each fact is stored once, which prevents update, insertion, and deletion anomalies. 1NF means atomic cell values and a primary key — no comma-separated lists. 2NF applies when the key is composite: every non-key column must depend on the whole key, not part of it —
product_namein an(order_id, product_id)table depends only onproduct_id, so it moves to aproductstable. 3NF means no non-key column depends on another non-key column —user_cityinordersdepends onuser_id, notorder_id, so it moves tousers. I normalize transactional data to 3NF, then denormalize specific read-heavy paths — a summary table or a flat search index — accepting the sync cost for the latency win."
Follow-ups to expect:
- "What's a functional dependency?" —
X → Ymeans knowing X always determines Y. Partial dependency (on part of a composite key) breaks 2NF; transitive (non-key → non-key) breaks 3NF. - "3NF vs BCNF?" — BCNF is stricter: every determinant must be a candidate key. They differ only when you have overlapping composite candidate keys — rare in practice.
- "Why not always denormalize for speed?" — Every duplicated value can drift out of sync; you take on triggers/jobs/app logic to keep copies consistent, and writes get more expensive.
- "How do you keep a denormalized copy consistent?" — Update it in the same transaction, or via CDC / an async job, and accept eventual consistency for non-critical fields (e.g. a cached display name).
Mistake to avoid: reciting "2NF removes partial dependencies" without being able to point at the offending column and name the dependency. Interviewers probe with a concrete table — practice spotting the FD, not the definition.
Quick Revision — 6 Lines
- Normalization removes redundancy and prevents update / insert / delete anomalies
- 1NF — atomic values, no repeating groups, must have a primary key
- 2NF — no partial dependencies (non-key must depend on FULL composite PK)
- 3NF — no transitive dependencies (non-key must depend ONLY on PK, not other non-key cols)
- BCNF — stricter 3NF: every determinant in any FD must be a superkey
- Denormalize deliberately for read-heavy systems — trade consistency risk for speed