Think of this like sorting mail into bins before counting envelopes — interviewers love it because it reveals whether you know how databases avoid doing extra work.
Question: What is aggregate optimization in SQL?
Answer: Aggregate optimization is the set of tricks a database uses to make COUNT, SUM, AVG, MIN, MAX, and GROUP BY queries run faster. The engine tries to reduce rows early, choose the cheapest aggregate plan, and avoid extra sorting or disk spills. In practice, that means filtering before grouping, using helpful indexes, and sometimes precomputing totals in summary tables.
Interview-Ready Answer: In SQL, aggregate optimization means helping the database compute grouped results with less work. I would explain it as: first cut down the rows with WHERE, then let the optimizer pick the best physical strategy such as hash aggregate or stream aggregate, and if the query is repeated a lot, pre-aggregate into a summary table or materialized view. The biggest gotcha is memory: if a hash aggregate has too many groups, it can spill to disk and get much slower.
Aggregation turns many rows into fewer rows. A query like SELECT region, SUM(amount) FROM sales GROUP BY region looks simple, but the database still has to read rows, build groups, keep running totals, and then return one row per group. Optimization is about doing those steps with the least CPU, memory, and disk I/O possible.
WHERE first. Rows filtered out here never reach the aggregate. This is the biggest win when you only need a date range or a subset of customers.region and amount, the engine tries not to touch extra columns. Less data read means fewer page reads and less memory traffic.AVG becomes SUM / COUNT, COUNT(*) becomes a row total, and MIN/MAX are read from the stored state.| Plan | Best when | Strength | Risk |
|---|---|---|---|
| Hash Aggregate | Input is unsorted | Fast per group | Spills when memory is small |
| Sort / Stream Aggregate | Input is already ordered | Low memory use | Sort cost can be high |
| Pre-aggregation | Query repeats often | Very fast reads | Data can become stale |
If the engine can read rows in group-key order, it can do a stream aggregate: it finishes one group, emits the result, then moves to the next. That is cheaper than building a huge in-memory hash table. If rows are not ordered, hashing is often faster than sorting, because it avoids O(n log n) sort work. But if the number of distinct groups is huge, the hash table can grow large and spill to disk, which hurts latency a lot.
WHERE clauses so the aggregate sees fewer rows.n rows is still at least O(n); optimization mostly reduces constants, memory pressure, and spill risk.O(n) average time with O(g) memory, where g is the number of groups.O(n log n) because of the sort, but it can be better if the input is already ordered.COUNT(*) counts rows, but COUNT(column) ignores NULL values.SUM(column) over no matching rows returns NULL, so reports often need COALESCE(SUM(...), 0).DISTINCT inside an aggregate usually adds extra dedup work, so it is more expensive than a plain aggregate.Memory hook: Sort the coins into piles first, then total each pile. That picture reminds you to filter early, group by a key, and precompute only when the same totals are asked for repeatedly.
A checkout service at an e-commerce company shows a live dashboard of revenue by day and region. The raw orders table has hundreds of millions of rows, so every second matters. The team starts with a plain GROUP BY on the fact table, but the dashboard gets slower every week as data grows. The fix is to filter to the last 30 days, partition by date, and keep a daily summary table for the most common metrics.
A developer changes the dashboard query to add COUNT(DISTINCT customer_id) and removes the date filter because they want a lifetime number. Suddenly the hash table explodes, temp disk usage jumps, and the query times out during peak traffic. Users see blank charts, logs show slow queries and temp-file growth, and the BI team blames the app when the real problem is that the aggregate is doing far too much work on too many rows.
-- Demo: basic aggregate optimization ideas in one runnable script.
-- The data set is small, but the patterns scale to large tables.
DROP TABLE IF EXISTS sales;
CREATE TABLE sales (
sale_id INTEGER PRIMARY KEY,
order_date DATE NOT NULL,
region VARCHAR(20) NOT NULL,
amount DECIMAL(10,2)
);
INSERT INTO sales (sale_id, order_date, region, amount) VALUES
(1, '2024-01-01', 'East', 100.00),
(2, '2024-01-01', 'East', 50.00),
(3, '2024-01-01', 'West', 75.00),
(4, '2024-01-02', 'East', NULL),
(5, '2024-01-02', 'West', 25.00),
(6, '2024-01-02', 'West', 25.00);
-- Basic grouped aggregate: one row per region.
SELECT
region,
COUNT(*) AS rows_seen,
COUNT(amount) AS paid_rows,
SUM(amount) AS total_amount
FROM sales
GROUP BY region
ORDER BY region;
-- Edge case: COUNT(amount) skips NULL, but COUNT(*) still counts the row.
-- This is why row counts and non-null counts are not interchangeable.
SELECT
order_date,
COUNT(*) AS total_rows,
COUNT(amount) AS non_null_amount_rows,
COALESCE(SUM(amount), 0) AS safe_total
FROM sales
GROUP BY order_date
ORDER BY order_date;
-- Edge case: no matching rows produce NULL from SUM, so reports often wrap COALESCE.
SELECT COALESCE(SUM(amount), 0) AS missing_region_total
FROM sales
WHERE region = 'North';
-- Pre-aggregation pattern for dashboards: compute the summary once, query it many times.
DROP TABLE IF EXISTS daily_sales_summary;
CREATE TABLE daily_sales_summary AS
SELECT
order_date,
region,
COUNT(*) AS orders,
COALESCE(SUM(amount), 0) AS revenue
FROM sales
GROUP BY order_date, region;
SELECT *
FROM daily_sales_summary
ORDER BY order_date, region;Follow-up & Tricky Questions:
COUNT(*) only needs to count rows, while COUNT(column) must check the column for NULL. The difference is small on tiny data, but it matters at scale.COUNT(DISTINCT user_id) is usually much heavier than COUNT(*).SUM return 0 when no rows match? No. It returns NULL, so you need COALESCE if the report expects zero.COUNT(column) count rows with NULL in that column? No. It skips them, which makes it different from COUNT(*).Common Mistakes:
WHERE should remove rows before GROUP BY; otherwise the engine groups data it did not need.COUNT(*) and COUNT(col). Use COUNT(*) for rows and COUNT(col) only when you intentionally want to ignore NULL.Memory Hook: Filter first, then sort into piles, then total the piles. That one line captures the whole mental model: shrink the data, group it cheaply, and precompute only when the same answer is asked again and again.
Cheat Sheet:
WHERE.COALESCE for empty-set totals that should show zero.Practice Tasks:
COUNT(DISTINCT customer_id) report using a pre-aggregated summary table.COUNT(*), COUNT(amount), and SUM(amount) on a table that contains NULL values.