1. Home
  2. Blog
  3. Databases & SQL

Databases & SQL

Database Normalization Explained: 1NF, 2NF, 3NF and BCNF With One Worked Example

Normal forms sound academic until bad data costs you money. Follow one messy orders table as we fix it step by step, and learn what each rule really means and when to bend it.

9 min read
Database normalization explained: 1NF, 2NF, 3NF and BCNF with one worked example

Key takeaways

  • Normalization organizes tables so each fact is stored in exactly one place.
  • 1NF means single values in each column, 2NF removes partial dependencies and 3NF removes transitive dependencies.
  • Each form builds on the one before, and 3NF is a sensible target for most business applications.
  • Denormalize only when you have measured a real read performance problem.

A customer moves city. You update their address, but only on one of their ten order rows. Now your database gives two different answers to one simple question, and nobody knows which is right. Database normalization is the design discipline that prevents this kind of problem.

Definitions of normal forms can seem abstract, so we will fix one table from start to finish instead. We created the final schema in SQLite and ran the join at the end to confirm it rebuilds the original information. Column names and values are invented for this example.

What is database normalization?

Database normalization is the process of structuring a relational database to reduce duplicated data and protect its integrity. In practice, it means splitting one wide table into several focused tables, and linking them with keys. Edgar F. Codd introduced the idea in the 1970s, and the rules later developed from it are known as normal forms.

The forms are cumulative: a table in 3NF is also in 2NF and 1NF. You work through them in order, and each one removes a specific kind of redundancy.

The messy table

Imagine a small shop that keeps everything in one table:

order_id customer city pincode products order_date
1 Asha Pune 411001 Keyboard x1 (1500), Mouse x2 (500) 2026-10-01
2 Ben Delhi 110001 Monitor x1 (9000) 2026-10-02

It looks convenient, and a spreadsheet often starts just like this. But try writing a query for “how many keyboards did we sell?” You would have to parse text, and that is only the first problem.

Three anomalies that normalization prevents

Redundant data leads to three classic problems, known as anomalies:

  • Update anomaly: Asha’s city is repeated on every order placed. Change it in one row but miss another, and your data contradicts itself.
  • Insert anomaly: you cannot add a new product to the catalogue until someone orders it, because products exist only inside orders.
  • Delete anomaly: delete Ben’s only order, and you lose the fact that Ben exists, along with the fact that you sell monitors.

Keys and dependencies in plain English

Three terms make all the rules easier to follow:

  • Primary key: the column or columns that identify one row uniquely. Our guide to primary keys vs foreign keys covers this in depth.
  • Functional dependency: column A determines column B if each value of A always goes with one value of B. For example, product_id determines price. It is written A → B.
  • Composite key: a primary key made of more than one column, such as (order_id, product_id).

First normal form (1NF)

The rule: every column holds a single, indivisible value, there are no repeating groups, and each row can be uniquely identified.

Our products column breaks this, because it packs several items into one cell. To fix it, give every product its own row:

order_id product_id customer city pincode product price qty
1 100 Asha Pune 411001 Keyboard 1500 1
1 101 Asha Pune 411001 Mouse 500 2
2 102 Ben Delhi 110001 Monitor 9000 1

Now (order_id, product_id) identifies each row. Queries are possible, but the duplication is worse than before, as Asha’s details now repeat for each product. That is normal at this stage, and the next forms deal with it. Avoid the tempting shortcut of adding product1, product2 and product3 columns, since that is still a repeating group.

Second normal form (2NF)

The rule: the table is in 1NF, and every non-key column depends on the whole primary key, not just part of it.

Look at the columns against our key (order_id, product_id):

  • qty depends on both. Quantity only makes sense for one product in one order.
  • customer, city, pincode and order_date depend only on order_id.
  • product and price depend only on product_id.

Those last two groups are partial dependencies. Move each one to its own table:

orders      (order_id PK, customer, city, pincode, order_date)
products    (product_id PK, product, price)
order_items (order_id, product_id, qty)  -- PK is both columns

A price now lives in one place, and you can add a product before anyone buys it. A table with a single-column primary key cannot have a partial dependency, so if its 1NF is sound, it is already in 2NF.

Third normal form (3NF)

The rule: the table is in 2NF, and no non-key column depends on another non-key column. Such a chain is called a transitive dependency.

Check our orders table. order_id determines the customer, the customer determines their city, and the city determines the pincode. Those facts are not really about the order, so we move them out:

