Skip to content
▸ ANALYTICS · the serving layer · petabyte warehouse → sub-second dashboard

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.

The mental model — a dashboard is a latency budget. Every panel that loads in 300 ms instead of 30 s did one of four things: it pre-computed the answer before anyone asked, it served it from something built for interactive analytics, it restrained what it asked for, or — the one people forget — it was verified, so the speed didn't buy a fast wrong answer.
Animated title card for Analytics, Dashboard and Optimization — a dashboard is a latency budget, spent four ways: pre-compute, serve, restrain, verify.
Lever № 1

Pre-compute

Compute the expensive answers before the user asks. Rollups, one-big-table marts, aggregate awareness, a semantic layer.

Lever № 2

Serve

Serve from something optimised for interactive analytics — materialised views, result cache, extracts, an OLAP tier.

Lever № 3

Restrain

Ask the backend for less data and fire fewer queries. Bounded windows, filter-aligned layout, lazy tiles.

Lever № 4

Verify

Make sure the optimisation hasn't made the dashboard confidently wrong. Freshness, semantics, skew, ratios, time zones.

Layer 1
Raw data
18.4 s
full scan
Layer 2
Semantic / aggregation
2.1 s
rollup read
Layer 3
Serving layer
180 ms
cache / OLAP
Layer 4
Dashboard
65 ms
browser render
Layer 5
Decision
the actual point

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

§ 01 · The diagnostic

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.

62 / 100
Dashboard health · exec revenue overview
2 correctness warnings · 3 performance warnings
P50 load1.2 sacceptable
P95 load6.8 stail is the problem
Queries per open27query explosion
Warehouse scan1.8 TBno aggregate
Cache hit rate38 %too low
Freshness47 minwithin SLA
Concurrent viewers312Monday 9am peak
Correctness warnings2ratio + partial day

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.

§ 02 · The centrepiece

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.

✗ Before — 22 widgets, all-time default18.2 s

Global Commerce Performance

All Time · 2018–Today
Date Range
All Time
Region
(All)
Channel
(All)
Device
(All)
Revenue$4.82B
Orders38.4M
Customers11.2M
AOV$125.52
Conversion3.81%
Sessions1.01B
Units Sold61.7M
Refund Rate6.2%

Revenue by Country (All Time)

United States — 18.7% United Kingdom — 7.9% Germany — 6.8% France — 5.9% Canada — 5.1% Australia — 4.8% Japan — 4.6% Brazil — 4.1% India — 3.9% Netherlands — 3.2% Spain — 3.1% Italy — 2.8% Mexico — 2.6% South Korea — 2.3% Sweden — 1.9% Singapore — 1.7% Other — 20.6%
United States18.7% United Kingdom7.9% Germany6.8% France5.9% Canada5.1% Australia4.8% Japan4.6% Brazil4.1% India3.9% Netherlands3.2% Spain3.1% Italy2.8% Mexico2.6% South Korea2.3% Sweden1.9% Singapore1.7% Other20.6%

Orders vs Marketing Spend (All Time)

OrdersMarketing Spend
1.0M 800K 600K 400K 200K 0 $100M $80M $60M $40M $20M $0 2018 2019 2020 2021 2022 2023 2024 2025 Orders Marketing Spend

Average Customer Spend (All Time)

$142
vs prior period  +6.3% ▲

Revenue Over Time (Daily, All Time)

$25M $20M $15M $10M $5M $0 2018 2019 2020 2021 2022 2023 2024 2025 Latest Day (Partial) $2.1M

Revenue by Category and Month (All Time)

Apparel Footwear Accessories Beauty Home Electronics Sports Toys Other
Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
$0 $20M $40M $60M $80M $100M+

Order Detail (All Time)

Order IDDateCustomerCountryRegionChannelDeviceCategoryProductUnitsUnit PriceDiscountRevenueRefundNet Revenue
1000000012025-05-15Emma JohnsonUnited StatesNorth AmericaOnlineDesktopApparelPerformance Hoodie · Black / L1$79.99$0.00$79.99$0.00$79.99
1000000022025-05-15Liam WilliamsUnited KingdomEuropeMobile AppMobileFootwearTrail Runner · Grey / 91$129.99$13.00$116.99$0.00$116.99
1000000032025-05-15Olivia BrownGermanyEuropeOnlineDesktopAccessoriesLeather Belt · Brown / 341$49.99$5.00$44.99$0.00$44.99
1000000042025-05-15Noah DavisCanadaNorth AmericaMobile AppMobileBeautyHydrating Serum 30ml2$34.99$0.00$69.98$0.00$69.98
1000000052025-05-15Ava MillerAustraliaAPACOnlineDesktopHomeCeramic Mug · White1$19.99$2.00$17.99$0.00$17.99
Showing 1–50 of 50,284 rows
  • 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
Load18.2 s
Queries31
Rows2.3 B
Bytes1.4 TB
Render3.2 s
✓ After — 5 headline KPIs, 28-day default780 ms

