•CASE STUDY

Fixing a Flawed Database Table (Normalization and Indexes)

4 min read·783 words·Beginner

Asked at

1 candidate report in Jun 2026

How to use this case study

SDE-2 / Mid

  • Spot redundancy and anomalies in a table
  • Normalize it into proper tables with primary and foreign keys

SDE-3 / Senior

  • Explain the normal forms (1NF, 2NF, 3NF) in simple terms
  • Add indexes for the required lookups
  • Discuss when to denormalize for speed

Staff / Principal

  • Plan a safe migration from the old table to the new schema with no downtime
  • Verify the data

Problem RestatementProblem

Goldman Sachs asked: here's a table with design flaws. Make it less redundant and faster to query. You're expected to know normalization (organizing tables to remove repeated data) and indexes.

Example flawed table (orders_flat):
order_idcustomer_namecustomer_emailcustomer_cityproduct_namesproduct_pricesorder_date
1Asha Raoasha@x.comPunePen, Book10, 2002026-09-01
2Asha Raoasha@x.comPuneLamp5002026-09-03

Problems:

  • Repeated customer data in every order: if Asha changes her email, many rows must change (an update anomaly), and one missed row means inconsistent data.
  • Lists inside a cell ("Pen, Book"): can't query "all orders containing Book" efficiently, and prices are separated from products. This breaks first normal form.
  • No keys or constraints: duplicates and bad data are possible.
  • No indexes for common lookups, such as orders by customer or by date.

Normal Forms in Plain Words

  • 1NF: one value per cell, no lists. Each row is unique (it has a primary key).
  • 2NF: every non-key column depends on the whole key (matters for composite keys).
  • 3NF: non-key columns depend only on the key, not on other non-key columns (e.g., customer_city depends on the customer, not the order).

The Fixed Design

CREATE TABLE customers (
  customer_id BIGINT PRIMARY KEY,
  name        TEXT NOT NULL,
  email       TEXT NOT NULL UNIQUE,
  city        TEXT
);
CREATE TABLE products (
  product_id  BIGINT PRIMARY KEY,
  name        TEXT NOT NULL,
  price_cents INT NOT NULL CHECK (price_cents >= 0)
);
CREATE TABLE orders (
  order_id    BIGINT PRIMARY KEY,
  customer_id BIGINT NOT NULL REFERENCES customers(customer_id),
  order_date  DATE NOT NULL
);
CREATE TABLE order_items (
  order_id    BIGINT REFERENCES orders(order_id),
  product_id  BIGINT REFERENCES products(product_id),
  quantity    INT NOT NULL CHECK (quantity > 0),
  unit_price_cents INT NOT NULL,          -- price at the time of order (history must not change)
  PRIMARY KEY (order_id, product_id)
);
Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
    CU["customers"] -->|"1 to many"| OR["orders"]
    OR -->|"1 to many"| OI["order_items"]
    PR["products"] -->|"1 to many"| OI

Indexes for Faster Lookups

Add indexes for the queries the business actually runs:

  • Orders for a customer, newest first → CREATE INDEX ON orders (customer_id, order_date DESC);
  • Orders in a date range → CREATE INDEX ON orders (order_date);
  • Which orders contain a product → the PK (order_id, product_id) doesn't help here, so add CREATE INDEX ON order_items (product_id);
  • Lookup by email → already indexed by the UNIQUE constraint.
Primary keys and foreign keys prevent duplicates and orphans. An index on foreign key columns also speeds up joins and deletes.

Check each important query with EXPLAIN to confirm it uses the index.

Deep Dive — How far to normaliseDeep dive

The flawed table repeats customer names and product prices on every row. The instinct is to normalise everything; the instinct after a slow report is to denormalise everything. Both are wrong on their own.

Weak

Leave it wide, because joins are slow

Keep customer name, address, product name and price on the order line.

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
  ROW["order_lines: customer_name, address, product_name, price"] --> UPD["Customer changes address"]
  UPD --> MANY["Update thousands of rows - some get missed"]
  MANY --> DIS["The same customer now has two addresses in the table"]
  ROW --> ASK["Which one is correct?"]
  ASK --> NONE["No source of truth - the table cannot answer"]

Redundancy is not primarily a storage problem, it is a truth problem: once the same fact lives in many rows, they disagree, and nothing in the schema says which is right.

Good

Normalise it fully

Customers, products, orders and order lines, each fact in one place, joined on read.

Correct, and the right default. Two things are still unhandled. Reporting queries now join four tables over millions of rows. And full normalisation would store only product_id on the order line — which loses the price the customer actually paid when the product's price later changes.

Best

One source of truth, plus deliberate derived copies

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
  SRC["Normalised tables - the source of truth"] --> SNAP["order_lines.unit_price_cents - the price at purchase"]
  SNAP --> HIST["Historical, not redundant - a different fact from today's price"]
  SRC --> TOT["orders.total_cents - maintained in the same transaction"]
  SRC --> MV["Materialised view for reports - refreshed on a schedule"]
  MV --> FAST["Report queries hit one table"]
  TOT --> RULE["Rule: every copy is derived, and derived in one place"]

Two kinds of "redundancy" that are not the same thing:

  • A price snapshot on the order line is not redundancy. "What the customer paid" and "what the product costs now" are different facts, and the order line is the only place the first one lives. Normalising it away is a bug, not a purification.
  • A maintained total or a materialised view is controlled redundancy. It is a copy, kept in sync in the same transaction or rebuilt on a schedule, and it exists because a measured query was too slow.

The rule to state: one source of truth, and every copy is derived from it in exactly one place. Denormalise in response to a measured problem, never in anticipation — and when you do, write down what maintains the copy, because an unmaintained derived column becomes the flawed table you were asked to fix.

Migrating Safely (bonus)

  1. Create the new tables.
  2. Backfill from orders_flat (split the lists, deduplicate customers by email).
  3. Dual-write new orders to both old and new tables, then verify counts and totals.
  4. Switch reads to the new tables, and remove the old table later.

Wrap-UpWrap-up

Split the flat table into customers, products, orders and order_items (one value per cell, each fact stored once, linked by primary and foreign keys with constraints), keep the historical unit price on order_items, and add indexes matching the real queries (customer + date, date, product). Denormalize only deliberately for reporting, and migrate with backfill, dual writes and verification.

More Case Studies

Frequently Asked Questions

What is the Fixing a Flawed Database Table (Normalization and Indexes) system design question?

Fixing a Flawed Database Table (Normalization and Indexes) is a system design interview question asked at FAANG companies. It covers databases and tests your ability to design scalable, production-ready systems. InterviewSkool's breakdown walks you through requirements, API design, architecture, and trade-offs.

Which companies ask the Fixing a Flawed Database Table (Normalization and Indexes) question?

Goldman Sachs have reportedly asked variations of this question in system design interviews. The exact wording may differ, but the core design challenges remain the same.

How should I prepare for the Fixing a Flawed Database Table (Normalization and Indexes) interview question?

Start with the problem statement and scale estimates, then design the high-level architecture. Focus on the core components, data model, and API design. InterviewSkool's breakdown covers the full solution with mermaid diagrams and trade-off analysis to help you prep efficiently.

What level is the Fixing a Flawed Database Table (Normalization and Indexes) question?

This question is suitable for SDE-2, SDE-3, and Staff engineer interviews. The level guidance on this page provides specific tips for each level — SDE-2 candidates should focus on core architecture, while Staff engineers should discuss trade-offs, monitoring, and incremental rollouts.

Practice with a Mock Interview

Apply what you learned in a live system design mock interview with InterviewSkool's AI interviewer.

Start System Design Interview →