orders    (order_id PK, customer_id FK, order_date)
customers (customer_id PK, name, city_id FK)
cities    (city_id PK, city, pincode)

A handy way to remember 3NF: every non-key column must depend on “the key, the whole key, and nothing but the key”.

Tip: Be practical with this. Real cities have many pincodes, so a production design would probably store the pincode with the customer’s address. Good normalization follows the true rules of your business, and not just the patterns in today’s sample data.

Boyce-Codd normal form (BCNF)

BCNF is a slightly stricter version of 3NF: for every functional dependency A → B, A must be a candidate key. Most 3NF tables already satisfy it. The exception usually appears when a table has overlapping candidate keys.

For example, consider enrollments (student, course, teacher). If each teacher teaches only one course, then teacher → course, yet teacher alone is not a key. To reach BCNF, split it into teacher_courses (teacher, course) and student_teachers (student, teacher). Higher forms, 4NF and 5NF, deal with rarer multi-valued cases, and most developers seldom need them day to day.

The final schema

Here is the result as real SQL, with foreign keys linking the tables:

CREATE TABLE cities (
  city_id INT PRIMARY KEY, city TEXT, pincode TEXT
);
CREATE TABLE customers (
  customer_id INT PRIMARY KEY, name TEXT,
  city_id INT REFERENCES cities(city_id)
);
CREATE TABLE products (
  product_id INT PRIMARY KEY, product TEXT, price INT
);
CREATE TABLE orders (
  order_id INT PRIMARY KEY, order_date TEXT,
  customer_id INT REFERENCES customers(customer_id)
);
CREATE TABLE order_items (
  order_id INT REFERENCES orders(order_id),
  product_id INT REFERENCES products(product_id),
  qty INT,
  PRIMARY KEY (order_id, product_id)
);

Nothing was lost. A join rebuilds the original view whenever you need it:

SELECT o.order_id, c.name, ci.city, p.product, oi.qty,
       oi.qty * p.price AS line_total
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
JOIN cities ci ON ci.city_id = c.city_id
JOIN order_items oi ON oi.order_id = o.order_id
JOIN products p ON p.product_id = oi.product_id
ORDER BY o.order_id, p.product;

-- 1 | Asha | Pune  | Keyboard | 1 | 1500
-- 1 | Asha | Pune  | Mouse    | 2 | 1000
-- 2 | Ben  | Delhi | Monitor  | 1 | 9000

If moving a customer used to mean editing many rows, it now means changing one value in one row. If those joins look unfamiliar, see SQL joins explained.

Normal forms at a glance

Form Rule What it removes
1NF Single values, unique rows Lists in cells and repeating groups
2NF 1NF, and depends on the whole key Partial dependencies
3NF 2NF, and depends only on the key Transitive dependencies
BCNF Every determinant is a candidate key Remaining key-related anomalies

When to denormalize

Normalization favours correctness and clean writes, but reads may need many joins. Denormalization means deliberately adding some redundancy back for speed. It is reasonable when:

  • Reporting and analytics tables are read heavily and updated in batches, as in star schemas.
  • You have measured a slow query, and better indexes did not fix it.
  • You need a historical snapshot rather than the current value.

That last point catches people out. Our order_items table reads the current product price, so raising a price tomorrow would change the value of old orders. Real systems store unit_price on each order line. That is not a design flaw, because the price at the time of sale is a separate fact from today’s price.

Normalize first, then denormalize only for a known reason, and plan how the copies will stay in sync. A migration is also easier to get right with good tests, so include checks for your schema in your regression testing checklist.

Final thoughts

You do not need to memorize formal definitions to design good tables. Ask of each column: “is this a fact about this table’s key, and only this key?” If not, it belongs elsewhere. Try this on the next table you create, and aim for 3NF unless you have a clear reason to do otherwise.

Frequently asked questions

What is normalization in a database, in simple words?

It means organizing data into related tables so that each fact is stored once. This reduces duplication and stops the same information from becoming inconsistent.

What is the difference between 2NF and 3NF?

2NF removes columns that depend on only part of a composite key. 3NF removes columns that depend on another non-key column rather than directly on the key.

Is 3NF enough?

For most business applications, yes. BCNF is worth checking when a table has overlapping candidate keys, while higher forms address rarer situations.

Does normalization make queries slower?

It can add joins, though with good indexes they are usually fast. Normalized tables can also make writes cheaper, because each value is updated in one place.