NorthStar Commerce — Executive Pulse

Last 28 Days
Updated 10:02 AM· Today 40% complete·provisional
Revenue$128.4M▲ 6.2% vs prior 28 days
Orders1.04M▲ 4.8% vs prior 28 days
Conversion4.12%▲ 0.31 pp vs prior 28 days
AOV$123.46▼ 1.3% vs prior 28 days
Active Customers742K▲ 8.7% vs prior 28 days

Revenue over time

Today 40% complete · provisional
Revenue (current 28 days)Revenue (prior 28 days)
$10M $8M $6M $4M $2M $0 Apr 25 Apr 30 May 5 May 10 May 15 May 20 May 25 May 30 Jun 3 Jun 7 Jun 11 Jun 15 Jun 19 Jun 23

What's driving the change?

vs prior 28 days

US West+$2.4M Enterprise+$1.8M Organic+$1.1M Mobile App+$0.7M Refunds−$0.6M

Showing top drivers of $5.4M increase

Conversion funnel

vs prior 28 days

Sessions32.4M+2.6% Product Views8.7M+3.4% Cart2.1M+1.2% Checkout0.98M−3.2% Orders1.04M+4.8%
⊘ Largest deterioration: Cart → Checkout  −3.2 pp

Top markets by revenue

vs prior 28 days

United States$72.6M ▲6.1% United Kingdom$16.3M ▲5.3% Canada$9.1M ▲7.8% Germany$7.6M ▲3.9% India$6.2M ▲9.4% Other$16.6M ▲4.6%
  • 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
Load780 ms
Queries6
Rows90 K
Bytes24 MB
Render110 ms

Find the optimisations

0 / 10 found
Pick a chip to see what changed between the two dashboards — and why it matters.

Illustrative 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.

§ 03 · Request anatomy

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.

THE ANTI-PATTERN — EVERY WIDGET IS ITS OWN ROUND TRIP Dashboard 1 page open KPI 1 → queryKPI 2 → query Trend → queryTable → query Map → queryFilter values → query Comparison → queryTooltip → query 8 queries × 500 users = 4,000 requests Warehouse queue depth climbing each query may be individually fast — the topology is what fails
Anti-pattern · request topology — illustrative
THE FIX — COLLAPSE THE TOPOLOGY, NOT JUST THE SQL 500 viewers Monday, 9:00 Result cache ~92% served here 6 query shapes deduplicated Aggregate mart daily grain Warehouse scheduled build only 4,000 requests → 6 distinct executions between refreshes Optimise the request topology, not merely the individual SQL statement.
Optimised · request topology — illustrative
§ 04 · Where the time goes

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

Browser / network250 ms
BI rendering900 ms
Query queue2200 ms
Warehouse execution3500 ms
Result transfer850 ms
Other300 ms
Total~8.0 s

✓ After — ~460 ms total

Browser / network180 ms
BI rendering180 ms
Cache lookup30 ms
Warehouse0 ms
Transfer70 ms
Total~460 ms

Illustrative latency breakdown · bars scale within each panel

"Slow dashboard" does not automatically mean "slow SQL." Before touching a query, find out whether the time is going to the queue (a concurrency problem), the render (too many marks), the transfer (too many rows crossing the wire), or the execution itself. Optimising a 3.5-second query to 2 seconds fixes less than a third of the wait; moving it behind a cache removes it entirely.
§ 05 · Trade-off simulator

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.

Data volume
Concurrent users
Connection
Aggregation
Result cache
Default range
Queries per open
Rows scanned
Latency class
Warehouse load
Freshness
Correctness risk
Adjust the controls to see the trade-off.

Qualitative model for teaching trade-offs · not a pricing or benchmark tool

§ 06 · Tool-specific patterns

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.

Optimisation levers by problem and BI platform
ProblemTableauLookerPower BIWarehouse / 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

§ 07 · Dashboard-side engineering

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".

01 · Intent

Does the dashboard answer a clear business question?

  • What decision does it support?
  • Who opens it, and how often?
  • What belongs on first paint?
02 · Information

Are the right KPIs and dimensions shown?

  • Is each metric attached to a decision?
  • Are similar metrics distinguishable?
  • Is anything here purely decorative?
03 · Computation

Where are the calculations executed?

  • Warehouse
  • Semantic layer
  • BI engine
  • Browser / client
04 · Interaction

What happens when the user touches it?

  • Filter, drill, hover
  • Tab switch, parameter change
  • How much refires each time?
05 · Freshness

Is the data current, complete and trusted?

  • What is the SLA?
  • Is the partial period labelled?
  • Does the cache respect it?
06 · Performance

What does it cost?

  • First paint
  • Interaction
  • Concurrency
  • Rendering vs execution

7.2Push computation to the right layer

