Querying for BI: grain, joins and aggregation
NWOrders NWOrderLines NWCustomers NWRegions NWFx- Why it matters
- Most wrong numbers in BI are join mistakes: a join that multiplies rows, or one that silently drops them. SQL is where you see the grain clearly.
- Typical production failure
- An analyst joins orders to order lines and counts rows as orders; the dashboard reports twice as many orders as the business has.
- When to use it
- Whenever data comes from a database: check the grain of every table and of every result before aggregating.
- When not to
- Writing the whole model in one giant query. Keep views at a clear grain and let the semantic model do the slicing.
Assignments
Q1 2026 sales by region
- Load the company pack into your SQL engine (see Before you start).
- order_lines contains exact duplicate rows from a re-export. In a CTE, keep one copy of each (SELECT DISTINCT * is enough here).
- Join the lines to orders. Keep only Status = 'Completed', OrderDate from 2026-01-01 to 2026-03-31, and exclude the test customer C19999.
- Line value = ROUND(Qty × UnitPrice × (100 − DiscountPct) / 100, 2). Convert EUR lines to USD: join fx_rates on the order month (first 7 characters of OrderDate) and Currency, and multiply; round each line to cents.
- Join regions on SalesRegionKey and group by RegionName. Add a grand total in a second query, or with ROLLUP if your engine has it.
Count orders, lines and customers correctly
Sales wants one row per channel for Q1 2026 with the number of orders, order lines and buying customers, plus a total row. The totals must be right, not just the channel rows.
- Same filters as the previous assignment (deduplicated, completed, no test customer, Q1 2026).
- Show in a comment why COUNT(*) after the join is the wrong way to count orders.
- Explain why the channel rows don't add up to the total for customers.
The customer list that lost customers
Marketing asked for every customer with their Q1 2026 sales, including customers who bought nothing (they want to win them back). The query they were given returns only 280 rows, but the company has 420 real customers. Find out why and fix it.
- Return one row per customer (excluding the test account) with Q1 2026 gross sales, 0 when they bought nothing.
- Explain the bug in one sentence.
Interview questions
- What does "grain" mean, and why check it before writing a join?
- Difference between WHERE and ON in a LEFT JOIN?
- COUNT(*) vs COUNT(column) vs COUNT(DISTINCT column)?
- Why do distinct counts not add up across categories?
Assessment
SELECT COUNT(*) FROM orders o JOIN order_lines l ON l.OrderID = o.OrderID returns:
- The number of orders
- The number of order lines that have a matching order
- The number of customers
- The number of orders with more than one line
A LEFT JOIN from customers to orders with WHERE orders.OrderDate >= '2026-01-01' returns:
- All customers
- Only customers with at least one order on or after that date
- Only customers without orders
- An error
Write a query that returns the grain (the key columns and whether they are unique) of each table in the company pack.
For each table compare COUNT(*) with COUNT(DISTINCT key) (or with a GROUP BY on the candidate key HAVING COUNT(*) > 1). order_lines will surprise you.