SQL Query Optimization
Making warehouse queries fast and cheap
Slow analytical queries usually suffer from predictable causes: missing predicates, non-sargable filters, heavy DISTINCT over big tables, and queries pulling more than asked. EXPLAIN and row-count discipline fix most of them.
What you will learn
- Read EXPLAIN / query profile output
- Apply the fastest wins: filters, sargability, column selection
- Design queries that use engine features (partition pruning, aggregation pushdown)
What is this?
Query optimization is the practice of structuring SQL so the engine scans less and aggregates earlier. The fastest wins are usually cheap: filter high early, avoid non-sargable predicates (function on the column), select only needed columns, avoid DISTINCT and correlated subqueries on big data, and let billable analytics engines prune partitions. EXPLAIN reveals what the planner actually does.
Why do you need it?
- Warehouse costs follow bytes scanned — efficient queries are cheaper and faster.
- Dashboards degrade when queries regress; optimization keeps SLAs.
- A 1000x speedup usually costs one line (a WHERE on a partition column).
- Understanding the plan turns 'why slow?' into a solvable engineering problem.
Mental model
A library catalog guiding a librarian
The query planner + indexes
The librarian won't open every shelf to find one book if the catalog (index) points directly there. Filtering early (ask for the right shelf) and using catalog entries is how queries avoid scanning the whole library.
How it works
- WHERE clauses on partition/date columns let warehouses prune entire storage chunks.
- Sargable predicates (col = x) use indexes; wrapping columns in functions (UPPER(col)) disables them.
- EXPLAIN (ANALYZE, COSTS) shows the plan: Seq Scan vs index vs partitions, row estimates, actual times.
- Pushdown: aggregations and filters that the engine can execute in storage don't ship bytes to your cluster.
- LIMIT everywhere helps exploration: never materialize a billion rows to look at 10.
Where query time goes and how to cut it.
Examples
Before and after
-- Slow: scans everything, function on column, selects *SELECT * FROM eventsWHERE UPPER(country) = 'US' AND day BETWEEN '2026-01-01' AND '2026-01-31'; -- Fast: sargable, date-limited, narrow projectionSELECT user_id, day, amountFROM eventsWHERE country = 'US' AND day BETWEEN '2026-01-01' AND '2026-01-31'LIMIT 100;Removing the function on the column lets the engine use filtering, the date window prunes partitions, and SELECT * shrinks to three columns.
In production
- A dashboard query that scanned 400GB dropped to 4GB by adding a date partition filter.
- A join slowed by cartesian surprise was fixed with a pre-aggregated daily fact.
- A reporting query got 40x faster by replacing COUNT(DISTINCT user) over a year with a precomputed DAU model.
Best practices
- Filter on the largest driver first: usually the date/partition column.
- Keep predicates sargable — compare raw columns, not wrapped ones.
- Return only the columns consumed; avoid SELECT * in production.
- Pre-aggregate heavy rollups into modeled tables for recurring dashboards.
Common mistakes
- Applying a function to the indexed column, killing the index.
- SELECT * dragging megabytes of columns into a Jupyter notebook.
- Aggregating before filtering — grouping a table you could have pruned first.
- Joining at the widest grain and paying for a cartesian blowup.
Debugging tips
- Run EXPLAIN (ANALYZE, COSTS) and read rows vs actual rows — estimates that differ wildly hint at stale stats.
- Slow query? Add a date filter first; 90% of analyst queries are fixed there.
- Compare bytes scanned before/after each change to track the win.
- When DISTINCT slows a query, canonicalize identity as a pre-aggregated unique key.
Interview checkpoint
Quick quiz
Which predicate is sargable (index-friendly)?
Adding a date range filter usually reduces warehouse bytes scanned.