Push computation to the right layer — which is not always the backend. "Move it to the warehouse" is a reflex, not an answer. Sorting twelve regions that are already on screen does not belong in a warehouse query, and a governed lifetime-value metric does not belong in a workbook. The useful rule is about distance and size: the closer computation gets to the user, the smaller the dataset it is allowed to touch.
Raw data10B rowsevent-level, immutable, never queried by a dashboard
Warehouse100M rowsjoins, distinct counts, large-window aggregation
Semantic layer100K rowsgoverned measures, dimensional logic, access rules
BI engine1K rowspresentation logic, interaction-dependent calculation
Browser50 markspurely visual — sorting, formatting, highlighting
Ten billion rows should never reach a browser so that JavaScript can "finish the calculation." Whenever a dashboard feels slow on a machine with a fast warehouse behind it, check whether the last mile is doing arithmetic it was handed rather than arithmetic it asked for.

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
and in the BI tool when
  • 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
examples
  • 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

Good for
  • genuinely fresh operational data
  • governed warehouse queries
  • small, selective interactions
  • cases where stale data is unacceptable
Risks
  • 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

Good for
  • 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
Benefits
  • columnar optimised storage
  • interaction largely decoupled from warehouse latency
  • reduced warehouse query cost
  • fast local filtering and aggregation
Risks
  • 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

Freshness is a requirement, not a religion. Ask what decision changes inside the freshness window. If nobody acts within the minute, minute-level freshness is a cost with no buyer.

7.4Your dashboard is doing too much work

Four ways a workbook quietly becomes the expensive part of the system.

Trap 01

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.

COUNTD([Customer ID]) // current period COUNTD([Customer ID]) // prior period COUNTD([Customer ID]) // by region COUNTD([Customer ID]) // by channel …

The same expensive calculation may execute repeatedly across worksheets, and distinct counts are among the least shareable operations a warehouse can be asked for.

BetterBuild governed aggregates upstream, and store sketches (HLL or equivalent) where approximate distinct counts are acceptable — they stay mergeable across rollups instead of forcing a rescan.
Trap 02

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.

{ FIXED [Customer ID] : SUM([Revenue]) } // …then wrapped in another calculation, // filtered, and duplicated across six worksheets

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.

BetterAsk whether the expression represents a reusable business entity or metric. If it does — customer lifetime revenue, account tenure, first-order date — model it upstream once instead of re-deriving it per workbook.
Trap 03

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.

BetterReduce the result set first. If the calculation needs a full-population denominator, compute that upstream and return it alongside — do not ship the population to the client so it can divide.
Trap 04

Duplicated calculated fields

Six fields implementing approximately the same idea:

Revenue Growth Revenue Growth % Revenue YoY YoY Revenue Change Rev Δ Revenue Variance

This produces maintenance risk, inconsistent definitions, redundant compute and — worst — users who cannot tell which number is the real one.

BetterOne governed metric, defined once, with a name that survives a meeting. If two genuinely differ, the difference belongs in the name and the description, not in the reader's head.

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.

✗ Before — 22 tiles, 0 hierarchyhigh cognitive load

Executive Business Overview

All Time
Revenue$128.4M
Orders1.04M
Units2.73M
Customers742K
Sessions8.6M
AOV$123.46
Conversion4.12%
Gross Margin36.7%
Net Revenue$127.9M
GMV$134.2M
Sales$128.1M
Margin18.4%
Discount$8.2M
Returns$4.7M
Refunds$3.1M
CAC$24.31
ROAS3.21x
CTR1.48%
CPC$0.68
NPS42
Repeat Rate23.6%
Inventory18.4K
⚠ What should the executive actually act on?

The same business, six tiles, grouped by what they tell you — and every one carries a comparison so the number can be judged.

✓ After — 6 tiles, grouped, actionableclear hierarchy

Executive Business Overview

Last 28 Days
Updated 10:02 AM
Outcome
Revenue$128.4M▲ 6.2% vs prior 28 days
Margin18.4%▲ 1.3 pp vs prior 28 days
Orders1.04M▲ 4.8% vs prior 28 days
Customer
Conversion4.12%▲ 0.31 pp vs prior 28 days
Repeat Rate23.6%▲ 2.1 pp vs prior 28 days
Health
Refund Rate2.4%▼ 0.4 pp vs prior 28 days
18 secondary metrics available on drill-down →

Illustrative scenario — not benchmark data

A KPI without a decision attached to it is decoration. Four of the tiles above measure revenue — Revenue, Net Revenue, GMV and Sales — and nothing on screen says how they differ. That is the next module.

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.

Revenue
$128.4M

Settled transactions, excluding tax.

Net Sales
$127.9M

Revenue after refunds.

GMV
$134.2M

Gross order value, before refunds.

BetterIf the distinctions matter, make them visible where the number is: a one-line description on the tile, a tooltip, the semantic-layer definition, and a lineage link where it helps. Otherwise readers treat similar names as identical measures — and they are right to, because nothing on screen told them otherwise.

7.7Four metrics, four different moments

