Reporting databases are designed differently from application databases. This guide covers the data-warehouse SQL that data and ETL testers meet: slowly changing dimensions (how history is kept), OLTP vs OLAP, star and snowflake schemas, and subtotal queries with ROLLUP, CUBE and GROUPING SETS.
Slowly Changing Dimensions (SCD)
In data warehousing, dimension data (customers, products, employees) changes slowly over time.
For example: a customer moves city, an employee changes department, a product changes price.
The question is: HOW do you handle these changes in your data warehouse?
SCDs define the strategy — and all three types are asked in data engineering interviews.
Asked at: Amazon, Flipkart, Walmart Labs, Myntra, any company with a data warehouse.
SCD Type 1 — Overwrite (No History)
Type 1 is like using a whiteboard — when something changes, you just ERASE and WRITE the new value.
Old value is gone forever. No history kept.
Use when history does not matter: fixing a typo in a name, updating a phone number.
Before update:
| customer_id | name | city |
|---|---|---|
| 101 | Naveed Khan | Bangalore |
After SCD Type 1 update (customer moved to Hyderabad):
| customer_id | name | city |
|---|---|---|
| 101 | Naveed Khan | Hyderabad |
-- SCD Type 1: Simple UPDATE, no history
UPDATE dim_customers
SET city = 'Hyderabad',
updated_at = NOW()
WHERE customer_id = 101;
-- Pros: Simple, storage-efficient
-- Cons: Historical data permanently lost
-- Use: Correcting errors, non-analytical attributes (phone, email typos)
SCD Type 2 — Add New Row (Full History) ← Most Important
Type 2 is like keeping ALL old pages of a notebook instead of erasing.
When Naveed moves from Bangalore to Hyderabad, we add a NEW row.
The old row is marked as "expired". The new row is marked as "current".
Now we can query: "Where did Naveed live in 2023?" — history is preserved!
This is the most commonly used SCD type in real data warehouses.
SCD Type 2 Table Design:
| customer_id | name | city | effective_from | effective_to | is_current |
|---|---|---|---|---|---|
| 101 | Naveed Khan | Bangalore | 2022-01-01 | 2024-06-30 | 0 |
| 101 | Naveed Khan | Hyderabad | 2024-07-01 | 9999-12-31 | 1 |
-- SCD Type 2: Expire old record, insert new record
START TRANSACTION;
-- Step 1: Expire the current record
UPDATE dim_customers
SET effective_to = DATE_SUB(CURDATE(), INTERVAL 1 DAY),
is_current = FALSE
WHERE customer_id = 101 AND is_current = TRUE;
-- Step 2: Insert new current record
INSERT INTO dim_customers
(customer_id, name, city, effective_from, effective_to, is_current)
VALUES
(101, 'Naveed Khan', 'Hyderabad', CURDATE(), '9999-12-31', TRUE);
COMMIT;
-- Query: What city was customer 101 in on 2023-06-01?
SELECT city FROM dim_customers
WHERE customer_id = 101
AND '2023-06-01' BETWEEN effective_from AND effective_to;
-- Returns: Bangalore ✓
-- Query: Current city of all customers
SELECT customer_id, name, city FROM dim_customers WHERE is_current = TRUE;
-- Surrogate key approach (better for joins)
-- dim_customers: surrogate_key (PK), customer_id (natural key), ...
-- fact_orders references surrogate_key, not customer_id
-- This way the fact row always points to the correct dimension snapshot
SCD Type 3 — Add New Column (Limited History)
Type 3 keeps only the CURRENT and ONE PREVIOUS value in extra columns.
When Naveed moves, we save the old city in "prev_city" and update "city".
Simple but limited — can only track ONE change back, not full history.
SCD Type 3 Table:
| customer_id | name | current_city | prev_city | city_changed_at |
|---|---|---|---|---|
| 101 | Naveed Khan | Hyderabad | Bangalore | 2024-07-01 |
-- SCD Type 3: Shift current to previous, update current
UPDATE dim_customers
SET prev_city = current_city,
current_city = 'Hyderabad',
city_changed_at = CURDATE()
WHERE customer_id = 101;
-- Pros: Simple queries for before/after comparison
-- Cons: Only one historical value stored, more schema changes needed for each new attribute
| SCD Type | History Kept | Complexity | Storage | Best For |
|---|---|---|---|---|
| Type 1 (Overwrite) | None | Very simple | Minimal | Fixing errors, non-analytical attrs |
| Type 2 (New Row) | Full history | Medium | High (grows over time) | Most analytics — who did what when |
| Type 3 (New Column) | Previous only | Simple | Low | Before/after comparison only |
| Type 4 (History table) | Full history (separate) | Medium | Medium | Keeping main table lean + audit log |
| Type 6 (Hybrid 1+2+3) | Full + current flag + prev col | Complex | Highest | Enterprise DWH with all requirements |
Star Schema, Snowflake and Data Warehouse SQL
Every data analyst, analytics engineer, data engineer, and senior backend SQL interview.
Companies: Amazon, Flipkart, Walmart, Myntra, Meesho, Razorpay, any company with BI/analytics.
The interviewer wants to know you understand OLAP vs OLTP and can design for reporting.
OLTP vs OLAP
| Feature | OLTP (Operational) | OLAP (Analytical) |
|---|---|---|
| Purpose | Day-to-day transactions | Business intelligence & reporting |
| Query type | Simple, fast, CRUD | Complex, aggregated, read-heavy |
| Data size | GBs | TBs to PBs |
| Normalization | Highly normalised (3NF) | Denormalised (star/snowflake) |
| Design | Many tables, many JOINs | Few wide tables, fewer JOINs |
| Latency | Milliseconds | Seconds to minutes |
| Examples | MySQL, PostgreSQL (production DB) | Redshift, BigQuery, Snowflake, Hive |
| Update frequency | Continuous (per transaction) | Batch loads (daily/hourly ETL) |
Star Schema
A Star Schema looks like a star: one big FACT table in the middle, surrounded by smaller DIMENSION tables.
FACT table: stores measurable events/numbers — sales amounts, clicks, orders, revenue.
DIMENSION tables: describe the who/what/when/where — customers, products, dates, locations.
The fact table has foreign keys pointing to all dimension tables.
It is called "star" because the diagram looks like a star!
Star Schema: E-Commerce Sales Data Warehouse
| Table | Type | Columns | Purpose |
|---|---|---|---|
| fact_sales | FACT | sale_id, date_key, customer_key, product_key, store_key, quantity, revenue, discount | One row per sale — the core metrics |
| dim_date | DIMENSION | date_key, date, day, week, month, quarter, year, is_holiday, fiscal_year | All dates for time-series analysis |
| dim_customer | DIMENSION | customer_key, customer_id, name, city, state, tier, signup_date | Customer attributes (SCD Type 2) |
| dim_product | DIMENSION | product_key, product_id, name, category, sub_category, brand, price_band | Product catalogue attributes |
| dim_store | DIMENSION | store_key, store_id, name, city, region, store_type | Store/channel attributes |
-- Create Star Schema tables
CREATE TABLE dim_date (
date_key INT PRIMARY KEY, -- e.g., 20250115 (YYYYMMDD)
full_date DATE NOT NULL,
day_of_week VARCHAR(10),
week_number INT,
month_num INT,
month_name VARCHAR(10),
quarter INT,
year INT,
is_weekend BOOLEAN,
is_holiday BOOLEAN,
fiscal_year INT,
fiscal_qtr INT
);
CREATE TABLE fact_sales (
sale_id BIGINT PRIMARY KEY AUTO_INCREMENT,
date_key INT REFERENCES dim_date(date_key),
customer_key INT REFERENCES dim_customer(customer_key),
product_key INT REFERENCES dim_product(product_key),
store_key INT REFERENCES dim_store(store_key),
quantity INT NOT NULL,
unit_price DECIMAL(10,2),
discount_pct DECIMAL(5,2) DEFAULT 0,
revenue DECIMAL(12,2),
cost DECIMAL(12,2)
);
-- Typical Star Schema Query: Revenue by Category and Quarter
SELECT
d.year, d.quarter, p.category,
SUM(f.revenue) AS total_revenue,
SUM(f.quantity) AS units_sold,
COUNT(DISTINCT f.sale_id) AS transactions
FROM fact_sales f
JOIN dim_date d ON f.date_key = d.date_key
JOIN dim_product p ON f.product_key = p.product_key
WHERE d.year = 2025
GROUP BY d.year, d.quarter, p.category
ORDER BY d.quarter, total_revenue DESC;
Snowflake Schema
Snowflake Schema: dimension tables are further normalised into sub-dimensions.
Example: In Star Schema, dim_product has category directly.
In Snowflake Schema: dim_product has a category_key → dim_category (separate table).
Star Schema: Denormalised, fewer JOINs, faster queries, more storage, easier to understand.
Snowflake Schema: Normalised, more JOINs, saves storage, harder to query, better data integrity.
Most real data warehouses use Star Schema for performance.
| Feature | Star Schema | Snowflake Schema |
|---|---|---|
| Normalisation | Denormalised dimensions | Normalised dimensions (sub-tables) |
| Number of tables | Fewer | More |
| JOIN complexity | Simpler | More complex |
| Query performance | Faster (fewer JOINs) | Slower (more JOINs) |
| Storage | More (redundant data) | Less (normalised) |
| BI Tool Support | Better (simpler model) | Works but more config |
| Use case | Most analytical use cases | When storage is a major constraint |
Rollup, Cube, Grouping Sets
ROLLUP: Generates subtotals and a grand total along a hierarchy.
CUBE: Generates all possible combinations of subtotals.
GROUPING SETS: Specify exactly which grouping combinations you want.
All three are extensions of GROUP BY for multi-dimensional aggregation.
-- ROLLUP: subtotals by dept, then overall total
SELECT dept, city,
COUNT(*) AS emp_count,
SUM(salary) AS total_salary
FROM employees
GROUP BY ROLLUP(dept, city);
-- Produces:
-- Engineering | Bangalore | 10 | 850000
-- Engineering | Mumbai | 5 | 420000
-- Engineering | NULL | 15 | 1270000 ← dept subtotal
-- HR | Delhi | 8 | 480000
-- HR | NULL | 8 | 480000 ← dept subtotal
-- NULL | NULL | 23 | 1750000 ← grand total
-- GROUPING() function: detect which NULLs are from ROLLUP (not real NULLs)
SELECT
CASE GROUPING(dept) WHEN 1 THEN 'ALL DEPTS' ELSE dept END AS dept,
CASE GROUPING(city) WHEN 1 THEN 'ALL CITIES' ELSE city END AS city,
SUM(salary) AS total
FROM employees
GROUP BY ROLLUP(dept, city);
-- CUBE: ALL combinations of dept + city
SELECT dept, city, SUM(salary) AS total
FROM employees
GROUP BY CUBE(dept, city);
-- Produces: (dept,city), (dept,ALL), (ALL,city), (ALL,ALL)
-- GROUPING SETS: choose exactly which groupings you want
SELECT dept, city, month, SUM(salary)
FROM employees
GROUP BY GROUPING SETS (
(dept, city), -- group by dept and city
(dept), -- group by dept only
() -- grand total
);
-- More flexible than ROLLUP or CUBE
FAQs
What is SCD Type 2?
A slowly changing dimension that keeps full history: when an attribute changes, the current row is closed with an end date and a new current row is inserted.
What is the difference between a star and a snowflake schema?
In a star schema each dimension is one denormalised table joined directly to the fact table; a snowflake schema normalises dimensions into several related tables, saving space at the cost of more joins.
What does ROLLUP do in SQL?
It adds subtotal rows for each level of the GROUP BY columns plus a grand total; CUBE adds subtotals for every combination of the columns.