Analytics, Dashboard & Optimization.
From petabyte warehouse to trustworthy, sub-second decision surface.
A dashboard is not a collection of charts. It is the final serving layer of the analytics system — the one place where data modelling, query design, caching, concurrency, visualisation, semantics, freshness and correctness all collide, in front of the person making the decision. When any one of them is wrong, the dashboard is either slow, expensive, or confidently misleading.
Pre-compute
Compute the expensive answers before the user asks. Rollups, one-big-table marts, aggregate awareness, a semantic layer.
Serve
Serve from something optimised for interactive analytics — materialised views, result cache, extracts, an OLAP tier.
Restrain
Ask the backend for less data and fire fewer queries. Bounded windows, filter-aligned layout, lazy tiles.
Verify
Make sure the optimisation hasn't made the dashboard confidently wrong. Freshness, semantics, skew, ratios, time zones.
Each layer exists to make the next one cheaper. The work you refuse to do at view time is the entire game — and the last layer is a person, not a chart.
Illustrative examples — not benchmarks
Read the dashboard like an on-call engineer.
Before optimising anything, look at what the dashboard is actually doing. These are the numbers worth pulling first — every BI platform exposes some form of them, and the shape of this panel is usually enough to tell you which of the four levers you need.
Illustrative diagnostic scenario
Read together, this panel already names the fix. A healthy P50 with a terrible P95 is not a slow-SQL story — it is a concurrency and cache story. 27 queries per open and 1.8 TB scanned says nobody built an aggregate. And two correctness warnings mean that whatever you do to the first six numbers, you are not finished.
Can you fix this dashboard?
The same executive dashboard, twice. Nothing changed about what the business wanted to know — only what the dashboard asks the warehouse for, and how it presents the answer. Flip between them.
You have 90 seconds. What changed?
Scan titles, filters, time ranges, panel count, chart density, data volume and load behaviour — then check yourself below.
Global Commerce Performance
Revenue by Country (All Time)
Orders vs Marketing Spend (All Time)
Average Customer Spend (All Time)
Revenue Over Time (Daily, All Time)
Revenue by Category and Month (All Time)
Order Detail (All Time)
| Order ID | Date | Customer | Country | Region | Channel | Device | Category | Product | Units | Unit Price | Discount | Revenue | Refund | Net Revenue |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 100000001 | 2025-05-15 | Emma Johnson | United States | North America | Online | Desktop | Apparel | Performance Hoodie · Black / L | 1 | $79.99 | $0.00 | $79.99 | $0.00 | $79.99 |
| 100000002 | 2025-05-15 | Liam Williams | United Kingdom | Europe | Mobile App | Mobile | Footwear | Trail Runner · Grey / 9 | 1 | $129.99 | $13.00 | $116.99 | $0.00 | $116.99 |
| 100000003 | 2025-05-15 | Olivia Brown | Germany | Europe | Online | Desktop | Accessories | Leather Belt · Brown / 34 | 1 | $49.99 | $5.00 | $44.99 | $0.00 | $44.99 |
| 100000004 | 2025-05-15 | Noah Davis | Canada | North America | Mobile App | Mobile | Beauty | Hydrating Serum 30ml | 2 | $34.99 | $0.00 | $69.98 | $0.00 | $69.98 |
| 100000005 | 2025-05-15 | Ava Miller | Australia | APAC | Online | Desktop | Home | Ceramic Mug · White | 1 | $19.99 | $2.00 | $17.99 | $0.00 | $17.99 |
| … | ||||||||||||||
- Defaults to all-time history on every panel
- 50,000-row detail table rendered on first paint
- 12 live warehouse queries, no extract or cache
COUNT(DISTINCT)over raw events for every tile- KPI logic duplicated in six separate calculated fields
- Any filter change triggers a full refresh of all 22 widgets
- Six pie charts, one with 17 slices
- Latest day plotted as if complete — it is 40% loaded
NorthStar Commerce — Executive Pulse
Revenue over time
Today 40% complete · provisionalWhat's driving the change?
vs prior 28 days
Showing top drivers of $5.4M increase
Conversion funnel
vs prior 28 days
Top markets by revenue
vs prior 28 days
Explore customers
Drill into segments, cohorts and behaviour
- Five headline KPIs, each with a period comparison
- 28-day default window; longer ranges on request
- One trend panel, one variance / anomaly panel
- Top-N contributors instead of a full detail table
- Progressive drill-down; lower section lazy-loaded
- Reads a cached daily aggregate, not raw events
- Metrics defined once in the semantic layer
- Latest day marked provisional until the load completes
Find the optimisations
0 / 10 foundIllustrative optimisation scenario
Notice what the "after" dashboard did not do: nobody tuned a single SQL statement. The gains came from asking a smaller question (28 days, aggregate table), asking it fewer times (6 queries, lazy tiles), and refusing to render what nobody reads (the 50,000-row table). The one genuinely new engineering artefact is the semantic layer — and it is the piece that keeps the fast version honest.
One dashboard open ≠ one query.
This is the thing dashboard tutorials skip. A viewer opens one page; the BI tool fans that single intent out into a query per visual, plus one per filter control, plus one per tooltip that has to resolve on hover. Then you multiply by everyone who opened it at 9am.
Where did the 8 seconds go?
The single most common mistake in a dashboard-performance interview is jumping straight to the SQL. Split the wall-clock time first — in this scenario the warehouse is only 44% of it, and the queue in front of the warehouse is nearly as large as the execution itself.
✗ Before — ~8.0 s total
✓ After — ~460 ms total
Illustrative latency breakdown · bars scale within each panel
Optimisation playground.
Six decisions, and the qualitative consequences of combining them. This is not a cost calculator and does not model any specific vendor's pricing — it exists to make the trade-offs muscle memory. Note how freshness and correctness risk move in the opposite direction from load and latency.
Qualitative model for teaching trade-offs · not a pricing or benchmark tool
The same problem, four different levers.
Interviewers notice when a candidate says "add a cache" without knowing what that means in the tool the team actually uses. These platforms do not offer identical mechanisms, and pretending they do is the giveaway — where the capability genuinely differs, the difference is stated.
| Problem | Tableau | Looker | Power BI | Warehouse / serving layer |
|---|---|---|---|---|
| Repeated dashboard requests | Hyper extract, plus the workbook/query cache in front of it. | Persistent derived tables and aggregate awareness, with caching policies where appropriate. | Import mode, plus user-defined aggregations over a larger model. | Materialised views and the warehouse result cache. |
| High concurrency | Extract with an optimised, well-shaped datasource — live connections multiply the load per viewer. | Aggregate-aware queries so most viewers resolve against a small table. | Import, or Direct Lake where the platform and storage support it. | A dedicated OLAP serving tier, or elastic compute with workload isolation. |
| Metric disagreement | A governed published datasource so the definition lives in one place, not in each workbook. | LookML — the semantic model is the product's core abstraction. | A shared semantic model with measures defined once. | Central metric definitions (dbt metrics, Cube, or equivalent) upstream of every tool. |
| Interactive real-time analytics | Live connection — but only where the freshness requirement genuinely justifies the cost. | Live governed semantic queries against a fast backend. | DirectQuery, or Direct Lake where supported. | Pinot, Druid, ClickHouse, or another serving tier built for the workload. |
| Slow first paint | Fewer worksheets per dashboard; defer what is below the fold. | Fewer tiles resolving on load; bounded default filters. | Fewer visuals per page; reduce the model's column cardinality. | Pre-warm the cache on a schedule before business hours. |
Platform capabilities change — verify against current vendor documentation before an interview
The BI layer is part of the execution architecture.
Everything up to here has been about the warehouse and what sits in front of it. This part is about the half candidates forget: a dashboard can be slow, confusing, stale, expensive or misleading while the SQL underneath is exemplary. The BI tool generates queries, repeats calculations, moves data across a network, pushes work into a browser and decides what a human sees first. That is architecture, not decoration.
7.1A six-dimension diagnostic
When a dashboard is "bad", it is usually failing on one of six axes. Naming the axis first is what stops an interview answer collapsing into "I'd tune the query".
Does the dashboard answer a clear business question?
- What decision does it support?
- Who opens it, and how often?
- What belongs on first paint?
Are the right KPIs and dimensions shown?
- Is each metric attached to a decision?
- Are similar metrics distinguishable?
- Is anything here purely decorative?
Where are the calculations executed?
- Warehouse
- Semantic layer
- BI engine
- Browser / client
What happens when the user touches it?
- Filter, drill, hover
- Tab switch, parameter change
- How much refires each time?
Is the data current, complete and trusted?
- What is the SLA?
- Is the partial period labelled?
- Does the cache respect it?
What does it cost?
- First paint
- Interaction
- Concurrency
- Rendering vs execution
7.2Push computation to the right layer
Compute in the warehouse when
- the calculation runs over large datasets
- it is reusable across many dashboards
- it defines a governed business metric
- it performs expensive joins
- it performs expensive distinct counts
- it aggregates over large windows
- many users repeatedly need the same answer
Compute in the semantic layer when
- the business definition must be shared
- dimensional logic must stay consistent
- measures need governed definitions
- access rules or metric contracts apply
- the calculation is lightweight
- it is presentation-specific
- it depends on user interaction
- it is cheap over an already-small result
Compute in the browser only when
- the dataset is genuinely tiny
- the calculation is purely visual or interaction-related
- sorting the rows already rendered
- formatting a number
- highlighting a value below target
- toggling a series on a legend
7.3Tableau: live is not automatically better
Connection strategy is treated as an identity — teams describe themselves as "a live shop" — when it is a per-panel engineering decision. Behaviour varies by version and configuration; what follows is the shape of the trade-off rather than a product specification.
Live connection
- genuinely fresh operational data
- governed warehouse queries
- small, selective interactions
- cases where stale data is unacceptable
- each filter interaction may issue new queries
- performance follows warehouse contention
- concurrency multiplies cost
- complex dashboards create query fan-out
- network latency becomes visible to the user
Extract / Hyper
- repeated interactive analysis
- datasets that fit an extract strategy
- dashboards refreshed on a schedule
- workloads where sub-second interaction matters more than second-level freshness
- columnar optimised storage
- interaction largely decoupled from warehouse latency
- reduced warehouse query cost
- fast local filtering and aggregation
- stale extracts; refresh failures that fail quietly
- oversized extracts
- full refresh where incremental would do
- semantic logic duplicated between extract and warehouse
Hybrid — usually the honest answer
Nothing says every panel must share one connection strategy.
- Executive KPIs → extract / aggregate
- Today's operational status → live
- Customer drill-through → on-demand warehouse query
- Historical trends → extract / aggregate table
This is nearly always more rational than declaring "everything must be live".
Illustrative fan-out. 300 concurrent users × 12 worksheets × 5 filter interactions each can amount to thousands of query executions against the warehouse in a few minutes — each individually fast, collectively a queue. Illustrative
7.4Your dashboard is doing too much work
Four ways a workbook quietly becomes the expensive part of the system.
Expensive calculated fields, repeated
A calculated field performs COUNTD(Customer ID) over millions of rows — and then the same expression is reused for current period, prior period, region, channel, product and campaign.
The same expensive calculation may execute repeatedly across worksheets, and distinct counts are among the least shareable operations a warehouse can be asked for.
Complex or nested LOD expressions
Level-of-detail expressions are genuinely powerful. They are also easy to nest until the generated query plan is unrecognisable.
Depending on the expression and the tool version, this can produce additional subqueries or a materially more expensive plan — especially when duplicated across a workbook.
Table calculations over huge results
RUNNING_SUM, RANK, WINDOW_AVG, LOOKUP, percent-of-total — these generally run after the result set has been retrieved.
The database may have done its job perfectly and returned in 200 ms. The BI tool then has hundreds of thousands of marks to process locally, and the user waits anyway.
Duplicated calculated fields
Six fields implementing approximately the same idea:
This produces maintenance risk, inconsistent definitions, redundant compute and — worst — users who cannot tell which number is the real one.
7.5The KPI graveyard
The header of a real executive dashboard. Twenty-two tiles, no hierarchy, four of them measuring almost the same thing. Look at it for five seconds before reading on.
Executive Business Overview
The same business, six tiles, grouped by what they tell you — and every one carries a comparison so the number can be judged.
Executive Business Overview
Illustrative scenario — not benchmark data
7.6Three cards, three numbers, no explanation
These sit side by side on a real dashboard. Nobody can say why they differ, so people quietly pick whichever supports their argument. Open each definition.
Settled transactions, excluding tax.
Revenue after refunds.
Gross order value, before refunds.
7.7Four metrics, four different moments
One dashboard strip. Every tile looks equally current — that is the whole problem. Flip the switch.
Yesterday 03:00⚠ 31 hours staleSLA: 1 hour
8 min ago✓ CurrentSLA: 15 min
42 min ago✓ Within SLASLA: 1 hour
3 days ago⚠ 3 days staleSLA: 1 day
Illustrative scenario — not benchmark data
7.8Dashboards accumulate archaeology
A dashboard estate is a codebase nobody refactors. An honest inventory usually looks something like this.
Illustrative estate
Do not delete blindly. Route the decision through owner, usage, last accessed, freshness, downstream dependency and certification status. Deleting a dashboard that three people depend on silently is how a cleanup programme gets cancelled.
7.9The filter lab
Thirteen controls stand between the user and the first insight. Each one is a question you are asking them to answer before the dashboard will say anything.
- Overwhelming to scan
- Hard to discover the useful ones
- Cache fragments across combinations
- Slow first paint
The same dashboard, filtered by how people actually decide. The customer control becomes a search box — because a domain nobody can scan should never be preloaded.
- Faster to first insight
- Maps to real decisions
- Query shapes repeat, so cache hits
- No 11.2M-value domain fetched
Illustrative scenario — not benchmark data
7.10The interaction cost lab
A dashboard is a workload generator with a title. Twenty-four worksheets do not load once — they load twenty-four times, plus the controls, plus everything that refires when someone touches a filter.
Executive Dashboard
The same business questions, scoped. Six visuals on first paint, three tabs that cost nothing until opened, and a filter that only touches the panels it actually means something for.
Optimised Dashboard
Illustrative scenario — not benchmark data
Scope is where most of that saving comes from. The user picks Product Category = Electronics; here is what each version decides to recompute.
✗ Global filter — 18 worksheets refresh
✓ Scoped filter — 5 worksheets refresh
7.11The client rendering lab
The warehouse answered in 210 ms. The dashboard still took 4.8 seconds. Everything below happens after the query is already finished.
Map
Scatter plot
Detail table
Time series
Illustrative scenario — not benchmark data
The arithmetic behind all four: a chart is only so many pixels wide.
7.12Tooltip abuse
- Revenue
- $4.8M
- Orders
- 38.2K
- Units
- 61.7K
- Customers
- 11.2K
- Margin
- 38.1%
- AOV
- $125.52
- Conversion
- 3.81%
- Country
- United States
- Region
- US West
- Campaign
- Spring-24
- Device
- Mobile
- Refunds
- $0.3M
- Forecast
- $5.1M
- Variance
- −5.9%
- Confidence
- 0.82
- Notes
- —
7.13Auto-refresh abuse
An executive dashboard set to refresh every 30 seconds, over a pipeline that produces new data every 60 minutes. That is roughly 120 refreshes between two meaningful changes — every one of them a full query workload, multiplied by everyone who left the tab open.
7.14Four different things called "stale"
These get used interchangeably in incident channels and mean entirely different repairs. Separating them is one of the more interview-worthy distinctions on this page.
The pipeline has not produced new data. Everything downstream is correct — and correctly out of date. Fix the pipeline.
New data exists, but a cached result has not been invalidated. The system is serving a correct answer to an old question. Fix invalidation.
Fresh data, valid cache — but this browser session has not re-fetched. Fix the refresh or reload behaviour.
The metric definition changed; the dashboard still computes the old one. Numbers look plausible and are wrong. Fix the definition and its lineage.
7.15Where the 4.4 seconds went
A single filter click, broken down. Read it before deciding what to optimise.
User clicks a filter — ~4.4 s total
Illustrative breakdown
7.16Where should this calculation live?
Eight calculations. Pick a layer for each — the card tells you whether it agrees, and why. The reasoning matters more than the label: interviewers are listening for size, reuse and governance, not for a memorised answer.
7.17Before you ship the dashboard
Intent
- What decision does this dashboard support?
- Who is the primary user?
- What belongs on first paint?
Metrics
- Are metrics defined once?
- Are similar metrics clearly distinguished?
- Are stale or unused KPIs present?
- Does every KPI carry a comparison or context?
Performance
- How many queries fire on first paint?
- How many fire after one filter interaction?
- Live or extract — and why this one?
- Is expensive calculation happening in the BI layer?
- How many marks are rendered?
Freshness
- What is the data SLA?
- Is the as-of visible?
- Are partial periods clearly labelled?
- Does caching respect the freshness rule?
UX
- Are there too many filters?
- Is detail progressively disclosed?
- Is the chart appropriate to the question?
- Can a user find the answer in five seconds?
7.18Three dashboards, three interviews
Work each one before opening the approach. Where behaviour depends on product version or configuration, these are framed as things to investigate rather than as vendor facts.
Live connection, 18 worksheets, 12 filters, five years of default history.
COUNTD repeated across six calculated fields, three LOD expressions, a 48,000-row detail grid, refreshing every minute against a source that changes hourly. How would you optimise it?
Reveal approach
1. Measure before touching anything. Performance Recording splits query time, render time and interaction cost — the answer changes completely depending on which dominates.
2. Decide what genuinely needs live access. The source changes hourly and the dashboard refreshes every minute; that alone is 60 refreshes per meaningful change. Almost certainly only a current-day operational slice needs live.
3. Move the repeated metrics upstream. Six calculated fields performing the same distinct count is one governed aggregate waiting to be built.
4. Build an extract or aggregate for the historical views, keep the small live slice if the business truly needs it, and let the two coexist — hybrid rather than doctrine.
5. Reduce first-paint worksheets, scope the filters to the panels they actually mean something for, and remove the 48,000-row grid in favour of Top-N plus drill-through.
6. Align refresh cadence with source freshness, ideally by invalidating on pipeline completion rather than on a timer.
Then re-measure and state what you bought — and what it cost in freshness.
Fourteen Looks embedded on one page, all exploring the same event table.
Each carries a slightly different definition of "active user", there is no aggregate awareness, time windows are large, and the cache is fragmented across many filter combinations. What would you change?
Reveal approach
Start with the definition, not the performance. Fourteen tiles with slightly different active-user logic is a governance failure that happens to also be slow. One LookML definition, owned, is the first change — everything else gets easier afterwards.
Give the queries a shape worth caching. Fourteen near-identical explores over large windows fragment the cache because no two requests match. Converging on common query shapes and bounded default windows is what makes caching effective at all.
Add aggregate awareness so tiles resolve against a smaller table where the grain permits, with persistent derived tables or aggregate tables where the workload justifies maintaining them.
Cut first-paint tiles. Fourteen embedded Looks is fourteen concurrent requests every time the page opens; most pages have three or four that people actually read first.
Name the trade-off: aggregates constrain ad-hoc exploration, so the process for adding a dimension has to be fast or analysts will route around the model.
Large semantic model, high-cardinality columns, many calculated columns, DirectQuery on every page.
Dozens of visuals, automatic page interactions left on, and users complaining about eight-second slicer response. How would you investigate?
Reveal approach
Profile first. Performance Analyzer attributes the eight seconds across DAX evaluation, query execution and visual rendering; query diagnostics show what is actually reaching the source. Which of the three dominates decides everything after.
Look at the model before the measures. High-cardinality columns drive model size and compression; calculated columns are materialised at refresh and inflate it further. Ask whether each one needs to exist, and whether it belongs upstream instead.
Question the storage mode. DirectQuery on every page means every interaction is a source round trip. Import, or Direct Lake where the platform and storage support it, changes the interaction profile entirely — with the usual freshness trade-off to state out loud.
Examine the DAX. Measures that force large filter-context evaluation, or that iterate row by row over big tables, are a common cause of slow slicers even on a healthy model.
Then the visuals. Reduce the count per page, turn off automatic cross-interaction where it is not wanted so one slicer does not refresh everything, and add aggregations for the summary paths.
Behaviour here depends on version, capacity and configuration — so present these as the things you would check, with the profiler deciding the order.
7.19SCOPE — answering in order
Handed a slow or confusing dashboard, work these five in order and say which one is failing before proposing a fix.
Are we showing the right metrics, defined once?
Where is each calculation actually executed?
How many queries and interactions are triggered?
Are we rendering more than anyone can read?
Is the data, the cache and the dashboard fresh?
A great dashboard does less.
It calculates less at interaction time. It asks fewer questions of the warehouse. It renders fewer marks. It presents fewer metrics. It exposes fewer filters. And yet it helps the user make more decisions.
Performance is not how fast the dashboard draws everything. Performance is how little unnecessary work it needs to do before the user understands the answer.
A dashboard can be fast and still be terrible.
Every fix below costs nothing at query time. They are pure comprehension wins — and each one is a live way to mislead an executive who trusts the chart.
1 · A pie chart with 17 slices
✗ Bad — angle comparison across 17 categories
Angle and area are the least accurately decoded visual channels. At 17 categories the chart carries a legend and no information.
✓ Better — sorted Top-N bar + "Other"
Position on a common axis is decoded precisely, sorting does the ranking for the reader, and the long tail is honestly collapsed into one labelled bar.
2 · A dual-axis chart implying correlation
✗ Bad — two scales chosen to make lines converge
Two independent axes can be scaled until almost any pair of series appears to move together. The reader sees a causal story the data never claimed.
✓ Better — small multiples, shared baseline
Separate panels, or both series indexed to a common baseline. The reader can still compare shape, but no scale trick manufactures a relationship.
3 · The mean hides the skew
✗ Bad — one number for a skewed distribution
A handful of enterprise orders drag the mean far above the typical customer. Decisions get made for a person who does not exist.
✓ Better — distribution with percentiles
Median $61, mean $142, P90 $310 — three numbers that describe the business honestly. The gap between median and mean is the insight.
4 · A truncated y-axis exaggerating movement
✗ Bad — axis starting at 94%
The underlying move is 1.4 percentage points. The axis makes it look existential, and someone will schedule a war room about it.
✓ Better — full scale, with the delta labelled
Keep the honest scale and annotate the change. If the small move genuinely matters, say so in words rather than smuggling it in through the axis.
5 · A 50,000-row detail table
✗ Bad — the whole fact table, on first paint
Costly to query, costly to transfer, costly to render — and functionally unreadable. It is an export disguised as a visualisation.
✓ Better — summary, Top-N, then search or export
Show the shape, rank the contributors, and give a path to the detail — search, drill-through, or an export for the person who genuinely needs all 50,000 rows.
The right chart for the question.
Chart selection begins with the analytical question, never with the chart menu. Novelty is not a design goal — if a reader has to learn a new visual grammar to read your dashboard, you have spent their attention on the wrong thing.
Reveal detail progressively.
An executive dashboard that opens on Layer 3 is not thorough, it is unusable — and it is also the expensive one, because every one of those detail panels is a query. Hierarchy is a performance decision as much as a design one.
Every optimisation has a bill.
Naming the cost of your own recommendation is the difference between a candidate who has read about these techniques and one who has run them in production.
Extract
Pre-aggregation
Cache
OLAP serving layer
Approximation
Semantic layer
Dashboards are becoming conversational.
The consumption model is changing from filter → click → inspect chart to ask → explain → investigate → drill → act. That does not retire any of the engineering above. It raises the stakes on one specific piece of it.
Where the products are heading
Named so you can hold a conversation about the landscape — not as a feature comparison. These capabilities change quickly; confirm specifics against current vendor documentation before you cite them in an interview.
What that surface actually feels like
A mocked exchange — deterministic sample content, no model call. The detail worth noticing is the last line.
Why did revenue fall yesterday?
Revenue decreased 8.4% versus the prior day.
Primary contributors:
- West region — −$1.2M
- Enterprise renewals — −$740K
- Mobile conversion — −0.8 pp
The diagram you should be able to redraw in 60 seconds.
Not an enterprise reference architecture — the minimum shape that lets you talk about any dashboard question. Three kinds of arrow matter: the data path, the metadata that governs it, and the metric contract every consumer resolves against.
Can you diagnose it?
Ten scenarios in the order a loop escalates them. Think your answer through before opening the approach — reading a good answer feels like learning and usually isn't.
An executive dashboard has 24 charts and nobody knows where to look.
How would you redesign it?
Reveal approach
Start with evidence, not taste: per-visual usage telemetry plus a handful of user interviews asking what decision they come here to make. Then rebuild around one question per surface — if there are three audiences, that is three dashboards over one governed model, not 24 charts and 18 filters. Impose a hierarchy: three to five KPI tiles with comparisons on top, two or three explanatory charts below, detail behind drill-through. Cut the palette to one, give the filters opinionated defaults, and ship it as a migration with a deprecation window rather than deleting charts people quietly depend on. Measure success by time-to-first-insight and repeat usage, not by charts removed.
A Tableau workbook takes 12 seconds after every filter click.
What would you investigate first?
Reveal approach
Record a Performance Recording and split the 12 seconds before touching anything — query execution, connection/queue, rendering, and layout compute are four different problems. A live connection re-querying every worksheet on each filter change points at extract-vs-live; many worksheets each firing their own query points at topology; a slow render with a fast query points at mark count or a giant crosstab. Check whether the filter is a quick filter forcing a full domain scan on a high-cardinality field, and whether context filters are being recomputed. Only after the split do you look at the generated SQL.
A sales dashboard defaults to all-time history.
Why could this be expensive, and what would you change?
Reveal approach
Every viewer's first paint scans the full history across every panel, so the most expensive query in the system is also the most frequently executed one — and partition pruning does nothing when the filter is "everything". It also grows without limit: the dashboard gets slower every quarter with no code change. Set a bounded default (28 days or the current quarter), make longer ranges an explicit choice, align partitioning to the date filter, and point the default view at a rollup. Keep the all-time number if the business wants it, but serve it from a pre-computed total rather than a live scan.
Finance and Sales dashboards show different revenue.
Where would you investigate metric semantics?
Reveal approach
Assume different definitions before assuming a bug. Collect each number's provenance — query, source, period boundary, filters — and reproduce both. Then walk the standard axes: gross vs net of refunds and discounts; bookings vs billings vs recognised revenue; order date vs ship date vs invoice date; time zone; currency rate and rate date; test, internal and intercompany transactions; cancellations. Build a reconciliation bridge that adds each named difference until one figure becomes the other — that artefact ends the argument. Only then look for defects: fan-out joins, dedup differences, a stale watermark. Prevent it with one definition, one business owner, visible certification, and an automated reconciliation job.
400 employees open the same dashboard at 9am Monday.
Why does performance collapse when each query takes only two seconds?
Reveal approach
Because the unit of load is not the query, it is queries × viewers, and they all arrive in the same three minutes. Twenty panels × 400 viewers is 8,000 executions against a warehouse with finite concurrency slots; everything past the slot limit queues, so the two-second query becomes a two-second query behind ninety seconds of waiting. The fix is topology, not tuning: a result cache so identical queries execute once, cache warming scheduled before 9am, an extract or aggregate so the queries are cheap enough to be absorbed, and workload isolation so ad-hoc analysis cannot starve the executive tier. If interactive concurrency is a permanent requirement, that is the argument for an OLAP serving tier.
Product wants minute-fresh data, but 95% of questions use yesterday or older.
Would you make the entire dashboard real-time?
Reveal approach
No — and being able to say that well is the point of the question. Ask what decision changes inside the freshness window; if nobody acts within the minute, the requirement is an alerting requirement wearing a dashboard costume. Split the surface: a small real-time panel fed by a streaming aggregate for the genuinely operational metrics, and the historical bulk served from batch rollups, each labelled with its own freshness. That keeps one expensive path narrow instead of making every panel expensive. Then push the threshold conditions into alerts that fire whether or not anyone is looking at the screen.
Design an analytics serving layer: 5 PB of source data, 500 concurrent users, sub-second interaction.
What is the end-to-end architecture?
Reveal approach
Lead with the principle: nothing scans 5 PB interactively, so the answer must already exist when the question is asked. Events land in an open table format on object storage, partitioned by date and clustered on the most selective dimension — that is the system of record and the drill-through target, not what dashboards query. Above it, rollup cubes at the ten to twenty dimension combinations users actually slice, built incrementally, with additive measures and HLL sketches so distinct counts stay mergeable. Serve those from an OLAP engine (Druid, Pinot, ClickHouse) built for high-concurrency sub-second scans, fronted by a result cache with warming, behind a semantic layer that routes queries to the right rollup automatically. Isolate interactive compute from batch, cap per-query resources, and control cost with incremental builds and usage-driven cube retirement. Then name the trade-off: precomputation buys latency at the cost of flexibility, so the process for adding a dimension has to be fast or people will route around the platform.
Conversion improves 35% right after a tracking deployment.
Business improvement, or instrumentation bug?
Reveal approach
Decompose the ratio first. Plot numerator and denominator separately: denominator fell with a flat numerator means top-of-funnel tracking broke or a bot filter changed; numerator rose alone is plausible improvement or double-counted events; both moved proportionally suggests a population or attribution change. Then check the release for renamed events, an SDK bump, a consent-banner change, or an event now firing twice. Plot hourly around the deploy — a vertical cliff at the deploy timestamp is instrumentation, a genuine product win almost always ramps. Segment by platform, app version and browser, since real breakage is usually scoped. Finally, reconcile against a system that did not change: payments, orders in the transactional database, server logs. Report with the decomposition and the independent check, not a hunch — then add volume anomaly detection and a tracking-plan test in CI so the next one announces itself.
4,000 dashboards and 600 versions of "active customer".
How would you restore trust?
Reveal approach
Refuse the tool migration — that produces 4,000 dashboards in a new tool. Sequence it. Measure first: per-dashboard usage, cost attribution, lineage, duplicate-definition detection, published openly so the conversation runs on evidence. Govern next: pick the twenty to fifty metrics the business actually runs on, agree one definition and one named business owner each, implement them once in the semantic layer, and mark dashboards built on certified metrics visibly. Consolidate: retire what has no views in 90 days with notification and a restore window, merge near-duplicates, and make the certified path faster than the bespoke one — otherwise people rebuild around it. Then cost: kill scheduled refreshes for dashboards nobody opens, add aggregates for the heavy ones, right-size compute. Finally prevent regrowth with certification gates and lineage-based change review. Report certified-viewing share, not dashboards deleted.
Leadership asks an AI assistant "why did revenue fall yesterday?"
Design the system that answers it while preserving governance, permissions, lineage, freshness and explainability.
Reveal approach
The assistant must not author SQL against raw tables. Route every question through the semantic layer so "revenue" resolves to the one governed definition, and execute under the asking user's identity so row-level security applies to generated answers exactly as it does to clicked ones — an assistant that bypasses RLS is a data breach with a chat interface. Answer from pre-computed aggregates for latency, and attach freshness to every response: if yesterday's partition is 96% loaded, the answer says so before it says the number. Make it explainable by returning the metric definition, the filters applied and the generated query alongside the narrative, so a human can audit the path. Log every question, resolved query and returned answer for lineage and review. Constrain scope: contribution analysis over modelled dimensions is a solvable problem; open-ended causal claims are not, and the system should decline rather than speculate. Then evaluate it like a product — a fixed question set with known answers, run on every change to the semantic model.
How to say it in the interview.
Four moves, in this order. The order is the signal — it shows you treat the dashboard as a system rather than a pile of SQL, and it keeps you from optimising something you have not measured.
Locate the latency
Split the wall clock before touching anything — queue, execution, transfer, render. "Slow dashboard" is not "slow SQL" until the numbers say so.
Pre-compute
Move expensive work out of view time: rollups, a one-big-table mart, aggregate awareness, and governed metrics defined once.
Accelerate & restrain
Materialised views, cache, extracts, an OLAP tier where justified — and bounded windows, filter-aligned layout, lazy tiles to ask for less.
Verify
Freshness, metric semantics, skew, ratios, time zones, approximation. Speed that buys a wrong answer is a regression.
That is the whole pillar in one breath, and it dovetails with Performance: the rollups and sketches you build here are made cheap by the scan, shuffle and skew techniques there, and both rest on the schemas from Design.
Pattern library — 19 serving-layer patterns.
The detailed reference behind everything above: each pattern as a scenario, its anti-pattern, the optimised rewrite, why it wins, and the impact. Grouped by the four levers. Expand what you need — this section is a lookup table, not a reading list.
A · Model
Pre-compute the answer before the question — rollups, marts, aggregate awareness, semantics.
№ 01Dashboards read rollup tables, never raw events
Dashboards read rollup tables, never raw events
Spotify — a "streams per day, last 90 days" tile wired straight to the raw play-events firehose, re-scanned on every load by every viewer.
-- the panel's query, run on every page load:
SELECT ds, COUNT(*) AS streams
FROM play_events -- billions of rows/day
WHERE ds >= DATEADD(day,-90,CURRENT_DATE)
GROUP BY ds;-- scheduled once/day: agg_streams_daily (1 row/day/dim)
INSERT INTO agg_streams_daily
SELECT ds, country, COUNT(*) streams, ...
FROM play_events WHERE ds = CURRENT_DATE GROUP BY ds, country;
-- the panel now scans ~90 rows:
SELECT ds, SUM(streams) FROM agg_streams_daily
WHERE ds >= DATEADD(day,-90,CURRENT_DATE) GROUP BY ds;Why it wins. A dashboard is read hundreds of times between data refreshes, so paying the scan once in a scheduled job and serving everyone from a one-row-per-day×dimension table is the highest-leverage move in all of analytics. Keep a small ladder of grains (hourly → daily → monthly) and point each panel at the coarsest one that answers it.
№ 02Pre-join a one-big-table mart so the dashboard never joins
Pre-join a one-big-table mart so the dashboard never joins
Airbnb — a bookings dashboard whose every filter triggers a 6-table star-schema join at query time, multiplied across panels.
SELECT d.market, l.room_type, SUM(f.gbv)
FROM fact_bookings f
JOIN dim_listing l ON l.listing_id = f.listing_id
JOIN dim_market d ON d.market_id = l.market_id
JOIN dim_date dt ON dt.ds = f.ds
JOIN dim_guest g ON g.guest_id = f.guest_id
... GROUP BY 1,2; -- every panel re-runs the joins-- built once in ELT: bookings_obt has the dims'
-- attributes denormalized onto each fact row.
SELECT market, room_type, SUM(gbv)
FROM bookings_obt
WHERE ds BETWEEN :start AND :end
GROUP BY market, room_type;Why it wins. Star schemas are the right storage model, but joins are the most expensive thing a dashboard does repeatedly. Flattening the hot dimensions onto the fact in an ELT step (a "one big table") trades a little storage and refresh cost for join-free reads — and columnar compression makes the duplicated dimension values nearly free. The semantic layer (№4) can still present it as a clean star.
№ 03Aggregate awareness — route each query to the smallest table that answers it
Aggregate awareness — route each query to the smallest table that answers it
Uber — the same metric is asked at city-month, country-week and global-day granularity; one table can't be optimal for all three.
-- every query hits the finest table (trip-level)
-- even when it only needs country-month totals,
-- OR analysts hard-code which rollup to use and
-- the wiring rots as rollups change.# Cube / Looker style: declare rollups; the
# query planner rewrites to the coarsest match.
pre_aggregations:
by_country_month: {measures: [gbv], dimensions: [country],
granularity: month}
by_city_day: {measures: [gbv], dimensions: [city],
granularity: day}
# a country-month question auto-routes to by_country_monthWhy it wins. Aggregate awareness (Looker's aggregate_awareness, Cube pre-aggregations, Mondrian agg tables) lets you define a hierarchy of rollups and have the BI layer automatically rewrite each query to the smallest pre-aggregation that can answer it — transparently falling back to raw for unusual cuts. Analysts write one logical query; the engine routes it.
№ 04A semantic layer — define each metric once
A semantic layer — define each metric once
Meta — five teams each hand-write "active user," with subtly different filters, and three dashboards disagree by 4% in the same all-hands.
-- dashboard A
COUNT(DISTINCT CASE WHEN events > 0 THEN user_id END)
-- dashboard B (forgot the bot filter)
COUNT(DISTINCT user_id)
-- dashboard C (different session window)
COUNT(DISTINCT CASE WHEN session_min >= 1 THEN user_id END)# semantic_model.yml — defined once, reused everywhere
metrics:
- name: weekly_active_users
label: WAU
calculation: count_distinct(user_id)
filter: "is_bot = false AND events > 0"
# every dashboard references metric('weekly_active_users')Why it wins. A semantic/metrics layer makes the metric definition a single governed object that every tool consumes, so the numbers reconcile by construction. It also centralises the join paths and rollup routing — which means the performance wins of №1–3 are applied once and inherited by every dashboard instead of re-litigated in each.
№ 05Store sketches, not counts — so distinct metrics stay roll-up-able
Store sketches, not counts — so distinct metrics stay roll-up-able
Reddit — a rollup stores daily_distinct_users as an integer, then a PM asks for the monthly distinct and the dashboard "helpfully" sums 30 days of it.
-- agg_daily.dau is an INT count of distinct users
SELECT SUM(dau) AS "MAU" -- ❌ double-counts
FROM agg_daily -- anyone active on
WHERE month = '2024-01'; -- 5 days counts 5×-- agg_daily.dau_hll holds a HyperLogLog sketch
SELECT HLL_ESTIMATE(HLL_COMBINE(dau_hll)) AS mau
FROM agg_daily
WHERE month = '2024-01';
-- sketches MERGE across any window → correct MAU,
-- L7, L28, quarter — all without rescanning raw.Why it wins. Sums, counts and min/max are additive — you can roll them up freely. Distinct counts are not, and pre-aggregating them as integers bakes in a double-counting bug. Storing a HyperLogLog sketch instead keeps the metric mergeable: any time window is a union of sketches, so the rollup stays both correct and cheap. (Same engine as Performance №14, used here to keep the serving layer honest.)
B · Serve
When pre-aggregation isn't enough, change the engine — MVs, cache, extracts, OLAP.
№ 06Materialized views + result cache for repeated queries
Materialized views + result cache for repeated queries
Salesforce — a heavy aggregate behind a popular tile, recomputed from scratch for every viewer although the underlying data changes hourly.
-- complex GROUP BY over a large base table,
-- executed fresh on every dashboard open even
-- though inputs only change once an hour.CREATE MATERIALIZED VIEW mv_kpi AS
SELECT region, ds, SUM(amount) amt, COUNT(*) n
FROM base GROUP BY region, ds;
-- MV auto-maintains incrementally; identical
-- repeat queries also return from the result
-- cache instantly until the data changes.Why it wins. A materialized view persists the aggregate and (on Snowflake/BigQuery/Redshift) maintains it incrementally as the base changes, so viewers read a small, current result. The warehouse result cache stacks on top: byte-identical repeat queries return with zero compute until the inputs change. Together they absorb the "everyone opens the same dashboard" load.
№ 07Put a real-time OLAP engine in front for sub-second @ high concurrency
Put a real-time OLAP engine in front for sub-second @ high concurrency
LinkedIn / Uber — a member-facing "who viewed your profile / trips this week" analytics surface: thousands of concurrent users, <200 ms expected, fresh to the minute. A batch warehouse can't do this.
-- every user request fires a warehouse query;
-- queue depth explodes at concurrency, p99 is
-- seconds, and freshness lags the batch job.-- ingest the stream into Druid/Pinot/ClickHouse:
-- • rollup at ingestion (pre-aggregated segments)
-- • columnar + inverted/bitmap indexes
-- • scatter-gather across data nodes
-- the app queries the OLAP store, not the warehouse:
SELECT dim, SUM(metric) FROM events_realtime
WHERE ts > now() - INTERVAL '7' DAY GROUP BY dim;Why it wins. Druid, Pinot and ClickHouse are built for exactly this: roll-up at ingestion, columnar segments with bitmap/inverted indexes, and scatter-gather execution tuned for many small concurrent aggregations with sub-second p99. They ingest from Kafka for minute-fresh data. It's the standard pattern when a dashboard is really a product surface, not an internal report.
№ 08Extract vs live connection — cache the data next to the BI tool
Extract vs live connection — cache the data next to the BI tool
Walmart — a Tableau workbook on a live warehouse connection where every filter click round-trips a fresh query, and 300 analysts do it all day.
-- LIVE connection: each filter/drill = a new
-- warehouse query. Interactive latency is at the
-- mercy of warehouse load; cost scales with clicks.-- Tableau Hyper extract / Power BI Import:
-- • a compressed columnar snapshot lives with the
-- BI engine; interactions hit RAM, not the WH
-- • scheduled refresh keeps it current
-- • filter to the needed rows/cols at extract time
-- use LIVE/DirectQuery only when true real-time
-- freshness is the requirement.Why it wins. A Tableau Hyper extract or Power BI import is a purpose-built columnar cache sitting next to the BI engine — interactions resolve in memory instead of round-tripping to the warehouse, which is both faster and dramatically cheaper. Reserve live/DirectQuery for genuinely real-time needs, and even then back it with aggregate awareness. Extract only the fields and grain the workbook uses (never SELECT *).
№ 09Refresh extracts and MVs incrementally, not full-rebuild
Refresh extracts and MVs incrementally, not full-rebuild
Datadog — a 2-year extract behind a usage dashboard, fully rebuilt every hour because "refresh" was left on the default.
-- hourly job re-reads and re-loads 730 days of
-- data to pick up the last hour of changes.-- Power BI incremental refresh policy:
-- archive > 2 years, refresh last 3 days
-- dbt incremental model:
{{ config(materialized='incremental') }}
SELECT ... FROM events
{% if is_incremental() %}
WHERE ds > (SELECT MAX(ds) FROM {{ this }})
{% endif %}Why it wins. The same "compute once" discipline from the Performance pillar (№23–24) applied to the serving layer: partition the extract/MV by date and refresh only the recent, mutable window — archiving the stable history. Refresh cost scales with new data, not total history, so the hourly job stays flat as the dataset grows.
№ 10Approximate counters for the headline tiles
Approximate counters for the headline tiles
TikTok — a real-time "unique viewers" big-number tile recomputing an exact COUNT(DISTINCT) over the live firehose every few seconds.
SELECT COUNT(DISTINCT viewer_id) AS unique_viewers
FROM live_views; -- exact, expensive, and
-- nobody reads the last 3 digits
-- of a 14,237,1•• counter.SELECT APPROX_COUNT_DISTINCT(viewer_id) AS unique_viewers
FROM live_views; -- ~1–2% error, a fraction
-- of the cost; exact reserved
-- for billing/export drill-downs.Why it wins. A headline counter is read at a glance — 14.2M vs 14,237,104 changes no decision, so paying for exactness is pure waste. Approximate distinct/percentile (HLL, t-digest) give the number in a fraction of the cost and memory. Keep exact computation for the places that legally require it — billing, finance, compliance exports.
C · Restrain
The cheapest query is the one the dashboard never fires.
№ 11Bounded default window + lazy-loaded tiles
Bounded default window + lazy-loaded tiles
GitHub — a 30-panel dashboard that defaults to "all time" and fires all 30 queries the instant it opens, including tiles below the fold nobody scrolls to.
-- default range: since the beginning of time
WHERE ds >= '2015-01-01'
-- and 30 panels issue their queries on page load,
-- 25 of them never scrolled into view.-- default to the window people actually look at:
WHERE ds >= DATEADD(day,-28,CURRENT_DATE)
-- render above-the-fold tiles first; defer the
-- rest until scrolled/expanded; let users opt in
-- to longer ranges explicitly.Why it wins. Most dashboard views only need a recent window, and most panels are never looked at in a given session. A sensible bounded default plus lazy tile loading turns a 30-query thundering herd on every open into a handful of small, recent-window queries — which also prune partitions cleanly. Longer ranges become an explicit, infrequent choice.
№ 12Align partition & cluster keys to the dashboard's filters
Align partition & cluster keys to the dashboard's filters
Stripe — every dashboard filters by merchant_id and date, but the serving table is partitioned only by date, so the merchant filter scans every file in the range.
-- table partitioned by ds only; merchant scattered.
SELECT ... FROM payments_mart
WHERE ds >= :start AND merchant_id = :m;
-- merchant_id = :m touches every file in the date
-- range — no skipping on the most-used filter.-- Snowflake: CLUSTER BY (ds, merchant_id)
-- BigQuery: PARTITION BY ds CLUSTER BY merchant_id
-- Delta: OPTIMIZE ... ZORDER BY (merchant_id)
-- now merchant_id prunes via min/max stats and the
-- dashboard's most common filter skips most files.Why it wins. The serving table should be physically organised around how the dashboard actually filters. Partition by the coarse time dimension, cluster/Z-order by the high-selectivity filter columns (merchant, account, country), and every interactive filter prunes instead of scans. This is Performance №26 applied with the dashboard's WHERE clause as the design input.
№ 13Survive concurrency with caching + elastic warehouses
Survive concurrency with caching + elastic warehouses
Atlassian — Monday 9am, 500 people open the same exec dashboard in five minutes; the warehouse queues and everyone watches spinners.
-- single-cluster warehouse; 500 concurrent runs of
-- the same uncached aggregate queue behind each
-- other → p99 measured in minutes.-- 1) MV + result cache so identical queries don't
-- recompute (see №6) — most of the 500 are cache hits
-- 2) multi-cluster / autoscaling warehouse for the
-- concurrent misses:
ALTER WAREHOUSE bi SET MIN_CLUSTER_COUNT=1
MAX_CLUSTER_COUNT=10 SCALING_POLICY='STANDARD';
-- 3) BigQuery: a BI Engine reservation for the dash.Why it wins. High-concurrency spikes are a different problem from slow queries — the fix is to (a) make most requests cache hits so they never touch compute, and (b) let the remainder fan out across auto-added clusters instead of queueing. The combination handles the Monday-morning herd without permanently over-provisioning a giant warehouse that sits idle the rest of the week.
№ 14Don't render a 50,000-row table widget
Don't render a 50,000-row table widget
Shopify — a "detail" tab is a raw table widget returning every transaction, so the warehouse ships 50k rows and the browser chokes rendering them.
SELECT * FROM transactions
WHERE ds >= :start; -- 50k+ rows to a table viz
-- huge result transfer, slow render, and no human
-- reads a 50,000-row on-screen table anyway.-- show the aggregate the chart actually needs:
SELECT category, SUM(amount) FROM transactions
WHERE ds >= :start GROUP BY category ORDER BY 2 DESC LIMIT 50;
-- row-level detail → a paginated drill-down or a
-- "download CSV" that runs an async export job.Why it wins. A visualization should return what a human can perceive — a few dozen bars, a ranked top-N, a trend line. Massive table widgets pay twice: a large result transfer from the warehouse and an expensive client-side render. Aggregate or top-N for the on-screen view, and route true row-level needs to pagination or an async export.
№ 15Cache with a TTL tied to the data-freshness SLA
Cache with a TTL tied to the data-freshness SLA
Pinterest — a dashboard fed by an hourly pipeline, but with caching off, so it recomputes continuously to show numbers that only change once an hour.
-- cache disabled / 0s TTL: the dashboard recomputes
-- on every interaction even though the source only
-- lands new data once per hour.-- cache TTL set to the pipeline cadence (e.g. 1h);
-- a scheduled "warm-up" runs the heavy queries right
-- AFTER each load so the first human always hits a
-- warm cache. Invalidate on load completion, not by
-- a guessed timer.Why it wins. There's no value in recomputing a number more often than its inputs change. Setting the cache TTL to the data-freshness SLA, and warming the cache immediately after each pipeline load, means viewers almost always hit a fresh cache and the warehouse runs the heavy query once per load instead of once per click. Best of all is event-driven invalidation keyed to load completion.
D · Correctness
A fast dashboard that's wrong is the worst kind.
№ 16Mean lies on skewed data — show median / percentiles
Mean lies on skewed data — show median / percentiles
DoorDash — an "average order value" KPI that a handful of catering whales drag 30% above what any typical customer ever spends.
SELECT AVG(order_value) AS "Typical order"
FROM orders; -- one $9,000 catering order
-- per 1,000 lifts the "average"
-- away from reality.SELECT APPROX_PERCENTILE(order_value,0.50) AS p50,
APPROX_PERCENTILE(order_value,0.90) AS p90,
AVG(order_value) AS mean
FROM orders; -- show p50 as "typical", and
-- the p50-vs-mean gap reveals skew.Why it wins. On the heavy-tailed distributions that dominate real business data, the mean is pulled toward the whales and misrepresents the typical case. Lead with the median, show a percentile spread, and consider a log scale for whale-heavy charts. The gap between mean and median is itself the skew signal. (Vocabulary on the Skew & Distributions page.)
№ 17Time zones & late data — define "today" and don't plot a half-loaded partition
Time zones & late data — define "today" and don't plot a half-loaded partition
Netflix — a global daily-active chart whose latest bar craters every morning, sparking a false-alarm Slack thread, because today's partition is only partly loaded and "day" is in UTC for a US-centric audience.
SELECT ds, COUNT(*) FROM events GROUP BY ds;
-- today's bar is partial (data still arriving) and
-- "ds" is UTC, so the curve dips every morning and
-- is shifted vs the users' local day.SELECT DATE(ts AT TIME ZONE 'America/Los_Angeles') AS day,
COUNT(*)
FROM events
WHERE ts < DATE_TRUNC('day', CURRENT_TIMESTAMP) -- exclude
GROUP BY 1; -- partial today
-- or mark the in-progress day as provisional in the viz.Why it wins. Two classic dashboard lies: plotting an incomplete current partition as if it were a finished day, and aggregating by an implicit UTC "day" that doesn't match how the business thinks about time. Excluding (or visibly flagging) the in-progress day kills the daily false-drop, and converting to a declared business time zone makes day-over-day comparisons honest.
№ 18Never pre-aggregate a ratio — store numerator and denominator
Never pre-aggregate a ratio — store numerator and denominator
Robinhood — a rollup stores conversion_rate per day, and a weekly tile averages the seven daily rates, producing a number that's mathematically wrong.
-- agg_daily.conv_rate = conversions/visits per day
SELECT AVG(conv_rate) AS weekly_rate -- ❌ a day with
FROM agg_daily -- 2 visits counts
WHERE week = :w; -- as much as one
-- with 2,000,000.-- store conversions and visits (both additive):
SELECT SUM(conversions) * 1.0 / SUM(visits) AS weekly_rate
FROM agg_daily
WHERE week = :w; -- ratio computed at read time
-- from rolled-up components.Why it wins. Ratios, rates and averages are not additive — you can't sum or average them across grains without weighting. The rule is to pre-aggregate only additive components (numerator and denominator, sums and counts) and compute the ratio at read time. Same family as storing sketches instead of distinct counts (№5): keep the building blocks, derive the metric.
№ 19Label the approximate & sampled panels
Label the approximate & sampled panels
Coinbase — finance pulls a "total settled volume" number off a dashboard tile that's quietly powered by APPROX_COUNT_DISTINCT and a 1% sample, and reconciliation later disagrees by 1.5%.
-- tile shows "Settled volume: 14,237,104" but it's
-- really a sampled/approximate estimate. Someone
-- treats it as the book of record.-- label it: "≈ 14.2M (approx, ±2%)"
-- approximate/sampled → exploration & glance KPIs
-- exact, un-sampled query → the path used for
-- billing, finance, compliance and any export.
-- one click from the tile to the exact drill-down.Why it wins. Approximation (№10) and sampling are the right call for speed — but only if consumers know which numbers are estimates. Visibly labelling approximate/sampled panels and routing finance-grade questions to an exact, un-sampled path preserves both speed and trust. The failure mode isn't the approximation; it's an estimate masquerading as the source of truth.