One dashboard strip. Every tile looks equally current — that is the whole problem. Flip the switch.

Executive stripas shipped
Revenue$128.4MUpdated
Yesterday 03:00
Orders1.04MUpdated
8 min ago
Conversion4.12%Updated
42 min ago
Inventory18.4KUpdated
3 days ago

Illustrative scenario — not benchmark data

Freshness is part of metric semantics. Revenue is 31 hours old against a one-hour SLA and inventory is three days old — but as shipped, the strip presents both alongside eight-minute-old orders as though they describe the same instant. A number without an as-of is a number the reader will assume is now.

7.8Dashboards accumulate archaeology

A dashboard estate is a codebase nobody refactors. An honest inventory usually looks something like this.

4,000dashboards
1,480not opened in 90 days
720duplicate titles
340use retired data sources
210reference deprecated metrics
96no successful refresh in 7 days
34actively used by executives

Illustrative estate

ActiveLow usageOwner reviewDeprecateArchive

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.

✗ Before — 13 controls before insight8 filter-domain queries
Region Country State City Product Category SKU Customer Channel Campaign Device Date Segment
Cognitive choices13
Possible filter statesEnormous
Cache reuseLow
First-paint filter queries8
  • 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.

✓ After — intent first2 filter-domain queries
Date Region Channel Advanced filters ▸
Acme Retail — US
Acme Logistics — UK
Acme Europe GmbH
Acme Consumer APAC
Cognitive choices3
Possible filter statesManageable
Cache reuseHigh
First-paint filter queries2
  • 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

If users cannot scan the domain, do not preload the domain. A customer dropdown over 11.2 million values must fetch and render every distinct value before it is usable — and then presents a list no human can read. Search, or narrow hierarchically (Region → Account → Customer), and resolve on selection. The same applies to SKU, transaction ID, session ID, email and device ID.

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.

✗ Before — 24 worksheets, everything global35 requests on open

Executive Dashboard

24 worksheets · all filters global
First paint
24 visualisation queries
8 filter-domain queries
3 parameter queries
Total 35 requests
User sets Region = West
31 worksheets refresh
6 filter domains reload
0 cached or reused
Total 37 new requests

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.

✓ After — 6 on first paint, scoped filters9 requests on open

Optimised Dashboard

6 first-paint visuals · 3 lazy tabs · 3 scoped filters
OverviewTrendsCustomersDetails (lazy)
First paint
6 visualisation queries
2 filter-domain queries
1 parameter query
Total 9 requests
User sets Region = West
4 chart updates
1 cross-filter state update
0 KPI queries — served from cache
Total 5 operations

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

Product Revenue
Conversion
Units Sold
Returns
Margin
Corporate Revenuewhy did this refresh?
Employee Countwhy did this refresh?
Inventory Agewhy did this refresh?
Customer NPSwhy did this refresh?

✓ Scoped filter — 5 worksheets refresh

Product Revenue
Conversion
Units Sold
Returns
Margin
Corporate Revenueuntouched
Employee Countuntouched
Inventory Ageuntouched
Customer NPSuntouched
13 unnecessary refreshes avoided. Employee count does not have a product category. Refreshing it anyway costs a query and teaches the user that the filter means less than it says — the two failure modes, cost and trust, arriving together.

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.

Warehouse query210 ms
Rows returned642 K
Marks rendered218 K
Table calculations9
Client memory1.3 GB
Browser render3.4 s

Map

220,000 marks
1,200 marks (clustered)
cluster / aggregate by zoom

Scatter plot

1.6M points
12K points (binned)
sample, bin, or draw density

Detail table

50,000 rows
Top 100 + search
paginate, Top-N, search

Time series

5M observations
365 daily points
aggregate to the visible grain

Illustrative scenario — not benchmark data

The arithmetic behind all four: a chart is only so many pixels wide.

5,000,000observations
÷
1,200pixel columns
=
4,167observations per pixel
1 yeardaily grain365 marks
30 dayshourly grain720 marks
24 hoursminute grain1,440 marks
1 hoursecond grain, where justified3,600 marks
Do not send more visual detail than the human eye can resolve. Roll the grain with the zoom level and the picture is identical — the same trend, the same conclusion, a fraction of the marks. Query grain should respect visual resolution.

7.12Tooltip abuse

✗ Hovering one point
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
✓ Hovering one point
$4.8M
Revenue · 18 June
▲ 6.2% vs prior period
View drivers →
A tooltip is secondary context, not a second dashboard. If sixteen fields genuinely matter at that point, the answer is a drill-through, not a hover.

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.

Pipeline completesCache invalidatesDashboard sees fresh data
Refresh frequency should follow data-change frequency, not anxiety. Event-driven invalidation beats a timer: the dashboard updates when there is something to update.

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.

Data stale

The pipeline has not produced new data. Everything downstream is correct — and correctly out of date. Fix the pipeline.

Cache stale