Does normalization apply to NoSQL databases?

Not in the same way. Document databases often embed related data for faster reads, but the trade-offs around duplicated data and updates still apply.

Keep learning

Work with Stack Quora

Need help with testing, WordPress or SEO?

We help teams ship better software and grow their sites. Tell us what you are working on and we will suggest a clear next step.

Keep reading

More guides on testing, AI & modern development

Enjoyed this article? Pick a topic and jump straight into our newest tutorials.

Artificial Intelligence

LLM Temperature and Top-p Explained: How AI Models Choose Their Next Word

LLM temperature and top-p explained with a simulation we ran: how probabilities are reshaped, what top-p keeps, recommended settings by…

Read article →
DevOps

YAML Explained: Syntax, the Norway Problem and Other Traps We Tested

YAML explained with a parser test: basic syntax, lists and maps, the Norway problem, versions turning into floats, octal zip…

Read article →
DevOps

Cron Jobs Explained: Syntax, Real Schedules and the Mistakes That Break Them

Cron jobs explained with schedules we verified: the five fields, operators, real examples, the day-of-month OR day-of-week trap, environment problems,…

Read article →
Software Testing

Bug Life Cycle in Software Testing: Every Status, Who Owns It and Common Mistakes

The bug life cycle explained: every defect status from New to Closed, who owns each step, rejected, deferred and reopened…

Read article →
Test Automation

Property-Based Testing in Python: Hypothesis Finds the Bugs Your Examples Miss

Property-based testing explained with Hypothesis tests we ran: properties vs examples, strategies, shrinking, the real bugs it found in our…

Read article →
Databases & SQL

SQL Pagination Explained: OFFSET vs Keyset, With a Million-Row Test

SQL pagination explained with a million-row test we ran: LIMIT and OFFSET, why deep pages get slow, keyset (cursor) pagination,…

Read article →
Databases & SQL

SQL EXISTS vs IN: Differences, the NOT IN NULL Trap and Performance

SQL EXISTS vs IN explained with queries we ran: how each works, why NOT IN returns no rows when a…

Read article →
Software Development

Python Logging Explained: Levels, Handlers, Formatters and Mistakes to Avoid

Python logging explained with code we ran: levels, basicConfig quirks, named loggers, handlers and formatters, logger.exception, lazy formatting, duplicate logs…

Read article →
Software Development

JavaScript Map vs Object: Key Differences, Pitfalls and a Benchmark

JavaScript Map vs Object explained with code we ran in Node.js: key types, ordering, the prototype trap, JSON, size, WeakMap…

Read article →
Software Development

Python Type Hints Explained: Annotations, mypy and the Errors It Catches

Python type hints explained with a real mypy run: basic annotations, Optional and unions, TypedDict, Literal, Protocol, generics, Callable, strict…

Read article →
Artificial Intelligence

LLM Tokens Explained: How AI Models Read Text, Count Costs and Fill Context Windows

LLM tokens explained with a tokenizer we ran: how text is split, why counts differ by language and tokenizer, context…

Read article →
DevOps

Semantic Versioning Explained: MAJOR.MINOR.PATCH, Ranges and Release Rules

Semantic versioning explained with code we ran: MAJOR.MINOR.PATCH rules, pre-releases, correct version sorting, caret and tilde ranges, 0.x versions, lockfiles…

Read article →
DevOps

Git Reset vs Revert: How to Undo Commits Safely, With a Tested Example

Git reset vs revert explained with a repository we built: soft, mixed and hard reset, revert commits, reverting merges, recovering…

Read article →
Software Testing

The Test Pyramid Explained: Layers, Ratios and When to Break the Shape

The test pyramid explained: unit, integration and end-to-end layers, sensible ratios, the ice cream cone anti-pattern, the testing trophy and…

Read article →
Test Automation

pytest Fixtures Explained: Scope, yield Teardown, conftest.py and Factories

pytest fixtures explained with a suite we ran: how fixtures work, scopes, yield teardown, conftest.py, factory fixtures, parametrized fixtures, built-ins…

Read article →
Databases & SQL

SQL Upsert Explained: INSERT ON CONFLICT, MERGE and the REPLACE Trap

SQL upsert explained with queries we ran: INSERT ON CONFLICT DO UPDATE and DO NOTHING, the excluded row, conditional updates,…

Read article →
Databases & SQL

SQL LIKE Operator Explained: Wildcards, Case Sensitivity and Performance

