33.3× Faster Dashboards
How one Sigma dashboard went from ten widget-level query paths to a shared data model — and how to make the same kind of change without changing what the numbers mean.
For months, the team couldn't rely on its main reporting dashboard. Some queries ran for several minutes. Others timed out before anything came back. The dashboard was so slow that analysts couldn't use it for normal reporting, which meant the system built to answer business questions was blocking the work instead.
By the time I joined, that slowness had been around long enough to feel normal. There wasn't one obviously broken chart to point at. The dashboard had about ten parallel query paths. Different widgets joined data in different ways and calculated related metrics for themselves. Those widget-level paths didn't always agree — the same campaign could show different totals depending on which chart you trusted. The browser looked like one product, but underneath it behaved like ten small reporting systems.
I replaced those paths with one shared data model. The model prepared the common relationships and metrics once, then each widget read from the same place. Speed was the main result. A secondary effect was consistency: fewer private query paths meant fewer ways for the numbers to drift apart. What follows is how that change turned an unusable dashboard back into a reporting tool, why the fix works, why it's easy to implement badly, and how to test it without confusing a faster answer with a correct one.
A dashboard is an interaction, not a query.
What humans feel is the whole trip from click to useful chart. That trip may include a browser calculation, a request from the BI tool, time waiting for a warehouse, SQL execution, data transfer, and chart drawing. Tuning the SQL can help, but it will not fix a queue, ten duplicate requests, or a large calculation left sitting in the browser.
Start with a trace, not a guess.
Open a page from a clean session and record what actually runs. Count warehouse queries and browser calculations. Note which widgets launch them, which data sources they read, whether the result came from cache, and how long each stage took. One slow query is easy to see. Ten medium queries running together can be harder to spot, even when their combined work is the real problem.
In the legacy workbook, one slow request merged logic from eleven workbook elements. That was a useful clue. The page was not merely reading a reporting table. It was rebuilding relationships and business rules while the user waited.
Parallel work is useful until it repeats the same thinking.
Parallel requests aren't automatically bad. Independent charts can load at the same time and shorten the total wait. Trouble starts when each request scans similar data, repeats the same joins, or calculates a slightly different version of the same metric. Then you pay twice: once in wait time, and again when two charts on the same page disagree. Those requests compete for warehouse slots, network bandwidth, browser memory, and the reader's attention. A page can look busy while still making little useful progress.
Think of ten cooks preparing ten plates. Parallel work helps if each cook has a separate task and the ingredients are ready. It hurts if every cook first walks to the store, makes the same stock, and writes a new recipe. The dashboard had the second pattern. Several widgets performed their own setup work before they could draw anything.
Concurrency also changes the shape of a benchmark. A single SQL statement tested alone may be quick, while the same statement slows down beside nine other requests. Measure the full page first. Then test the costly queries alone to learn whether the problem is the query itself, competition between queries, or both.
Set the row meaning before you tune the query.
The grain of a table is what one row means. This is the first question in dashboard design because joins can silently change it. Suppose one post belongs to two campaigns. Joining post performance directly to campaigns may produce two copies of the post. If a widget sums views after that join, the page can become fast and wrong.
One post on one observation date. Use it for trends and change over time.
One post at its newest accepted state. Use it when a page only needs current totals.
One valid campaign and post link. Use it to keep a real many-to-many relationship explicit.
One campaign and post row with the common reporting metrics ready to use.
The rebuilt path separated these jobs. The page did not scan the full post-by-day history when it only needed the latest state. Campaign membership lived in its own bridge. The reporting mart expanded that relationship at a known grain and exposed the metrics the widgets shared.
Prepared data has a price.
Moving work out of the click path does not make the work disappear. It moves the cost to refresh time. The team now owns storage, refresh order, freshness targets, and failure recovery. That is usually a good trade when many readers reuse the result, but it should be said out loud. In this system, prepared tables refreshed through a controlled warehouse path. A latest-state table used a twelve-hour freshness target because the page did not need second-by-second change.
Choose the smallest table that can answer the question.
A time-series chart needs history. A campaign scorecard may only need the latest accepted value for each post. Reading the history table for both is safe but wasteful. It scans many observations that the scorecard will throw away. A latest-state fact removes those unused rows before the click. A campaign-post serving mart goes one step further by preparing the relationship the campaign widgets use.
Smaller does not only mean fewer rows. It can mean fewer columns, simpler joins, and less work to find the newest record. The best serving table follows the question. It contains enough detail to filter and explain the answer, but not every piece of source history.
Watch for fanout.
Fanout happens when one row joins to several rows and a measure gets copied. For example, one post with 1,000 views may belong to two campaigns. A campaign-post result needs two relationship rows, but a portfolio total must not blindly sum both copies and report 2,000 unique views. The model needs a declared rule for the level at which a metric may be added.
This is why grain is also a performance tool. Once each table has one clear row meaning, the warehouse can prepare the right result without defensive widget formulas, distinct counts, or repeated deduplication. Correct structure removes work.
Use two tests because they answer two different questions.
A performance study can easily compare unlike things. This project used two views of speed, and they need to stay separate.
Matched page loads
Open the same saved view in each workbook, wait for the page to finish, and repeat. This asks: which version felt faster for this exact view?
Thirty-day usage history
Compare all measured workbook queries over time. This asks: what latency and failure pattern did users encounter across real use?
Read a distribution, not one average.
The median, also called P50, is the middle observed time. P90 is the time that 90 percent of observations beat. P95 and P99 move farther into the slow tail. P90 is useful, but it is not the worst case. A page can have a good median and still punish some users with very long waits.
Always show the sample size beside a percentile. A percentile from three runs is a quick check, not a stable service target. Also record failures. A fast query that fails is not a good user experience.
Treat cache as part of the experiment.
A cache returns work that was already done. Warm-cache tests can describe repeat use. Cold-cache tests can describe first use. Neither is wrong, but mixing them hides the cause of a speed difference. In a fresh trace from this study, 24 of 30 test-workbook warehouse queries were cached, compared with one of three legacy-workbook queries. That makes cache a real part of the observed experience — and it confounds any attempt to pin the gain on one cause.
A benchmark needs a written protocol.
Write the steps before seeing the result. Name the saved view, account, filters, time window, warehouse size, and completion signal. Decide whether the cache should be warm or cold. Alternate the order of the two versions so the first one does not always receive a different system state. Repeat enough times to see normal variation, and keep every valid run instead of dropping slow ones because they look unusual.
Capture at least three clocks: the user's page-load time, the BI tool's processing time, and warehouse execution time. If the page is slow while warehouse time is low, the bottleneck may be browser work, request coordination, or data transfer. If warehouse time dominates, inspect scans, joins, grouping, and queue pressure.
Look at widget count, browser formulas, fetch size, and chart rendering.
Look at scans, joins, grain, filters, and repeated metric work.
Look at concurrency, warehouse size, workload isolation, and schedule overlap.
Cache is carrying the experience. Decide whether first-use speed matters.
Speed is a failed project if the number changes meaning.
Moving rules into a shared model changes joins, row selection, and metric timing. Each of those changes can alter the answer. So performance testing needs a separate correctness test. Do not use matching numbers as a speed sample, and do not use fast load times as proof of accuracy.
The correctness study compared 336 known campaign deliverables across three campaigns. The new model matched 333 of 336 campaign, link, creator, and platform records. That is a 99.1 percent match for the tested identity fields. It did not validate every engagement metric, and it did not turn the three campaigns into a performance benchmark.
A practical semantic contract
- Write down what one row means in every prepared table.
- Name the source of each metric and the time at which it is valid.
- Test joins for missing rows and multiplied rows.
- Compare known records at the level a user can inspect.
- Keep unresolved differences visible instead of forcing a match.
A short playbook for a slow dashboard
- Define the user action.Choose the exact page, filters, and sign that tells you loading is done.
- Trace the whole request.Count widget requests, warehouse queries, browser work, queue time, fetch time, and cache hits.
- Group repeated logic.Find joins, identity rules, latest-row rules, and metrics rebuilt by several widgets.
- Set the grain.State what one row means before combining data. Protect measures from join fanout.
- Prepare shared work upstream.Build small facts, bridges, latest-state tables, and serving marts for work many readers reuse.
- Keep widgets thin.Let them filter, group, and display. Do not let each one invent the business answer.
- Benchmark fairly.Repeat matched loads. Separate cold and warm cache. Report sample size, percentiles, and failures.
- Check the meaning.Reconcile known records and metrics. A quicker wrong answer is still wrong.
When not to precompute
Keep work at request time when the question is rare, the result must be live to the second, or the possible filter combinations are too wide to prepare well. Precompute when the same logic serves many widgets or users, when source scans are large, and when a small delay in freshness is acceptable. Most useful systems end up mixing both approaches.
What stuck
The biggest win was not a clever SQL tweak. Define shared business logic once, calculate the reusable metrics once, and let every widget consume the same trusted result. It worked because the page stopped behaving like ten separate data products.
Speed was the headline. Consistency was the quieter secondary effect. When every widget stopped inventing its own joins and metrics, the numbers on the page had one place to come from — so they stopped drifting apart for accidental reasons. A dashboard feels fast when someone gets a trustworthy answer before they lose the thread of the question. That takes more than SQL tuning. It needs a clear model, a short request path, and evidence that speed did not change the truth of the numbers.
Evidence note
The worked example uses a July 28, 2026 performance comparison, fresh query traces from both workbooks, the warehouse model definitions, and a separate 336-record correctness study. The exact page-load result is the strongest direct speed evidence. The thirty-day history describes different workloads and should not be read as a controlled causal test.
The shared-model workbook was faster in both views of the evidence.
For one matched Sigma saved view, the shared-model workbook loaded about 2.76 seconds faster at the median across three runs. This is still the cleanest direct comparison in the study.
| Workbook | Run 1 | Run 2 | Run 3 | Median |
|---|---|---|---|---|
| Shared-model test | 2,961 ms | 3,012 ms | 2,940 ms | 2,961 ms |
| Legacy version | 5,717 ms | 5,744 ms | 5,331 ms | 5,717 ms |
The production comparison used the actual rollout windows. The V1 baseline covered June 29 to July 28. V2 launched on July 31, so its first live period ran through August 6. Median query time fell from 9,566 milliseconds to 287 milliseconds, a 33.3× difference. P90 fell from 37,433 milliseconds to 1,015 milliseconds, a 36.9× difference.
| Measure | V2 launch, Jul 31–Aug 6 | V1 baseline, Jun 29–Jul 28 |
|---|---|---|
| Measured queries | 4,116 | 25,303 |
| P50 | 287 ms | 9,566 ms |
| P90 | 1,015 ms | 37,433 ms |
| P95 | 1,477 ms | 45,362 ms |
| P99 | 16,941 ms | 66,252 ms |
| Reported failed-query rate | 5.6% | 23.4% |
| Non-cancellation failures | 0 | Not separately audited |
Sigma marked 232 V2 launch queries as failed. Grouping those rows by the Query Error Response field showed one reason for every row: “Query canceled by Sigma.” The log showed no warehouse or model error behind those records. The reported failed-query rate was 76% lower, and the launch data contained zero non-cancellation failures.
What I'd take from this
The result fits the design change. Sigma no longer asked a set of widgets to rebuild separate versions of the answer. Shared identity, campaign relationships, latest-row rules, and common metrics ran upstream. The widgets consumed one prepared model and had less work left to coordinate when the page opened.
Speed is the main measured result. The secondary effect was data consistency: one shared path replaced multiple widget-level query paths that had been free to produce slightly different answers for the same business question. That does not replace the separate correctness study, but it is why the redesign was about trust as well as wait time.
The 33.3× figure is a real observed difference, but it compares a thirty-day V1 baseline with V2's first week after launch. The matched view gives the stronger direct comparison at about 1.9× faster. The workbooks also used different source paths, query shapes, and cache patterns. The evidence shows a faster redesigned experience. It does not assign every saved millisecond to one cause.