A dashboard is like a receipt tape: if you group at the wrong level, the numbers look neat and are still wrong.
Question: Dashboard Aggregations
Answer: Dashboard aggregations turn raw rows into summary numbers, such as daily revenue, orders, active users, or conversion rate. In SQL, you usually do this with GROUP BY, plus SUM, COUNT, AVG, and conditional logic like CASE. The biggest skill is choosing the right grain, which means the exact level you want to summarize at, such as per day or per customer.
Interview-Ready Answer: I build dashboard aggregations by first picking the right grain, then filtering to the correct fact rows, then grouping by the dashboard bucket, like day or product. After that I compute metrics with COUNT, SUM, and COUNT(DISTINCT ...), and I use COALESCE plus a calendar table when I need zero-filled days. I also watch out for double counting after joins, because one order can become many rows if I join to line items too early.
Think of a dashboard query as a funnel: raw facts go in, a smaller set of grouped rows comes out. The query is not just ‘count things’; it is ‘count the right things at the right level’.
SUM for money, COUNT for volume, AVG for averages, and COUNT(DISTINCT ...) for unique users.GROUP BY will skip it. A dashboard usually wants zero instead of a missing line, so you join to a calendar table and use COALESCE.The database reads rows, keeps a running state for each group, and then finalizes the result. With a hash aggregate, the engine stores each group in memory using a hash table; with a sort aggregate, it sorts rows by the grouping key first. In both cases, the important idea is that the database only needs to remember one state per group, not one state per row.
WHERE filter to discard unwanted rows.LEFT JOIN to a calendar table restores the missing buckets.Complexity is usually O(N) time for N input rows, because each row is read once, plus O(G) memory for G groups. A dashboard with 10 million events and 30 daily buckets is usually cheap; a dashboard grouped by day, product, country, and channel can explode into hundreds of thousands of groups and use much more memory. Distinct counts are the expensive part, because COUNT(DISTINCT ...) needs extra work to remember unique values.
| Tool | Best use | Gotcha |
|---|---|---|
GROUP BY | One row per bucket | Rows collapse |
| Window function | Add totals to rows | Rows stay visible |
HAVING | Filter groups | Runs after grouping |
GROUP BY is the main tool for dashboards because dashboards usually want summary rows. Window functions, which are aggregate functions that do not remove rows, are useful when you want each raw row plus a total beside it. HAVING is for post-aggregation filters, such as ‘show only products with more than 100 orders’.
COUNT(*) vs COUNT(column): COUNT(*) counts rows, while COUNT(column) ignores NULL values. That difference matters if missing data should not be counted.WHERE removes rows before grouping, while HAVING removes groups after grouping. Putting the wrong condition in the wrong place changes the meaning of the metric.LEFT JOIN if the dashboard must show zeroes.Memory model: remember ‘filter, bucket, count, then fill gaps’. If you can explain that flow clearly, you can whiteboard most dashboard queries under pressure.
Imagine a checkout service for an e-commerce app. The business dashboard shows daily revenue, paid orders, and unique paying customers. The SQL query must exclude canceled and pending orders, group by day, and show a zero for days when the store was offline or simply had no orders.
Here is the classic bug: an engineer joins orders to order_items before summing revenue. One order with three items becomes three rows, so the revenue suddenly triples. The dashboard still looks ‘reasonable’ because the numbers are larger, but finance notices a mismatch against the ledger. In logs, you may see row counts that are much higher than order counts, and the user-facing symptom is a sales graph that spikes for the wrong reason.
This is why aggregation questions matter: dashboards are often the first place bad SQL becomes visible to executives, support, and finance. A tiny counting mistake can look like a company-wide growth miracle or a fake outage.
-- Dashboard aggregation example:
-- We keep a calendar table so days with no sales still appear as zero.
-- We filter to paid orders before grouping so canceled/pending rows do not inflate revenue.
-- We count distinct customers so one buyer placing multiple orders is not counted twice.
CREATE TABLE calendar (
day DATE PRIMARY KEY
);
INSERT INTO calendar (day) VALUES
(DATE '2024-06-01'),
(DATE '2024-06-02'),
(DATE '2024-06-03'),
(DATE '2024-06-04'),
(DATE '2024-06-05');
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY,
order_day DATE NOT NULL,
customer_id INTEGER NOT NULL,
status VARCHAR(20) NOT NULL,
amount DECIMAL(10,2) NOT NULL
);
INSERT INTO orders (order_id, order_day, customer_id, status, amount) VALUES
(1, DATE '2024-06-01', 101, 'paid', 120.00),
(2, DATE '2024-06-01', 102, 'paid', 80.00),
(3, DATE '2024-06-01', 103, 'pending', 50.00),
(4, DATE '2024-06-02', 101, 'paid', 35.00),
(5, DATE '2024-06-02', 104, 'canceled', 60.00),
(6, DATE '2024-06-03', 105, 'paid', 200.00),
(7, DATE '2024-06-05', 101, 'paid', 40.00),
(8, DATE '2024-06-05', 106, 'paid', 40.00);
WITH daily_paid AS (
SELECT
order_day,
COUNT(*) AS paid_orders,
SUM(amount) AS revenue,
COUNT(DISTINCT customer_id) AS paying_customers
FROM orders
WHERE status = 'paid'
GROUP BY order_day
)
SELECT
c.day,
COALESCE(d.paid_orders, 0) AS paid_orders,
COALESCE(d.revenue, 0) AS revenue,
COALESCE(d.paying_customers, 0) AS paying_customers,
COALESCE(ROUND(d.revenue / NULLIF(d.paid_orders, 0), 2), 0) AS avg_order_value
FROM calendar c
LEFT JOIN daily_paid d
ON d.order_day = c.day
ORDER BY c.day;
-- What this protects against:
-- 1) June 4 has no orders, but the dashboard still shows the day with zeros.
-- 2) Pending and canceled rows do not enter the metric.
-- 3) If paid_orders is 0, NULLIF prevents divide-by-zero in average order value.Follow-up & Tricky Questions:
LEFT JOIN the aggregated facts onto it. Then wrap missing metrics with COALESCE(..., 0) so the dashboard shows zero instead of a blank.GROUP BY and a window function? GROUP BY collapses rows into summary rows, while a window function keeps the original rows and adds a metric beside them. Use windows when you need row detail plus a total.NULLIF so you do not divide by zero.HAVING? Use it when you want to filter groups after aggregation, such as ‘only days with revenue above 10,000’. If you put that condition in WHERE, the query means something different.COUNT(DISTINCT ...) sometimes slow? Because the database must track uniqueness, which is more expensive than a simple count. On large dashboards, that can become the heaviest part of the query.COUNT(status = 'paid') a trap? It is not a portable way to count only paid rows, because COUNT counts non-NULL expressions rather than true values. Use SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) for clear, portable SQL.LEFT JOIN sometimes behave like an inner join? If you put a filter on the right table in WHERE, you remove the null-extended rows and lose the zero buckets. Keep right-table filters inside the join condition or inside the pre-aggregation.Common Mistakes:
WHERE and use the correct status logic.COALESCE.Memory Hook: Filter, bucket, count, fill. First remove bad rows, then choose the bucket, then compute the metric, then fill in the empty dates.
Cheat Sheet:
GROUP BY collapses rows into dashboard rows.WHERE filters before aggregation; HAVING filters after.COUNT(*) counts rows; COUNT(column) ignores NULL.CASE for conditional metrics like paid revenue.LEFT JOIN to show zero days.Practice Tasks:
GROUP BY results by hour versus by day.HAVING.