New data exists, but a cached result has not been invalidated. The system is serving a correct answer to an old question. Fix invalidation.

Dashboard stale

Fresh data, valid cache — but this browser session has not re-fetched. Fix the refresh or reload behaviour.

Semantic stale

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

Browser event20 ms
BI calculation170 ms
Generate query40 ms
Network80 ms
Warehouse queue1800 ms
Warehouse execution900 ms
Transfer260 ms
Client render1100 ms
Total~4.4 s

Illustrative breakdown

What would you fix first? The warehouse executes for 900 ms — a fifth of the wait. The queue in front of it is twice that, and the client render is longer than the execution. A candidate who answers "optimise the SQL" has just optimised the fifth-largest bar. Queueing is a concurrency and caching problem; render time is a marks problem. Neither is fixed by a better query plan.

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.

Customer lifetime revenue
Highlight a value when it falls below target
30-day distinct active users
Sort the 12 regions currently displayed
Currency conversion using governed daily FX rates
Revenue vs the prior 28 days, on the current filter
Percent-of-total across a 4 million row result
Format a number as compact currency ($1.2M)
The closer computation gets to the user, the smaller the dataset should be. Every one of these answers falls out of that sentence plus one question: is anyone else going to need this number?

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.

Scenario ATableau · executive sales

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.

Scenario BLooker · product analytics

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.

Scenario CPower BI · operations

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.

S
Semantics

Are we showing the right metrics, defined once?

C
Computation

Where is each calculation actually executed?

O
Operations

How many queries and interactions are triggered?

P
Presentation

Are we rendering more than anyone can read?

E
Expiry

Is the data, the cache and the dashboard fresh?

Optimise only after identifying which dimension is failing. The framework earns its keep by stopping the reflex answer — most people reach for computation when the problem is operations or expiry.

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.

§ 08 · Visualisation engineering

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

which is bigger, slice 4 or slice 11?

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"

Alpha Bravo Cobalt Delta Echo 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

left axis 0–50 right axis 900–1100 "they clearly track each other"

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

signups · index 100 revenue · index 100

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

$142 AVERAGE ORDER VALUE a figure almost no customer spends

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

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%

96% 94% "uptime is falling off a cliff"

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

100% 0% −1.4 pp — real, but not a cliff

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

50,000 rows · 3.2 s to render · nobody scrolls past 40

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

search ⌕ export ↓ 90 K rows scanned · 110 ms · the long tail on demand

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.

§ 09 · Encoding choice

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.

Trend over time?
→ Line
Continuous time axis; slope carries the meaning.
Rank categories?
→ Horizontal bar
Sorted, with room for long labels.
Distribution?
→ Histogram / box plot
Where skew and outliers actually live.
Relationship?
→ Scatter
Two measures; add a trend line only if justified.
Part-to-whole?
→ Stacked bar
Pie only for a very small, very simple split.
Flow between states?
→ Sankey
Only when the flow relationship is the point.
Geography?
→ Map
Only if location itself drives the decision.
Exact lookup?
→ Table
Bounded, sorted, searchable — not a data dump.
Single KPI?
→ KPI + context
A number with no comparison cannot be judged.
§ 10 · Information hierarchy

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.

Layer 1 · first screen
What happened?
RevenueConversionActive usersOrders
Layer 2 · one click in
Why?
TrendVarianceSegmentsContributorsAnomalies
Layer 3 · on demand
What do I inspect?
CustomerRegionProductCampaignTransaction
§ 11 · When it hurts

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

HelpsFast repeated exploration, isolated from warehouse contention.
HurtsWhen second-level freshness is genuinely required, or the dataset will not fit.

Pre-aggregation

HelpsRepeated, known dimensions and grains — the bulk of dashboard traffic.
HurtsAd-hoc analysis across dimensions nobody modelled; a new question needs a new cube.

Cache

HelpsMany viewers asking the same question between refreshes.
HurtsPoor invalidation serves stale answers with total confidence.

OLAP serving layer

HelpsHigh-concurrency interactive analytics at sub-second latency.
HurtsAnother system to operate, load, monitor and reconcile.

Approximation

HelpsExploration and glanceable KPIs, where ±1% changes nothing.
HurtsBilling, compliance and financial reconciliation — never approximate the number that goes to the ledger.

Semantic layer

HelpsOne definition, visible lineage, and a change you can scope before shipping.
HurtsBecomes a bottleneck if the path to add a metric is slower than building around it.
§ 12 · The 2026 shift

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.

User question "why did conversion fall?" AI analyst plans the query Semantic model metric definitions permissions · lineage ▲ THE LOAD-BEARING PIECE Aggregates warehouse / OLAP Answer + visualisation Garbage semantics → confident garbage explanation. the model will narrate the wrong number as fluently as the right one
Conversational analytics · the semantic layer is the control point
The interview lesson: AI makes the semantic layer more important, not less. A human analyst who gets a strange number usually pauses and asks why. A language model will produce a fluent, plausible, well-structured explanation of a wrong metric without hesitating. Every governance control you skipped — one definition, row-level permissions, lineage, freshness, an audit trail — becomes load-bearing the moment answers are generated rather than clicked.

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.