The SQL LIKE operator explained with queries we ran: % and _ wildcards, escaping, NOT LIKE and NULL, case sensitivity…

Read article →
Software Development

Python Dataclasses Explained: Less Boilerplate, Safer Defaults and Real Examples

Python dataclasses explained with code we ran: generated methods, default_factory, frozen and ordered classes, __post_init__, slots, kw_only and when to…

Read article →
Software Development

JavaScript Destructuring Explained: Objects, Arrays, Defaults and Common Errors

JavaScript destructuring explained with code we ran in Node.js: objects, arrays, renaming, defaults, nested patterns, rest, function parameters and the…

Read article →
Software Development

Python f-strings Explained: Formatting Numbers, Dates and Debug Output

Python f-strings explained with code we ran: format specifiers for numbers, alignment and dates, the = debug shortcut, rounding surprises,…

Read article →
Artificial Intelligence

AI Hallucinations Explained: Why Language Models Make Things Up and How to Reduce It

AI hallucinations explained: what they are, why language models produce them, common types, how to reduce them with grounding and…

Read article →
DevOps

Environment Variables Explained: .env Files, Secrets and Config Done Right

Environment variables explained with Python examples we ran: reading and setting them, why every value is a string, .env files,…

Read article →
Test Automation

Mocking in Python: unittest.mock, patch and the Mistakes That Make Tests Lie

Mocking in Python with unittest.mock, tested: Mock and return_value, side_effect, patching where a name is used, spec and autospec, mocking…

Read article →
Software Testing

Code Coverage Explained: Line vs Branch Coverage and What 100% Really Means

Code coverage explained with a real coverage.py run: line, branch and condition coverage, reading a report, why 100% coverage can…

Read article →
Databases & SQL

DELETE vs TRUNCATE vs DROP in SQL: Differences, Rollback and When to Use Each

DELETE vs TRUNCATE vs DROP explained with tested SQL: what each removes, rollback, triggers, identity reset, speed, and how PostgreSQL,…

Read article →
Databases & SQL

SQL Views Explained: Create, Use and Avoid the Pitfalls, With Examples

SQL views explained with queries we ran: creating a view, live results, hiding columns, performance, updatable and materialized views, broken…

Read article →
Artificial Intelligence

What Are AI Agents? How They Work, With a Simple Agent Loop You Can Run

What are AI agents? A plain guide to the agent loop, tools, memory and guardrails, with a small Python loop…

Read article →
DevOps

Monitoring vs Observability: The Real Difference, With Logs, Metrics and Traces

Monitoring vs observability explained: known and unknown failures, logs, metrics and traces, a checkout incident walkthrough, golden signals, OpenTelemetry and…

Read article →
Test Automation

TDD vs BDD: Key Differences, One Feature Built Both Ways and When to Use Each

TDD vs BDD explained with one discount feature built both ways: red-green-refactor with code we ran, Given-When-Then scenarios, tools, trade-offs…

Read article →
Software Testing

User Acceptance Testing (UAT): Process, Checklist and Real Examples

User acceptance testing explained: who runs it, how it differs from QA, a seven-step process, a UAT test case template,…

Read article →
Artificial Intelligence

What Are Embeddings? Vectors and Cosine Similarity Explained Simply

What are embeddings? A plain-language guide to vectors, cosine similarity and semantic search, with a small example we ran, common…

Read article →
Test Automation

Flaky Tests: What Causes Them and How to Fix Them for Good

Flaky tests explained with demos we ran: the main causes, how to confirm and diagnose one, proper fixes for timing,…

Read article →
Software Testing

Load Testing vs Stress Testing: Differences, Metrics and k6 Examples

Load testing vs stress testing explained: what each proves, spike and soak tests, the metrics that matter, why averages mislead,…

Read article →
Software Testing

Test Case vs Test Scenario: Key Differences, Examples and When to Use Each

Test case vs test scenario explained: clear definitions, a side-by-side table, one password reset feature taken from scenarios to cases,…

Read article →
Artificial Intelligence

What Is a Large Language Model? How LLMs Work, Explained Without the Hype

What is a large language model? LLMs explained in plain language: tokens, embeddings, attention, training, temperature and context windows, a…

Read article →
Test Automation

Selenium vs Playwright: Key Differences, Code Compared and Which to Choose

Selenium vs Playwright compared: architecture, waiting, browser and language support, debugging, the same test in both tools, migration advice and…

Read article →