Back to Blog
Lesson 15 of the PostgreSQL: SQL & PostgreSQL from Scratch course
DatabasesAugust 2, 20264 min read

Normalizing the Store Schema: A Beginner’s Guide to 1NF & 2NF

Learn how to apply normalization principles to your PostgreSQL schema. Eliminate redundancy and improve data integrity by splitting orders into headers and items.

PostgreSQLSQLNormalizationDatabase DesignSchema Optimization
Creative flat lay of gadgets and 'Beginners Guide' text on wooden table.

Previously in this course, we covered implementing foreign keys to link tables and using CHECK constraints to enforce business rules. In this lesson, we take those concepts further by applying normalization to our store schema.

Normalization is the process of organizing a database to reduce data redundancy and improve data integrity. Without it, your tables become cluttered, prone to update anomalies, and inefficient to query.

What is Normalization?

Normalization is a series of "forms" (rules). For a beginner, the most critical are the first two:

  1. First Normal Form (1NF): Eliminate repeating groups and ensure each column contains atomic values.
  2. Second Normal Form (2NF): Ensure all non-key columns depend on the entire primary key.

Think of it as tidying up your digital workspace. By separating concerns, we ensure that a change in one piece of data doesn't require us to update multiple rows in different places.

Applying 1NF: Atomic Values

A table is in 1NF if every cell contains only one value and there are no "repeating groups."

Imagine an orders table where you store items as a comma-separated string: product_ids = '101, 105, 202'. This is a classic 1NF violation. You cannot easily join this to a products table or calculate the sum of prices. To achieve 1NF, you must break these out into individual rows.

Applying 2NF: Separating Order Headers from Items

2NF requires that all data in a table relates directly to the primary key. If you have an orders table that includes customer_name, order_date, product_id, and quantity, you have a problem.

If a customer buys three different products, you end up repeating the customer_name and order_date three times. This is redundant and dangerous—what if you mistype the name on one of those rows?

We solve this by splitting the data into two tables:

  • orders (The Header): Stores information about the transaction itself (who, when).
  • order_items (The Details): Stores the line items belonging to that order (what, how many, at what price).

Worked Example: Refactoring the Schema

Let’s transform our draft schema into a normalized one.

SQL
-- 1. The Header Table
CREATE TABLE orders (
    order_id SERIAL PRIMARY KEY,
    customer_id INT REFERENCES customers(customer_id),
    order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 2. The Items Table
CREATE TABLE order_items (
    item_id SERIAL PRIMARY KEY,
    order_id INT REFERENCES orders(order_id),
    product_id INT REFERENCES products(product_id),
    quantity INT CHECK (quantity > 0),
    unit_price NUMERIC(10, 2)
);

By separating these, the orders table stays lean. If you need to know what was in an order, you join order_items using the order_id as we will explore in upcoming lessons.

Hands-on Exercise

To advance our running project, I want you to perform a mental (or scratchpad) refactor of a hypothetical shipping_addresses table.

If you currently have a customers table that includes shipping_address, shipping_city, and shipping_zip, consider this: what happens if a customer has multiple addresses?

  • Task: Sketch out how you would create a separate addresses table that links back to customers using a foreign key. How does this prevent you from having to copy-paste the address every time a customer places a new order?

Common Pitfalls

  1. Over-normalizing: Don't split tables so much that you need a 10-table join just to see a basic order summary. Normalization is a balance; see Data Modeling for Scalable Systems for more on when to stop.
  2. Forgetting Data Types: When splitting tables, ensure your Foreign Key column (e.g., order_id in order_items) matches the data type of the Primary Key in the source table exactly.
  3. Ignoring Constraints: Normalization doesn't replace constraints. Always keep your NOT NULL and CHECK constraints on the new, smaller tables.

FAQ

Q: Does normalization make queries slower? A: Usually, it makes them faster because the database scans less redundant data. However, for massive datasets, you might occasionally "denormalize" (combine tables) for specific read-heavy reports.

Q: Is 3NF necessary for beginners? A: 3NF (removing transitive dependencies) is important, but focus on 1NF and 2NF first. Get comfortable with header-item structures before worrying about more complex dependencies.

Recap

Normalization isn't just theory; it’s the foundation of a robust database design. By applying 1NF (atomic values) and 2NF (separating order headers from line items), you've ensured that your store database is efficient, scalable, and easy to maintain. We’ve successfully moved from a flat, messy data structure to a professional, relational design.

Up next, we will learn how to populate these new tables using INSERT statements.

Similar Posts