Tableau
Tableau Agent · Tableau Pulse
AI-supported insight exploration, natural-language assistance and generated insight summaries layered over governed data sources.
Looker
Gemini-powered conversational analytics
Natural-language questions answered against the LookML semantic model, with generated charts and exploration paths.
Power BI
Copilot
Report and page generation assistance, summarisation, natural-language analysis and DAX authoring help over the semantic model.
Sigma
AI-assisted exploration
Natural-language analysis inside workbooks, with an emphasis on showing the analytical path that produced the answer.

What that surface actually feels like

A mocked exchange — deterministic sample content, no model call. The detail worth noticing is the last line.

Analytics assistantmock
You

Why did revenue fall yesterday?

Assistant

Revenue decreased 8.4% versus the prior day.

Primary contributors:

  • West region — −$1.2M
  • Enterprise renewals — −$740K
  • Mobile conversion — −0.8 pp

Yesterday's data is 96% complete.

ExplainShow driversCompare 28 daysOpen SQL
Read the last line again. "Yesterday's data is 96% complete" is the most important sentence in that answer, and it is the one a naive implementation omits. An assistant that reports an 8.4% decline without disclosing that the day is still loading has invented a crisis out of a partial partition — and it will defend the number if you ask it to.
§ 13 · The whiteboard

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.

Sources apps · events · SaaS Stream / batch ingest · CDC Warehouse lakehouse · raw + curated Gold aggregates + semantic layer metrics defined once Cache / MV / OLAP the serving tier BI / dashboard filter · click · drill AI assistant ask · explain · act metric contract — the dashboard never redefines a metric same contract governs generated answers, plus permissions & lineage data path metadata · metrics · permissions
The 60-second whiteboard — every box earns its place
§ 14 · Diagnose it

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.

Easy01

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.

Easy02

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.

Easy03

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.

Medium04

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.

Medium05

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.

Medium06

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.

Hard07

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.

Hard08

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.

Hard09

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.

Hard10

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.

§ 15 · The 60-second articulation

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.

01

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.

02

Pre-compute

Move expensive work out of view time: rollups, a one-big-table mart, aggregate awareness, and governed metrics defined once.

03

Accelerate & restrain

Materialised views, cache, extracts, an OLAP tier where justified — and bounded windows, filter-aligned layout, lazy tiles to ask for less.

04

Verify

Freshness, metric semantics, skew, ratios, time zones, approximation. Speed that buys a wrong answer is a regression.

"First I'd find out where the latency actually is — queue, execution, transfer or render — because a slow dashboard usually isn't slow SQL. Then I'd work four levers. Pre-compute: does this panel scan raw events or a rollup? Aggregate it, mart it, and put a semantic layer over it so each metric is defined once and aggregate-aware. Serve: materialised views and result cache for the repeated shapes, and a Druid/Pinot/ClickHouse tier if it's a genuinely interactive surface at high concurrency. Restrain: a bounded default window, partitioning aligned to the filters, lazy tiles, fewer queries per open. And verify throughout — median not mean on skew, additive components not pre-baked ratios, sketches not distinct counts, a provisional flag on the partial day, and a clear line between approximate and exact. The goal isn't a fast dashboard. It's a fast dashboard people can trust."

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.

The three engineering pillars together → Design is the schema the data lives in. Performance is making one petabyte query over it cheap. Analytics is turning the whole thing into a fast, honest dashboard. Practise the query mechanics in Practice · Q&A, then take the Dashboarding & BI Skill Check.
§ 16 · Reference

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.

On the company names below. Each scenario is written in the style of a workload that company is publicly known for — a music-streaming firehose, a marketplace's booking funnel — because concrete scenarios are easier to reason about than abstract ones. They are interview-style scenarios inspired by that kind of workload, not claims about any company's internal architecture.

A · Model

Pre-compute the answer before the question — rollups, marts, aggregate awareness, semantics.

№ 01Dashboards read rollup tables, never raw events
rollup / aggregate tableall warehouses
№ 01

Dashboards read rollup tables, never raw events

rollup / aggregate tableall warehouses

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.

✗ Anti-pattern — view-time scan of raw
-- 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;
✓ Optimized — read the daily rollup
-- 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.

Impact: billions of rows scanned per load → ~90 rows; warehouse spend on that tile drops by 3–4 orders of magnitude.
№ 02Pre-join a one-big-table mart so the dashboard never joins
OBT · denormalizationall warehouses
№ 02

Pre-join a one-big-table mart so the dashboard never joins

OBT · denormalizationall warehouses

Airbnb — a bookings dashboard whose every filter triggers a 6-table star-schema join at query time, multiplied across panels.

✗ Anti-pattern — join the star at view time
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
✓ Optimized — read one wide mart
-- 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.

