Data Modeling Interview Questions Explained
Data modelling interviews test design decisions, and real data forces them. Seven questions are answered by building a star schema from a year of real online retail orders: declaring the grain, where price belongs, messy product names, customers who changed country, unknown members, a date dimension and additivity.
GloryTecks Team
Training & Placement Faculty
Last updated: 8 October 2026
Data modelling interviews rarely ask you to recite the difference between a star and a snowflake schema. They give you a business process and ask you to design for it, and then ask what you would do when the data misbehaves. These data modeling interview questions are answered by building a star schema from a real year of online retail orders, because real data forces the design decisions that interviewers ask about.
The data is the UCI Online Retail set: every order line from a UK online gift retailer between December 2010 and December 2011 (UCI: Online Retail). The model is built in DuckDB: a sales fact table with product, customer and date dimensions.
import duckdb
db = duckdb.connect()
db.execute("INSTALL excel; LOAD excel")
db.execute("CREATE TABLE raw AS SELECT * FROM read_xlsx('Online Retail.xlsx', all_varchar = true)") # UCI Online Retail
db.execute("""CREATE TABLE lines AS SELECT InvoiceNo AS invoice, StockCode AS product, TRIM(Description) AS description,
CAST(Quantity AS INTEGER) AS qty, ROUND(CAST(UnitPrice AS DOUBLE), 2) AS price,
CAST(CAST(CustomerID AS DOUBLE) AS INTEGER) AS customer, Country AS country,
TIMESTAMP '1899-12-30' + to_seconds(CAST(ROUND(CAST(InvoiceDate AS DOUBLE) * 86400) AS BIGINT)) AS ts
FROM raw""")
def ask(title, sql):
print(f"-- {title}")
for row in db.execute(sql).fetchall():
print(" ", *row)
ask("D1 grain: one fact row per invoice line", "SELECT COUNT(*) AS lines, COUNT(DISTINCT invoice) AS invoices FROM lines")
ask("D2 does price belong to the product? prices recorded for product 85123A", """
SELECT COUNT(DISTINCT price), MIN(price), MAX(price), ANY_VALUE(description) FROM lines WHERE product = '85123A'""")
ask("D3 product codes with more than one description, and some of the extra ones", """
SELECT COUNT(*) FROM (SELECT product FROM lines WHERE description IS NOT NULL
GROUP BY product HAVING COUNT(DISTINCT description) > 1)""")
ask(" descriptions seen on stock code 23071", """
SELECT description, COUNT(*) AS lines, SUM(qty) AS units FROM lines WHERE product = '23071' GROUP BY description ORDER BY lines DESC""")
db.execute("""CREATE TABLE dim_product AS
SELECT ROW_NUMBER() OVER (ORDER BY product) AS product_key, product AS stock_code, description
FROM (SELECT product, description, ROW_NUMBER() OVER (PARTITION BY product ORDER BY COUNT(*) DESC, description) AS n
FROM lines WHERE description IS NOT NULL AND price > 0 GROUP BY product, description)
WHERE n = 1
UNION ALL SELECT 0, NULL, 'Unknown product'""")
ask(" dim_product keeps the most used priced description per code", "SELECT COUNT(*) - 1, ANY_VALUE(description) FILTER (WHERE stock_code = '23071') FROM dim_product")
ask("D4 customers recorded under more than one country", """
SELECT COUNT(*) FROM (SELECT customer FROM lines WHERE customer IS NOT NULL GROUP BY customer HAVING COUNT(DISTINCT country) > 1)""")
db.execute("""CREATE TABLE dim_customer AS
WITH visits AS (SELECT customer, country, MIN(ts) AS first_seen FROM lines WHERE customer IS NOT NULL GROUP BY customer, invoice, country),
changes AS (SELECT *, LAG(country) OVER (PARTITION BY customer ORDER BY first_seen) AS previous FROM visits),
versions AS (SELECT customer, country, first_seen AS valid_from FROM changes WHERE previous IS NULL OR previous <> country)
SELECT ROW_NUMBER() OVER (ORDER BY customer, valid_from) AS customer_key, customer, country, valid_from,
LEAD(valid_from) OVER (PARTITION BY customer ORDER BY valid_from) AS valid_to
FROM versions
UNION ALL SELECT 0, NULL, 'Unknown', TIMESTAMP '1900-01-01', NULL""")
ask(" dim_customer as type 2: customers, rows, rows for customer 12417", """
SELECT COUNT(DISTINCT customer), COUNT(*), (SELECT COUNT(*) FROM dim_customer WHERE customer = 12417) FROM dim_customer""")
ask(" customer 12417's versions", "SELECT customer_key, country, valid_from, valid_to FROM dim_customer WHERE customer = 12417 ORDER BY valid_from")
db.execute("""CREATE TABLE fact_sales AS
SELECT l.invoice, CAST(l.ts AS DATE) AS date_key, COALESCE(c.customer_key, 0) AS customer_key,
COALESCE(p.product_key, 0) AS product_key, l.qty, l.price, l.qty * l.price AS revenue
FROM lines l
LEFT JOIN dim_product p ON p.stock_code = l.product
LEFT JOIN dim_customer c ON c.customer = l.customer AND l.ts >= c.valid_from AND (c.valid_to IS NULL OR l.ts < c.valid_to)""")
ask("D5 unknown members: lines without a customer or product, and revenue kept vs lost", """
SELECT (SELECT COUNT(*) FROM fact_sales WHERE customer_key = 0) AS unknown_customer_lines,
(SELECT COUNT(*) FROM fact_sales WHERE product_key = 0) AS unknown_product_lines,
ROUND((SELECT SUM(revenue) FROM fact_sales f JOIN dim_customer c USING (customer_key))) AS revenue_with_unknown_member,
ROUND((SELECT SUM(qty * price) FROM lines l JOIN (SELECT DISTINCT customer FROM dim_customer) c USING (customer))) AS revenue_joining_on_null_ids""")
db.execute("""CREATE TABLE dim_date AS
SELECT CAST(d AS DATE) AS date_key, DAYNAME(d) AS weekday, MONTH(d) AS month
FROM range(DATE '2010-12-01', DATE '2011-12-10', INTERVAL 1 DAY) t(d)""")
ask("D6 a date dimension shows the days with no sales at all", """
SELECT d.weekday, COUNT(*) AS days, COUNT(*) FILTER (WHERE f.date_key IS NULL) AS days_without_sales
FROM dim_date d LEFT JOIN (SELECT DISTINCT date_key FROM fact_sales) f USING (date_key)
GROUP BY d.weekday ORDER BY days_without_sales DESC, d.weekday LIMIT 3""")
ask("D7 distinct customers are not additive", """
SELECT (SELECT SUM(n) FROM (SELECT COUNT(DISTINCT customer_key) AS n FROM fact_sales
WHERE customer_key <> 0 GROUP BY MONTH(date_key), YEAR(date_key))) AS sum_of_monthly_counts,
(SELECT COUNT(DISTINCT customer) FROM dim_customer WHERE customer IS NOT NULL) AS customers_in_the_year""")-- D1 grain: one fact row per invoice line
541909 25900
-- D2 does price belong to the product? prices recorded for product 85123A
8 0.0 5.91 WHITE HANGING HEART T-LIGHT HOLDER
-- D3 product codes with more than one description, and some of the extra ones
647
-- descriptions seen on stock code 23071
MARIE ANTOINETTE TRINKET BOX GOLD 27 242
damages 1 -65
MARIE ANTOIENETT TRINKET BOX GOLD 1 1
-- dim_product keeps the most used priced description per code
3937 MARIE ANTOINETTE TRINKET BOX GOLD
-- D4 customers recorded under more than one country
8
-- dim_customer as type 2: customers, rows, rows for customer 12417
4372 4389 3
-- customer 12417's versions
60 Belgium 2010-12-17 11:51:00 2011-04-28 13:09:00
61 Spain 2011-04-28 13:09:00 2011-06-05 15:46:00
62 Belgium 2011-06-05 15:46:00 None
-- D5 unknown members: lines without a customer or product, and revenue kept vs lost
135080 138 9747748.0 8300066.0
-- D6 a date dimension shows the days with no sales at all
Saturday 53 53
Monday 53 6
Friday 54 4
-- D7 distinct customers are not additive
13680 4372D1. What is the first decision in a dimensional design?
Declaring the grain: exactly what one fact row represents (Kimball: grain). Here it is one row per invoice line, 541,909 rows from 25,900 invoices. The lowest grain the source offers is the safest choice, because invoice and daily totals can always be rolled up from lines, but lines can never be recovered from totals. The invoice number stays in the fact table as a degenerate dimension: it groups lines, but has no attributes of its own to put in a dimension table (Kimball: degenerate dimensions).
D2. Does price belong in the fact table or the product dimension?
Let the data decide. One product, the white hanging heart T-light holder, was sold at 8 different prices, from 0 to 5.91. A price that varies by line is a measurement of the sale, so it belongs in the fact table. A list price could still live in the product dimension, but then it is a separate attribute with a separate meaning, and the model should name it as such.
D3. What do you do with a messy dimension attribute?
647 product codes have more than one description. Stock code 23071 shows why: its proper name appeared on 27 lines, a misspelling on one, and the word "damages" on a line that removed 65 units. The description field doubles as a note field for stock adjustments. The product dimension therefore keeps one description per code, the one used most often on priced sales lines, and the notes stay in the source. In an interview, say how you chose the canonical value and where the alternatives remain available.
D4. A customer's country changed. How do you model that?
Ask first whether history matters. If reports must show sales under the country the customer had at the time of the sale, use a type 2 slowly changing dimension: add a new row for each change, with its own surrogate key and validity dates (Kimball: type 2). Eight customers appear under two countries. Customer 12417 went from Belgium to Spain on 28 April 2011 and back to Belgium on 5 June, so the dimension has three rows for that customer. Each fact row joins to the version valid at the sale's timestamp. The dimension has 4,389 rows for 4,372 customers plus one unknown member.
What to add: this is also why surrogate keys exist. The customer number alone cannot identify a row any more.
D5. What happens to facts with no customer?
135,080 lines have no customer ID. Leave the key NULL, and any inner join to the customer dimension silently drops those lines: 8,300,066 in revenue survived such a join, against 9,747,748 in total. The standard fix is an "Unknown" member with its own key, here 0, so every fact row joins to something and totals stay complete. The same applies to products: 138 lines had codes with no usable description and point to an unknown product row.
D6. Why build a date dimension instead of using the date column?
Because a date column only contains dates that had sales; a date dimension contains every date, plus attributes such as weekday, month and holidays (Kimball: calendar date dimension). Joining the full calendar to the facts showed that none of the 53 Saturdays in the period had a single sale. That is a fact about the business, presumably closed on Saturdays, which a fact table alone would never reveal. Six Mondays also had no sales, worth checking against the holiday calendar.
D7. Which measures can you add up?
Revenue and quantity are additive: they sum across any dimension. Distinct customers are not (Kimball: additive facts). Adding up each month's distinct customers gave 13,680, while the year had 4,372 distinct customers, because most customers bought in several months. Store the parts of a non-additive measure, or recompute it from the atomic grain, rather than summing pre-computed counts.
How to answer modelling questions
- Start with the business process and the grain.
- Ask how each attribute changes over time, and whether history matters.
- Plan for missing and messy values with unknown members and canonical attributes.
- Say which measures are additive before anyone builds a summary table.
The theory behind these answers is covered in data modelling: star schema, snowflake and data vault, change handling in slowly changing dimensions explained, and the same ideas in a BI tool in data modelling in Power BI. For related rounds, see data warehouse interview questions and data engineering interview questions and answers.
What are common data modelling interview questions?
What is the grain of a fact table?
When should I use a type 2 slowly changing dimension?
Why use surrogate keys instead of natural keys?
Dimensional modelling is part of the Data Engineering course.
Sources
- Grain (Kimball Group dimensional modeling techniques) — Kimball Group
- Type 2: Add New Row (Kimball Group dimensional modeling techniques) — Kimball Group
- Degenerate Dimensions (Kimball Group dimensional modeling techniques) — Kimball Group
- Additive, Semi-Additive, and Non-Additive Facts (Kimball Group) — Kimball Group
- Calendar Date Dimensions (Kimball Group dimensional modeling techniques) — Kimball Group
- Online Retail dataset (UCI Machine Learning Repository) — UCI Machine Learning Repository
GloryTecks Team
Training & Placement Faculty
The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.
Want to master Interview Questions?
Join GloryTecks’ expert-led Data Engineering training in Hyderabad with real-time projects and 100% placement support.
Related articles
Python Interview Questions for Data Roles
The plain-Python coding tasks data-analyst and data-engineer interviews use, without pandas: counting, deduplicating, grouping, streaming large files, flattening JSON, joining, and the bugs interviewers plant. Each has a worked solution and the follow-up questions to expect.
Data Science Interview Questions and Answers 2026
A data science interview is several interviews in one: statistics, machine learning, coding, product sense and a take-home. This guide maps each round with model answers, then works a churn take-home on real telecom data, including the checks that separate a strong submission from a high score.
AWS Interview Questions for Data Engineers
AWS interview questions for data engineers on S3, Glue, Athena, Redshift, Kinesis, Step Functions and security, each with the points a strong answer includes, the follow-up to expect, and three end-to-end scenario questions.