Code smells exist in data warehouses too. They aren't bugs — the queries still run — but they're signs the model is fighting the workload. Here are the five that recur most.
Smell 1 — Every dashboard query joins 8+ tables
If your typical analytics query looks like:
SELECT ...
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
JOIN categories c ON c.id = p.category_id
JOIN customers cu ON cu.id = o.customer_id
JOIN addresses a ON a.id = cu.address_id
JOIN regions r ON r.id = a.region_id
JOIN currency_rates cr ON cr.currency = o.currency
...
WHERE o.created_at > '2026-01-01'
…you've inherited an OLTP schema in your warehouse. The fix is a dimensional model where most of these are pre-joined into one or two fact tables.
Smell 2 — Two dashboards disagree on "active users"
Marketing's dashboard says 22,000 WAU. Product's dashboard says 19,500. Same data, two queries, two answers.
This happens when:
- "Active" is defined twice (once by marketing's events filter, once by product's stricter event filter).
- One team excludes internal employees; the other doesn't.
- One team uses UTC midnight boundaries; the other uses local time.
The fix is a conformed dimension — a single, canonical definition of "user" with flags for the various exclusions. Every team queries the same dimension. The fight moves from "whose number is right" to "which flag to use", which is a much shorter conversation.
Smell 3 — Updating one column requires rewriting the entire table
If changing product_category triggers a full CREATE TABLE AS SELECT over 200M rows, your model isn't partitioned, isn't keyed on the change boundary, or has the column in too many places.
The fix: keep mutable attributes in dimension tables, not duplicated into facts. Use SCDs (next module) so changes don't require fact rewrites.
Smell 4 — Timestamps are inconsistent across tables
Some tables use created_at (UTC, microseconds), some use creation_date (local date, no time), some use event_time (UTC ISO 8601 strings, not native timestamp).
Symptoms: every analyst writes CAST(... AS timestamp) everywhere. Join conditions break on timezone. Date filters silently exclude or include the wrong day.
The fix: a project-wide convention for timestamp columns (always event_time TIMESTAMPTZ, always UTC, native type). Enforce in the staging layer.
Smell 5 — No way to answer "what was the price last quarter?"
Your product_id 100 used to cost ₹999. Today it costs ₹1,499. Your warehouse has a products table that was overwritten. The price last quarter is lost.
This is the most expensive smell because the data is gone. Recovery requires source backups or rebuilding from change logs.
The fix is SCD Type 2 — every change to the dimension is a new row with effective_from and effective_to. Historical queries join on the row that was active at the time of the fact.
Other smells worth flagging
- No fact grain documented — analysts don't know what one row "means". GROUP BY semantics vary by query.
- Surrogate keys mixed with natural keys — some tables use UUIDs, some integer IDs, some natural keys. Joins are error-prone.
- NULL semantics are unclear — does
signup_country = NULLmean "didn't ask", "user opted out", or "ETL failed"? - "Special" rows hiding in dimension tables — a
customersrow with id=0 and name="(unknown)" that needs to be excluded from every WHERE clause.
How to find them
Quick audits to run on a warehouse you're inheriting:
- Top 10 slowest dashboard queries → which joins are repeated? Candidates for denormalization or a wide pre-aggregated table.
- Search the codebase for "definition of X" → if "definition of active user" appears in 5 places, you have a conformed dimension problem.
- Check the dimension tables → any without
effective_from / effective_to? Could be SCD Type 1 (intentional) or just lost history (smell). - Look for
WHERE name != '(unknown)'→ tells you about implicit exclusions hardcoded in queries.
The cost of leaving them
These smells compound. A startup with 50 dashboards and 3 of them disagree on WAU might be OK. At 500 dashboards and 50 stakeholders, every metric review meeting argues about definitions. By then, fixing it costs an engineering quarter.
Takeaway
Model smells aren't bugs but they're warnings. The teams that ship clean dashboards in year 3 are the ones who fixed the smells in year 1.