Impact: N panels × a 6-way join per refresh → a single wide scan; the most common cause of a "slow dashboard, idle warehouse… then a spike."
№ 03Aggregate awareness — route each query to the smallest table that answers it
aggregate navigationLooker · Cube · dbt
№ 03

Aggregate awareness — route each query to the smallest table that answers it

aggregate navigationLooker · Cube · dbt

Uber — the same metric is asked at city-month, country-week and global-day granularity; one table can't be optimal for all three.

✗ Anti-pattern — one grain for everything
-- 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.
✓ Optimized — let the layer pick the rollup
# 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_month

Why 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.

Impact: coarse questions hit kilobyte tables, fine questions still work — without analysts hand-picking the source.
№ 04A semantic layer — define each metric once
metrics / semantic layerdbt · LookML · Cube
№ 04

A semantic layer — define each metric once

metrics / semantic layerdbt · LookML · Cube

Meta — five teams each hand-write "active user," with subtly different filters, and three dashboards disagree by 4% in the same all-hands.

✗ Anti-pattern — metric re-defined per dashboard
-- 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)
✓ Optimized — one governed definition
# 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.

Impact: dashboards stop disagreeing; one change to the definition propagates everywhere; "whose number is right?" stops being a meeting.
№ 05Store sketches, not counts — so distinct metrics stay roll-up-able
additivity · HLL sketchSnowflake · BQ · Druid
№ 05

Store sketches, not counts — so distinct metrics stay roll-up-able

additivity · HLL sketchSnowflake · BQ · Druid

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.

✗ Anti-pattern — sum of daily distincts (wrong)
-- 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×
✓ Optimized — store a mergeable HLL sketch
-- 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.)

Impact: distinct-user metrics become rollup-safe at every grain — no raw rescans, no silent over-count.

B · Serve

When pre-aggregation isn't enough, change the engine — MVs, cache, extracts, OLAP.

№ 06Materialized views + result cache for repeated queries
materialized view · cacheSnowflake · BQ · Redshift
№ 06

Materialized views + result cache for repeated queries

materialized view · cacheSnowflake · BQ · Redshift

Salesforce — a heavy aggregate behind a popular tile, recomputed from scratch for every viewer although the underlying data changes hourly.

✗ Anti-pattern — recompute per view
-- complex GROUP BY over a large base table,
-- executed fresh on every dashboard open even
-- though inputs only change once an hour.
✓ Optimized — materialize + let cache serve
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.

Impact: the first viewer pays; the next hundred are served from MV + cache for ~free until the next data load.
№ 07Put a real-time OLAP engine in front for sub-second @ high concurrency
Druid · Pinot · ClickHouseserving store
№ 07

Put a real-time OLAP engine in front for sub-second @ high concurrency

Druid · Pinot · ClickHouseserving store

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.

✗ Anti-pattern — interactive app on a batch warehouse
-- every user request fires a warehouse query;
-- queue depth explodes at concurrency, p99 is
-- seconds, and freshness lags the batch job.
✓ Optimized — pre-aggregated columnar OLAP tier
-- 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.

Impact: seconds-and-queueing → sub-200 ms at thousands of QPS, minute-fresh — latency a warehouse fundamentally can't hit at that concurrency.
№ 08Extract vs live connection — cache the data next to the BI tool
extract · import modeTableau · Power BI
№ 08

Extract vs live connection — cache the data next to the BI tool

extract · import modeTableau · Power BI

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.

✗ Anti-pattern — live query on every interaction
-- LIVE connection: each filter/drill = a new
-- warehouse query. Interactive latency is at the
-- mercy of warehouse load; cost scales with clicks.
✓ Optimized — columnar extract as a cache
-- 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 *).

Impact: interaction latency decouples from warehouse load; the per-click warehouse bill goes to roughly zero.
№ 09Refresh extracts and MVs incrementally, not full-rebuild
incremental refreshPower BI · dbt · MVs
№ 09

Refresh extracts and MVs incrementally, not full-rebuild

incremental refreshPower BI · dbt · MVs

Datadog — a 2-year extract behind a usage dashboard, fully rebuilt every hour because "refresh" was left on the default.

✗ Anti-pattern — full refresh of all history
-- hourly job re-reads and re-loads 730 days of
-- data to pick up the last hour of changes.
✓ Optimized — only the changed window
-- 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.

Impact: an hourly refresh that read 730 days now reads ~3; refresh time and cost stop growing with the table.
№ 10Approximate counters for the headline tiles
approximate analyticsall warehouses
№ 10

Approximate counters for the headline tiles

approximate analyticsall warehouses

TikTok — a real-time "unique viewers" big-number tile recomputing an exact COUNT(DISTINCT) over the live firehose every few seconds.

✗ Anti-pattern — exact distinct for a glanceable KPI
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.
✓ Optimized — approximate for the headline
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.

Impact: the most-viewed tiles get cheap and fast; exactness is spent only where it actually matters.

