Articles

What Is Database Normalization? A Practical Guide

Database normalization organizes tables to eliminate redundant data and update anomalies. The normal forms explained with a worked example.

The Lycoris Team The Lycoris Team · · 4 min read
Rows of library card catalog drawers

Database normalization is the process of structuring relational tables so that each piece of data is stored in exactly one place, reducing redundancy and preventing the update anomalies that come with duplicating the same fact across multiple rows. It’s organized as a series of “normal forms,” each building on the last, that define progressively stricter rules about what a well-structured table looks like.

The problem normalization solves

Imagine a single table that stores orders, with columns for the order details plus the customer’s name, email, and shipping address repeated on every row for every order that customer places. That design has three concrete problems:

  • Update anomaly — if a customer changes their email, you have to update every order row for that customer, and missing one leaves the data inconsistent.
  • Insertion anomaly — you can’t record a new customer’s information until they place an order, because customer data only exists attached to order rows.
  • Deletion anomaly — deleting a customer’s only order deletes their contact information entirely, since it wasn’t stored anywhere else.

Normalization fixes this by splitting data into separate tables connected by keys, so each fact — a customer’s email, a product’s price — lives in exactly one row, referenced rather than repeated.

The normal forms

First normal form (1NF) requires that each column hold a single, atomic value — no comma-separated lists of phone numbers crammed into one field — and that each row be uniquely identifiable, typically by a primary key.

Second normal form (2NF) builds on 1NF by requiring that every non-key column depend on the entire primary key, not just part of it. This only matters for tables with a composite primary key (made of multiple columns); if an order-items table is keyed on (order_id, product_id) but stores a product_name that only depends on product_id, that’s a 2NF violation — the product name belongs in a separate products table.

Third normal form (3NF) goes further: every non-key column must depend on the primary key and nothing but the primary key — no non-key column should determine another non-key column. If a table stores both zip_code and city, and city is fully determined by zip code, that’s a transitive dependency that violates 3NF; city belongs in a separate lookup keyed by zip code.

Most application schemas stop at 3NF, which eliminates the great majority of practical redundancy. Higher normal forms (BCNF, 4NF, 5NF) exist and matter for specific edge cases, but they’re rarely a day-to-day concern outside of formal database design work.

A worked example

A denormalized orders table might look like this, with customer and product details repeated on every row:

order_id | customer_name | customer_email | product_name | product_price | quantity

Normalized to 3NF, that becomes three related tables:

customers: customer_id | name | email
products:  product_id  | name | price
orders:    order_id | customer_id | product_id | quantity

Each customer’s email now lives in exactly one row. Changing it means updating one record, not hunting down every order that customer ever placed. This is a direct application of the relational model that SQL and relational databases like PostgreSQL are built around — tables connected by foreign keys instead of flattened into one wide sheet.

When to denormalize

Normalization optimizes for data integrity, not read speed. A fully normalized schema often requires joining several tables to answer a common query, and joins have a real cost at scale. It’s common, once a system is under real load, to deliberately denormalize specific tables — duplicating a customer’s name onto the orders table, for instance — to avoid a join on a query that runs constantly.

This is a tradeoff, not a mistake: you’re accepting some redundancy and the risk of update anomalies in exchange for faster reads. It’s usually paired with other performance tools rather than used blindly — a well-placed database index often closes the performance gap without giving up normalization at all, and should generally be tried first. Denormalization becomes more attractive as a system scales further, alongside strategies like database replication for read-heavy workloads.

Normalization outside the relational model

Normalization is a relational-database concept — it assumes a schema of tables and foreign keys. Document-oriented and other NoSQL databases often invert the tradeoff by design, embedding related data directly inside a document specifically to avoid joins, accepting redundancy from the start in exchange for reads that touch only one record. Neither approach is universally correct; it depends on whether your workload is write-heavy and consistency-sensitive, or read-heavy and latency-sensitive.

The takeaway

Normalization organizes a relational schema so each fact lives in one place, preventing the update, insertion, and deletion anomalies that come from duplicating data across rows. First, second, and third normal form cover the practical cases most schemas need, and 3NF is a reasonable default to design toward. Denormalize deliberately, and only after indexing hasn’t been enough, when read performance on a specific query matters more than avoiding redundancy.

The Lycoris Team The Lycoris Team · · 5 min read

What Is a Database Trigger?

A database trigger is a procedure that runs automatically on an insert, update, or delete — enforcing rules the application layer can't guarantee.

#Databases #SQL #Backend
The Lycoris Team The Lycoris Team · · 4 min read

Primary Key vs Foreign Key vs Unique Constraint

Primary keys identify a row, foreign keys link one table to another, and unique constraints just prevent duplicates. How the three differ in SQL.

#Databases #SQL #Backend