The Hunger Shift
A medium Data Modeling interview practice problem on DataDriven. Design the schema on the canvas: tables, keys and relationships, then get feedback on it.
- Domain
- Data Modeling
- Difficulty
- medium
- Seniority
- L5
Problem
We run a food-delivery marketplace, and product wants a live board of the dishes surging in each city right now, the kind of thing that reads 'pad thai is up 40 percent in Austin this week.' A single order usually bundles several different dishes, and the same dish shows up under many spellings across restaurant menus. The board refreshes every 15 minutes across hundreds of cities.
What's everyone eating? The answer changes hourly.
Worked solution and explanation
Why this problem exists in real interviews
This is a pre-aggregation and dimensional-conformance problem wearing a food-delivery costume. Anyone can draw a star schema. The signal is two decisions: normalizing dish names into dim_dishes so counts do not fragment across menu variants, and materializing agg_dish_city_hourly so a 15-minute refresh never scans raw orders. Miss the first and 'Pad Thai' trends nowhere because its orders scatter across 'pad thai noodles' and 'Thai Noodles'. Miss the second and the dashboard blows its SLA the moment real volume arrives.
Break down the requirements
Step 1: Normalize dish names with dim_dishes
Restaurants call the same thing by different names. A canonical dimension with a canonical_name and a synonyms map prevents trending signals from fragmenting.
Step 2: Keep fact_orders at order-item grain
An order averages 2.5 items, so the atomic fact is one row per order item, each with its own surrogate key and a dish_sk. Grouping items back to a parent order rides on order_id. This is the source of truth for every downstream rollup and the input to the aggregation job. Leaving it at 'order grain' while carrying dish_sk breaks the primary key.
Step 3: Pre-aggregate at dish-city-hour
agg_dish_city_hourly is built on a schedule. A 15-minute refresh reads this table, never the raw fact.
Step 4: Materialize trending_scores
Trending ratio is recent_count / baseline_count. Storing the computed values with computed_at lets the dashboard read a single row per dish and city. Storing the raw counts alongside the ratio is the more defensible choice because it makes the score auditable, but a table that carries only the derived percent change at (dish, city) grain is still a valid trending materialization.
The solution
Below is one defensible model. The conceptual anchor is the pre-aggregation layer: raw order items feed agg_dish_city_hourly, which feeds trending_scores.
- dim_dishes: dish_sk BIGINT PK, canonical_name TEXT, cuisine TEXT, tags JSON
- dim_cities: city_sk BIGINT PK, city_name TEXT, metro_area TEXT, timezone TEXT
- fact_orders: order_item_sk BIGINT PK, order_id BIGINT, dish_sk BIGINT FK, city_sk BIGINT FK, order_ts TIMESTAMP, quantity INT, amount FLOAT
- agg_dish_city_hourly: dish_sk BIGINT FK, city_sk BIGINT FK, hour_bucket TIMESTAMP, order_count INT, gross_sales FLOAT
- trending_scores: dish_sk BIGINT FK, city_sk BIGINT FK, computed_at TIMESTAMP, recent_count INT, baseline_count INT, trending_ratio FLOAT
- dim_dishes references fact_orders
- dim_cities references fact_orders
- dim_dishes references agg_dish_city_hourly
- dim_cities references agg_dish_city_hourly
- dim_dishes references trending_scores
- dim_cities references trending_scores
The analysis pattern
SELECT
d.canonical_name,
c.city_name,
t.trending_ratio
FROM trending_scores t
JOIN dim_dishes d ON d.dish_sk = t.dish_sk
JOIN dim_cities c ON c.city_sk = t.city_sk
WHERE t.computed_at = (SELECT MAX(computed_at) FROM trending_scores)
AND t.trending_ratio > 1.2
ORDER BY c.city_name, t.trending_ratio DESCTrade-offs and alternatives
- Pre-aggregation layer plus canonical dish dimension
- Refresh SLA is achievable. Atomic fact remains for backfills. Requires an aggregation job and a dish resolution pipeline. Storage cost is low relative to raw orders.
- On-demand query over raw orders
- No aggregation job to maintain. Simpler to reason about. Cannot meet a 15-minute refresh at high volume without a columnar engine and aggressive caching.
Common follow-up questions
- How would you handle a new menu item that has no synonym mapping yet? (Tests fallback logic when `dim_dishes` has no canonical match.)
- What is the exact definition of `recent_count` and `baseline_count`? (Tests whether trending windows are explicit and consistent.)
- How would you backfill `agg_dish_city_hourly` if a dish synonym mapping changes retroactively? (Tests reprocessability of the aggregation layer.)
- How would you extend the dashboard to support a 1-minute refresh during peak hours? (Tests pre-aggregation cadence and potential stream processing.)
- How would you prevent a single viral order spike from skewing trending ratios? (Tests smoothing, floor thresholds on baseline counts, and outlier handling.)