C · Restrain

The cheapest query is the one the dashboard never fires.

№ 11Bounded default window + lazy-loaded tiles
default window · lazy loadBI layer
№ 11

Bounded default window + lazy-loaded tiles

default window · lazy loadBI layer

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.

✗ Anti-pattern — all-time, all-at-once
-- 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.
✓ Optimized — bounded default + on-demand
-- 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.

Impact: queries per open drop from 30 to ~5, each scanning a recent slice instead of all history.
№ 12Align partition & cluster keys to the dashboard's filters
filter-aligned layoutall warehouses
№ 12

Align partition & cluster keys to the dashboard's filters

filter-aligned layoutall warehouses

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.

✗ Anti-pattern — layout ignores the filters
-- 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.
✓ Optimized — cluster on the hot 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.

Impact: the most-used filter goes from "scan the whole date range" to "skip to the matching files."
№ 13Survive concurrency with caching + elastic warehouses
concurrency · autoscaleSnowflake · BQ
№ 13

Survive concurrency with caching + elastic warehouses

concurrency · autoscaleSnowflake · BQ

Atlassian — Monday 9am, 500 people open the same exec dashboard in five minutes; the warehouse queues and everyone watches spinners.

✗ Anti-pattern — 500 identical live queries
-- single-cluster warehouse; 500 concurrent runs of
-- the same uncached aggregate queue behind each
-- other → p99 measured in minutes.
✓ Optimized — serve from cache, scale out the rest
-- 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.

Impact: the herd is mostly cache hits; the rest scale out and back in — interactive latency holds, idle cost doesn't.
№ 14Don't render a 50,000-row table widget
viz cardinalityBI layer
№ 14

Don't render a 50,000-row table widget

viz cardinalityBI layer

Shopify — a "detail" tab is a raw table widget returning every transaction, so the warehouse ships 50k rows and the browser chokes rendering them.

✗ Anti-pattern — dump everything into a grid
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.
✓ Optimized — aggregate, top-N, paginate, export
-- 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.

Impact: result size drops from tens of thousands of rows to dozens; the panel renders instantly.
№ 15Cache with a TTL tied to the data-freshness SLA
caching · warm-upBI layer
№ 15

Cache with a TTL tied to the data-freshness SLA

caching · warm-upBI layer

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.

✗ Anti-pattern — recompute faster than data changes
-- cache disabled / 0s TTL: the dashboard recomputes
-- on every interaction even though the source only
-- lands new data once per hour.
✓ Optimized — TTL = freshness, scheduled warm-up
-- 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.

Impact: the expensive query runs ~once per data load, not once per interaction; every viewer still sees current numbers.

D · Correctness

A fast dashboard that's wrong is the worst kind.

№ 16Mean lies on skewed data — show median / percentiles
distributionanalytics
№ 16

Mean lies on skewed data — show median / percentiles

distributionanalytics

DoorDash — an "average order value" KPI that a handful of catering whales drag 30% above what any typical customer ever spends.

✗ Anti-pattern — a single mean on a long tail
SELECT AVG(order_value) AS "Typical order"
FROM   orders;        -- one $9,000 catering order
                      -- per 1,000 lifts the "average"
                      -- away from reality.
✓ Optimized — median + percentile spread
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.)

Impact: the KPI reflects the typical customer instead of being silently governed by the top 0.1%.
№ 17Time zones & late data — define "today" and don't plot a half-loaded partition
as-of correctnessanalytics
№ 17

Time zones & late data — define "today" and don't plot a half-loaded partition

as-of correctnessanalytics

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.

✗ Anti-pattern — naive latest day, ambiguous TZ
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.
✓ Optimized — complete days, explicit TZ
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.

Impact: no more morning false-alarms; day boundaries match the business, and trends are comparable.
№ 18Never pre-aggregate a ratio — store numerator and denominator
non-additive metricsanalytics
№ 18

Never pre-aggregate a ratio — store numerator and denominator

non-additive metricsanalytics

Robinhood — a rollup stores conversion_rate per day, and a weekly tile averages the seven daily rates, producing a number that's mathematically wrong.

✗ Anti-pattern — average of daily rates
-- 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.
✓ Optimized — keep the additive parts
-- 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.

Impact: rates and averages roll up correctly to any grain; the weekly number stops being a meaningless average-of-averages.
№ 19Label the approximate & sampled panels
disclosure · trustanalytics
№ 19

Label the approximate & sampled panels

disclosure · trustanalytics

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%.

✗ Anti-pattern — approximate dressed as exact
-- tile shows "Settled volume: 14,237,104" but it's
-- really a sampled/approximate estimate. Someone
-- treats it as the book of record.
✓ Optimized — disclose & route exactness
-- 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.

Impact: fast approximate tiles for everyday use, an audited exact path where it counts, and nobody reconciles against the wrong one.

← Performance pillar  ·  Practice · Q&A  ·  ↑ Top