SQL for Analytics
The language you actually live in
Analytical SQL reads modeled data with confidence: selecting clean columns, filtering, aggregating and joining facts and dimensions. It is the primary language BI developers speak to their warehouse.
What you will learn
- Write correct SELECT/FROM/WHERE/GROUP BY/ORDER BY for analytics
- Explain the difference between logical and physical query order
- Build a query that a BI tool can consume as a dataset
What is this?
Analytical SQL is the subset of SQL used for reading and shaping data for reports — selecting relevant columns, filtering to the right scope, grouping and aggregating, and joining facts to dimensions. Because warehouses are columnar and SQL is declarative, the engine decides how to execute; your job is to say exactly what you need.
Why do you need it?
- Nearly every BI tool compiles down to SQL somewhere — mastering SQL makes the tool transparent.
- SQL is portable across warehouses; models and reports built in SQL survive tool migrations.
- Correctness in analytics is mostly a question of scope and grain, and SQL expresses both.
- Performance starts with the query shape, not the tool's cache.
Mental model
Ordering precisely at a restaurant kitchen
Writing analytical SQL
You describe the dish (columns), dietary limits (WHERE), how many plates (GROUP BY) and the sequence (ORDER BY). The kitchen picks the equipment and order of cooking — you don't micromanage the flame, you specify the outcome.
How it works
- SELECT lists the columns or expressions you want; the engine scans only those (columnar win).
- WHERE filters rows before any grouping, trimming the data used downstream.
- GROUP BY collapses rows per the specified dimensions; aggregations (SUM, COUNT, AVG) compute over each group.
- HAVING filters groups after aggregation (unlike WHERE).
- ORDER BY sorts the final result — often cheap at the end and expensive if overused.
- Logical order: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY. Physical execution may differ.
Logical processing order of a query.
Examples
Read a modeled fact table
SELECT d.month, c.region, COUNT(DISTINCT f.order_id) AS orders, SUM(f.gmv) AS gmvFROM fact_orders fJOIN dim_date d ON f.date_key = d.date_keyJOIN dim_customer c ON f.cust_key = c.cust_keyWHERE f.gmv > 0 AND d.month >= '2026-01'GROUP BY d.month, c.regionORDER BY d.month, c.region;This reads one fact and its dimensions, filters to positive GMV and a date window, aggregates to monthly-regional grain, and orders it for a table visual. The BI tool consumes the result set directly.
In production
- A weekly region sales report is a single GROUP BY query over a fact table.
- Data checks in BI pipelines are SQL assertions: expected counts, no duplicates, no null keys.
- BI tools import models built in SQL (views, dbt models) so reports never parse raw production tables.
Best practices
- Filter early and at the right level — WHERE before GROUP BY where possible.
- Use COUNT(DISTINCT ...) deliberately; it is expensive but correct on high-cardinality keys.
- SELECT only needed columns; never SELECT * on a warehouse.
- Write the expected grain into the query so reviewers can verify it at a glance.
Common mistakes
- Using HAVING to filter rows that WHERE could filter — slower and confusing.
- Grouping and then re-aggregating in the BI tool, creating nested duplication.
- Joining facts to facts without a clear grain, silently multiplying rows.
- Forgetting NULL handling in WHERE (NULL never equals anything).
Common issues
Query returns far more rows than expectedA join to a many-sided table multiplied the grain
Verify the grain target and join at the right level (dedupe dimensions first).
COUNT(DISTINCT ...) is extremely slowHigh-cardinality column scanned over a huge window
Pre-aggregate in a model or restrict the date window early.
Debugging tips
- Run the inner scope first: SELECT .. FROM fact WHERE .. to verify row counts before grouping.
- Check grain: if orders > expected, a dimension join multiplied rows.
- Test filters on a small date window before scanning history.
- COUNT(*) vs COUNT(column) — the latter skips NULLs and surprises people.
Interview checkpoint
Hands-on lab
- Write a grouped aggregation over facts and dimensions
- Verify grain before trusting the numbers
- Use WHERE vs HAVING correctly
Query a Star Schema Like a BI Developer
35 min · beginner
Quick quiz
Which clause removes rows from a GROUP BY result set?
COUNT(column) and COUNT(*) always return the same value.