SQL for Analysis
Answer business questions with SQL, from your first query to finding why a number moved. Every exercise runs in your browser on the data of Raket Parts, a fictional maker of rocket parts. For clear examples, see JOIN double-counting and GROUP BY and HAVING.
You will be able to
- write queries that pick the right rows
- count and sum without double counting
- join tables safely
- explain why a figure changed
Your first queries
Read how company data is stored, then write SELECT, WHERE, ORDER BY and DISTINCT queries.
What one row means
Find the key, prove the grain and spot history rows before you count anything.
Choosing the right rows
NULL, blanks, cancelled records and date boundaries, so you count the right population.
Counting and summing correctly
COUNT, GROUP BY, HAVING, shares of total and matching the finance figure.
Joining tables safely
INNER and LEFT joins, and how to stop a join doubling your totals.
Finding what is missing
Anti-joins, UNION and EXCEPT, and reconciling two sources.
Building in steps
Subqueries and CTEs, each step checked by a count.
Dates and time
Months, quarters, time zones and like-for-like comparisons.
Window functions
Ranking, running totals, LAG and LEAD.
Sequences and cohorts
Ordering events, time in a state and retention.
Diagnosing wrong results
Find why a number is wrong, ending with the €2.6m sales question.
Reusable SQL
Views, and four tests to run before anyone else uses your query.
Want to practice on your own questions? The free SQL practice page has the same tables and ten starter questions.
Open free SQL practiceSQL for Analysis / Module 1
Your first queries
Learn what a database is and write your first SQL queries. You will practice on realistic sales, people and factory data from Raket Parts, a fictional company, guided by Teun Veldhorst, Finance Director, who needs your answers. No experience needed.
Sections
SQL for Analysis / Module 1 / Free practice
Try it on your own question
Your query
This box has its own copy of the data, so you can change or delete rows without affecting anything else. Reset brings back the starting data and query.
SQL for Analysis / Module 2
What one row means
Before you count anything, find out what one row stands for. You will learn to name a table's grain, test whether a column is really a key, and spot tables that keep history.
This module builds on module 1. If you are new to SQL, start with module 1.
Sections
SQL for Analysis / Module 2 / Free practice
Try it on your own question
Your query
This box has its own copy of the data, so you can change or delete rows without affecting anything else. Reset brings back the starting data and query.
SQL for Analysis / Module 3
Choosing the right rows
The right answer needs the right rows. Learn how blanks (NULL) behave, how to leave out cancelled records, why two teams can count "sales" differently, and how to set date boundaries that don't drop a day.
This module builds on module 2. If you are new to SQL, start with module 2.
Sections
SQL for Analysis / Module 3 / Free practice
Try it on your own question
Your query
This box has its own copy of the data, so you can change or delete rows without affecting anything else. Reset brings back the starting data and query.
SQL for Analysis / Module 4
Counting and summing correctly
Now you add things up. Learn the three ways to count, SUM and AVG by group, why averages can mislead, how to show a share of a total, and how to match finance's number.
This module builds on module 3. If you are new to SQL, start with module 3.
Sections
SQL for Analysis / Module 4 / Free practice
Try it on your own question
Your query
This box has its own copy of the data, so you can change or delete rows without affecting anything else. Reset brings back the starting data and query.
SQL for Analysis / Module 5
Joining tables safely
Real questions need more than one table. Learn how tables fit together, INNER and LEFT joins, and the join mistake that silently inflates totals, then fix it.
This module builds on module 4. If you are new to SQL, start with module 4.
Sections
SQL for Analysis / Module 5 / Free practice
Try it on your own question
Your query
This box has its own copy of the data, so you can change or delete rows without affecting anything else. Reset brings back the starting data and query.
SQL for Analysis / Module 1 / 1.1
1.1 How company data is stored
Section 1.1 complete
Next: 1.2 SELECT and FROM, where you choose the columns you need.
SQL for Analysis / Module 1 / 1.2
1.2 SELECT and FROM
Section 1.2 complete
Next: 1.3 WHERE, where you choose the rows.
SQL for Analysis / Module 1 / 1.3
1.3 WHERE: choosing rows
Section 1.3 complete
Next: 1.4 ORDER BY and LIMIT, where you sort and find the top records.
SQL for Analysis / Module 1 / 1.4
1.4 ORDER BY and LIMIT
Section 1.4 complete
Next: 1.5 DISTINCT and aliases.
SQL for Analysis / Module 1 / 1.5
1.5 DISTINCT and aliases
Section 1.5 complete
Next: the module check: five questions on fresh tasks, with no hints.
Continue to the module check Recap this section Back to module 1
SQL for Analysis / Module 1 / Check
Module 1 check
Module 1 complete
Next: Module 2, What one row means.
Continue to module 2 Try it on your own question Recap module 1 Back to module 1
SQL for Analysis / Module 2 / 2.1
2.1 Name the grain
Section 2.1 complete
Next: 2.2 Is it really a key?.
SQL for Analysis / Module 2 / 2.3
2.3 Find the repeats
Section 2.3 complete
Next: 2.4 Tables with history.
SQL for Analysis / Module 2 / 2.4
2.4 Tables with history
Section 2.4 complete
Next: the module check: 4 tasks with no hints.
Continue to the module check Recap this section Back to module 2
SQL for Analysis / Module 2 / Check
Module 2 check
Module 2 complete
Next: Module 3, Choosing the right rows.
Continue to module 3 Try it on your own question Recap module 2 Back to module 2
SQL for Analysis / Module 3 / 3.1
3.1 Blanks: NULL
Section 3.1 complete
Next: 3.2 Leave out what doesn't count.
SQL for Analysis / Module 3 / 3.2
3.2 Leave out what doesn't count
Section 3.2 complete
Next: 3.3 Agree what it counts.
SQL for Analysis / Module 3 / 3.4
3.4 Date boundaries
Section 3.4 complete
Next: the module check: 4 tasks with no hints.
Continue to the module check Recap this section Back to module 3
SQL for Analysis / Module 3 / Check
Module 3 check
Module 3 complete
Next: Module 4, Counting and summing correctly.
Continue to module 4 Try it on your own question Recap module 3 Back to module 3
SQL for Analysis / Module 4 / 4.1
4.1 Three ways to count
Section 4.1 complete
Next: 4.2 SUM, AVG and GROUP BY.
SQL for Analysis / Module 4 / 4.2
4.2 SUM, AVG and GROUP BY
Section 4.2 complete
Next: 4.3 Averages that mislead.
SQL for Analysis / Module 4 / 4.3
4.3 Averages that mislead
Section 4.3 complete
Next: 4.4 Shares, per cent and points.
SQL for Analysis / Module 4 / 4.4
4.4 Shares, per cent and points
Section 4.4 complete
Next: 4.5 Match finance's number.
SQL for Analysis / Module 4 / 4.5
4.5 Match finance's number
Section 4.5 complete
Next: the module check: 4 tasks with no hints.
Continue to the module check Recap this section Back to module 4
SQL for Analysis / Module 4 / Check
Module 4 check
Module 4 complete
Next: Module 5, Joining tables safely.
Continue to module 5 Try it on your own question Recap module 4 Back to module 4
SQL for Analysis / Module 5 / 5.1
5.1 How the tables fit
Section 5.1 complete
Next: 5.2 INNER and LEFT JOIN.
SQL for Analysis / Module 5 / 5.2
5.2 INNER and LEFT JOIN
Section 5.2 complete
Next: 5.3 Joins that double count.
SQL for Analysis / Module 5 / 5.3
5.3 Joins that double count
Section 5.3 complete
Next: 5.4 Fix the double count.
SQL for Analysis / Module 5 / 5.4
5.4 Fix the double count
Section 5.4 complete
Next: the module check: 4 tasks with no hints.
Continue to the module check Recap this section Back to module 5
SQL for Analysis / Module 5 / Check
Module 5 check
Module 5 complete
Next: modules 6 to 12, coming soon.
Back to the Path Try it on your own question Recap module 5 Back to module 5
SQL for Analysis / Module 1 / Recap
Recap card
1.1 How company data is stored
How company data is stored
- A database is one shared place where a company keeps its data. SQL is how you ask it questions.
- Each table is about one subject: customers, invoices or invoice lines.
- A row is one record. A column is one fact about it.
- A key identifies exactly one row, and other tables use it to link to that row.
- Every column has a type: text, date or number.
Common mistake
Counting rows as if each row were an invoice. In a table of invoice lines, one invoice can take several rows, so counting rows gives too many invoices and too low an average. Check what one row means first.
SQL for Analysis / Module 1 / Recap
Recap card
1.2 SELECT and FROM
SELECT and FROM
SELECTpicks the columns.FROMnames the table.- Commas between columns, none after the last.
*after SELECT means every column. Between numbers, it means multiply.- SQL can calculate:
+ - * /. Use4.0to get decimals.
Syntax
SELECT column1, column2 FROM table_name; SELECT number * 1.0 / number AS name;
Example
SELECT customer_ref, country FROM customers; SELECT 18479223 * 1.0 / 3029 AS average;
Common mistake
Forgetting a comma between columns. SELECT customer_ref country returns only one column, with no error. Put a comma between every pair of columns.
SQL for Analysis / Module 1 / Recap
Recap card
1.3 WHERE: choosing rows
WHERE: choosing rows
WHEREkeeps only rows that pass a test.- Text and dates in quotes, numbers without.
AND: both true.OR: either true.IN: any of a list.BETWEEN: a range, both ends included.
Syntax
SELECT columns FROM table_name WHERE condition AND other_condition;
Example
SELECT invoice_ref, net_amount FROM customer_invoices WHERE net_amount > 50000 AND invoice_date BETWEEN '2025-07-01' AND '2025-09-30';
Common mistake
Using AND when you mean OR. "Customers in the Netherlands and Belgium" sounds like AND, but no customer is in both countries at once, so AND finds nobody. Use IN ('NL', 'BE').
SQL for Analysis / Module 1 / Recap
Recap card
1.4 ORDER BY and LIMIT
ORDER BY and LIMIT
ORDER BYsorts. Smallest first unless you addDESC.LIMITkeeps the first rows.- Clause order: SELECT, FROM, WHERE, ORDER BY, LIMIT.
Syntax
SELECT columns FROM table_name ORDER BY column DESC LIMIT n;
Example
SELECT invoice_ref, net_amount FROM customer_invoices ORDER BY net_amount DESC LIMIT 5;
Common mistake
Forgetting DESC. Without it, sorting goes smallest first, so a "top 5" quietly gives you the 5 smallest. Add DESC after the column.
SQL for Analysis / Module 1 / Recap
Recap card
1.5 DISTINCT, COUNT and aliases
DISTINCT, COUNT and aliases
DISTINCTlists each value, or each combination, once.COUNT(*)counts rows.COUNT(DISTINCT col)counts different values.ASnames a result column.
Syntax
SELECT COUNT(DISTINCT column) AS name FROM table_name;
Example
SELECT COUNT(DISTINCT country) AS countries FROM customers;
Common mistake
Using DISTINCT to make repeats disappear. If rows repeat when you did not expect it, find out why first. It usually means one row is not what you thought.
SQL for Analysis / Module 1 / Recap
Recap card
Module 1: Your first queries
How company data is stored
- A database is one shared place where a company keeps its data. SQL is how you ask it questions.
- Each table is about one subject: customers, invoices or invoice lines.
- A row is one record. A column is one fact about it.
- A key identifies exactly one row, and other tables use it to link to that row.
- Every column has a type: text, date or number.
Common mistake
Counting rows as if each row were an invoice. In a table of invoice lines, one invoice can take several rows, so counting rows gives too many invoices and too low an average. Check what one row means first.
SELECT and FROM
SELECTpicks the columns.FROMnames the table.- Commas between columns, none after the last.
*after SELECT means every column. Between numbers, it means multiply.- SQL can calculate:
+ - * /. Use4.0to get decimals.
Syntax
SELECT column1, column2 FROM table_name; SELECT number * 1.0 / number AS name;
Example
SELECT customer_ref, country FROM customers; SELECT 18479223 * 1.0 / 3029 AS average;
Common mistake
Forgetting a comma between columns. SELECT customer_ref country returns only one column, with no error. Put a comma between every pair of columns.
WHERE: choosing rows
WHEREkeeps only rows that pass a test.- Text and dates in quotes, numbers without.
AND: both true.OR: either true.IN: any of a list.BETWEEN: a range, both ends included.
Syntax
SELECT columns FROM table_name WHERE condition AND other_condition;
Example
SELECT invoice_ref, net_amount FROM customer_invoices WHERE net_amount > 50000 AND invoice_date BETWEEN '2025-07-01' AND '2025-09-30';
Common mistake
Using AND when you mean OR. "Customers in the Netherlands and Belgium" sounds like AND, but no customer is in both countries at once, so AND finds nobody. Use IN ('NL', 'BE').
ORDER BY and LIMIT
ORDER BYsorts. Smallest first unless you addDESC.LIMITkeeps the first rows.- Clause order: SELECT, FROM, WHERE, ORDER BY, LIMIT.
Syntax
SELECT columns FROM table_name ORDER BY column DESC LIMIT n;
Example
SELECT invoice_ref, net_amount FROM customer_invoices ORDER BY net_amount DESC LIMIT 5;
Common mistake
Forgetting DESC. Without it, sorting goes smallest first, so a "top 5" quietly gives you the 5 smallest. Add DESC after the column.
DISTINCT, COUNT and aliases
DISTINCTlists each value, or each combination, once.COUNT(*)counts rows.COUNT(DISTINCT col)counts different values.ASnames a result column.
Syntax
SELECT COUNT(DISTINCT column) AS name FROM table_name;
Example
SELECT COUNT(DISTINCT country) AS countries FROM customers;
Common mistake
Using DISTINCT to make repeats disappear. If rows repeat when you did not expect it, find out why first. It usually means one row is not what you thought.
SQL for Analysis / Module 2 / Recap
Recap card
2.1 Name the grain
Name the grain
- The grain is what one row stands for: "one row is one…".
- Count rows only in a table whose grain matches what you are counting.
Common mistake
Counting rows in the wrong table. Order lines are not orders. Name the grain before you count.
SQL for Analysis / Module 2 / Recap
Recap card
2.2 Is it really a key?
Is it really a key?
- A key never repeats.
- Test:
COUNT(*)equalsCOUNT(DISTINCT column). - Some keys need two columns. Glue them with
||to test.
Syntax
SELECT COUNT(*),
COUNT(DISTINCT column)
FROM table_name;Example
SELECT COUNT(DISTINCT so_ref || '-' || line_no) FROM sales_order_lines;
Common mistake
Assuming a reference is a key. so_ref is a key of orders but repeats in order lines. Test it in the table you are using.
SQL for Analysis / Module 2 / Recap
Recap card
2.3 Find the repeats
Find the repeats
GROUP BYmakes one group per value.HAVINGfilters groups.WHEREfilters rows.
Syntax
SELECT column, COUNT(*) FROM table_name GROUP BY column HAVING COUNT(*) > 1;
Example
SELECT customer_ref, COUNT(*) AS invoices FROM customer_invoices GROUP BY customer_ref HAVING COUNT(*) > 100;
Common mistake
Using WHERE for a count. WHERE COUNT(*) > 1 fails, because counts only exist after grouping. Use HAVING.
SQL for Analysis / Module 2 / Recap
Recap card
2.4 Tables with history
Tables with history
- History tables add a new row or version instead of overwriting.
- The highest version is the current status.
- Decide which version you mean before you count.
Syntax
SELECT COUNT(*) FROM table_name WHERE status = 'WITHDRAWN';
Example
SELECT COUNT(*) AS withdrawn FROM product_revisions WHERE revision_status = 'WITHDRAWN';
Common mistake
Counting every row. 3,176 rows are 3,110 revisions, of which 3,044 are in use, for 2,830 products.
SQL for Analysis / Module 2 / Recap
Recap card
Module 2: What one row means
Name the grain
- The grain is what one row stands for: "one row is one…".
- Count rows only in a table whose grain matches what you are counting.
Common mistake
Counting rows in the wrong table. Order lines are not orders. Name the grain before you count.
Is it really a key?
- A key never repeats.
- Test:
COUNT(*)equalsCOUNT(DISTINCT column). - Some keys need two columns. Glue them with
||to test.
Syntax
SELECT COUNT(*),
COUNT(DISTINCT column)
FROM table_name;Example
SELECT COUNT(DISTINCT so_ref || '-' || line_no) FROM sales_order_lines;
Common mistake
Assuming a reference is a key. so_ref is a key of orders but repeats in order lines. Test it in the table you are using.
Find the repeats
GROUP BYmakes one group per value.HAVINGfilters groups.WHEREfilters rows.
Syntax
SELECT column, COUNT(*) FROM table_name GROUP BY column HAVING COUNT(*) > 1;
Example
SELECT customer_ref, COUNT(*) AS invoices FROM customer_invoices GROUP BY customer_ref HAVING COUNT(*) > 100;
Common mistake
Using WHERE for a count. WHERE COUNT(*) > 1 fails, because counts only exist after grouping. Use HAVING.
Tables with history
- History tables add a new row or version instead of overwriting.
- The highest version is the current status.
- Decide which version you mean before you count.
Syntax
SELECT COUNT(*) FROM table_name WHERE status = 'WITHDRAWN';
Example
SELECT COUNT(*) AS withdrawn FROM product_revisions WHERE revision_status = 'WITHDRAWN';
Common mistake
Counting every row. 3,176 rows are 3,110 revisions, of which 3,044 are in use, for 2,830 products.
SQL for Analysis / Module 3 / Recap
Recap card
3.1 Blanks: NULL
Blanks: NULL
- NULL means no value recorded. It is not zero.
- Find it with
IS NULL/IS NOT NULL. =and<>skip NULL rows.
Syntax
WHERE column IS NULL WHERE column <> value OR column IS NULL
Example
SELECT COUNT(*) FROM enquiries WHERE campaign_ref IS NULL;
Common mistake
Writing = NULL. It never matches. Use IS NULL.
SQL for Analysis / Module 3 / Recap
Recap card
3.2 Leave out what doesn't count
Leave out what doesn't count
- Exclude cancelled, test and internal records on purpose.
- Count what you excluded, and say so.
Syntax
WHERE status <> 'CANCELLED'
Example
SELECT COUNT(*) FROM sales_order_lines WHERE line_status <> 'CANCELLED';
Common mistake
Excluding silently. Others cannot reproduce your number. Add a footnote with the count and value removed.
SQL for Analysis / Module 3 / Recap
Recap card
3.3 Agree what it counts
Agree what it counts
- The same word can mean different events: ordered, invoiced, paid.
- Agree the event, the date and the exclusions.
Common mistake
Comparing numbers with different definitions. €22.3m ordered and €18.5m invoiced are both right for Q3.
SQL for Analysis / Module 3 / Recap
Recap card
3.4 Date boundaries
Date boundaries
- Use
>= startand< next start. - BETWEEN includes both ends, which is risky with timestamps.
Syntax
WHERE date_col >= 'start' AND date_col < 'next_start'
Example
SELECT COUNT(*) FROM customer_invoices WHERE invoice_date >= '2025-09-01' AND invoice_date < '2025-10-01';
Common mistake
Losing the last day. With timestamps, BETWEEN … '2025-09-30' stops at midnight on the 30th.
SQL for Analysis / Module 3 / Recap
Recap card
Module 3: Choosing the right rows
Blanks: NULL
- NULL means no value recorded. It is not zero.
- Find it with
IS NULL/IS NOT NULL. =and<>skip NULL rows.
Syntax
WHERE column IS NULL WHERE column <> value OR column IS NULL
Example
SELECT COUNT(*) FROM enquiries WHERE campaign_ref IS NULL;
Common mistake
Writing = NULL. It never matches. Use IS NULL.
Leave out what doesn't count
- Exclude cancelled, test and internal records on purpose.
- Count what you excluded, and say so.
Syntax
WHERE status <> 'CANCELLED'
Example
SELECT COUNT(*) FROM sales_order_lines WHERE line_status <> 'CANCELLED';
Common mistake
Excluding silently. Others cannot reproduce your number. Add a footnote with the count and value removed.
Agree what it counts
- The same word can mean different events: ordered, invoiced, paid.
- Agree the event, the date and the exclusions.
Common mistake
Comparing numbers with different definitions. €22.3m ordered and €18.5m invoiced are both right for Q3.
Date boundaries
- Use
>= startand< next start. - BETWEEN includes both ends, which is risky with timestamps.
Syntax
WHERE date_col >= 'start' AND date_col < 'next_start'
Example
SELECT COUNT(*) FROM customer_invoices WHERE invoice_date >= '2025-09-01' AND invoice_date < '2025-10-01';
Common mistake
Losing the last day. With timestamps, BETWEEN … '2025-09-30' stops at midnight on the 30th.
SQL for Analysis / Module 4 / Recap
Recap card
4.1 Three ways to count
Three ways to count
COUNT(*): rows.COUNT(column): rows where column is not NULL.COUNT(DISTINCT column): different values.
Syntax
SELECT COUNT(*), COUNT(column),
COUNT(DISTINCT column)
FROM table_name;Example
SELECT COUNT(*), COUNT(campaign_ref),
COUNT(DISTINCT campaign_ref)
FROM enquiries;Common mistake
Using COUNT(column) to count rows. It skips blanks: 208, not 328.
SQL for Analysis / Module 4 / Recap
Recap card
4.2 SUM, AVG and GROUP BY
SUM, AVG and GROUP BY
SUM AVG MIN MAXsummarise a column.- With GROUP BY you get one summary per group.
strftime('%Y-%m', date)gives the month.
Syntax
SELECT group_col, SUM(value) FROM table_name GROUP BY group_col;
Example
SELECT strftime('%Y-%m', invoice_date) AS month,
SUM(net_amount) AS invoiced
FROM customer_invoices
GROUP BY month;Common mistake
A column that is neither grouped nor summarised. Every SELECT column must be in GROUP BY or inside SUM, COUNT, AVG…
SQL for Analysis / Module 4 / Recap
Recap card
4.3 Averages that mislead
Averages that mislead
- A few large values pull an average up.
- Report averages per group when groups differ.
- Divide totals rather than averaging averages.
Example
SELECT SUM(net_amount) / COUNT(*) FROM customer_invoices;
Common mistake
Averaging averages. €4,784 instead of the true €5,115.
SQL for Analysis / Module 4 / Recap
Recap card
4.4 Shares, per cent and points
Shares, per cent and points
CASE WHEN … THEN 1 ELSE 0 ENDcounts matching rows.- Share = part × 100.0 ÷ total.
- Changes in shares are points.
Syntax
SELECT SUM(CASE WHEN condition
THEN 1 ELSE 0 END) * 100.0
/ COUNT(*)
FROM table_name;Example
SELECT SUM(CASE WHEN channel = 'WEBSHOP'
THEN 1 ELSE 0 END) * 100.0
/ COUNT(*)
FROM sales_orders;Common mistake
Calling a points change a per cent change. 27.5% to 26.7% is 0.8 points.
SQL for Analysis / Module 4 / Recap
Recap card
4.5 Match finance's number
Match finance's number
- Net = invoiced − credit notes.
- Reconcile step by step and size each step.
Example
SELECT SUM(amount) FROM credit_notes WHERE issue_date >= '2025-07-01' AND issue_date < '2025-10-01';
Common mistake
Comparing gross with net. A gap of exactly the credit notes is not an error.
SQL for Analysis / Module 4 / Recap
Recap card
Module 4: Counting and summing correctly
Three ways to count
COUNT(*): rows.COUNT(column): rows where column is not NULL.COUNT(DISTINCT column): different values.
Syntax
SELECT COUNT(*), COUNT(column),
COUNT(DISTINCT column)
FROM table_name;Example
SELECT COUNT(*), COUNT(campaign_ref),
COUNT(DISTINCT campaign_ref)
FROM enquiries;Common mistake
Using COUNT(column) to count rows. It skips blanks: 208, not 328.
SUM, AVG and GROUP BY
SUM AVG MIN MAXsummarise a column.- With GROUP BY you get one summary per group.
strftime('%Y-%m', date)gives the month.
Syntax
SELECT group_col, SUM(value) FROM table_name GROUP BY group_col;
Example
SELECT strftime('%Y-%m', invoice_date) AS month,
SUM(net_amount) AS invoiced
FROM customer_invoices
GROUP BY month;Common mistake
A column that is neither grouped nor summarised. Every SELECT column must be in GROUP BY or inside SUM, COUNT, AVG…
Averages that mislead
- A few large values pull an average up.
- Report averages per group when groups differ.
- Divide totals rather than averaging averages.
Example
SELECT SUM(net_amount) / COUNT(*) FROM customer_invoices;
Common mistake
Averaging averages. €4,784 instead of the true €5,115.
Shares, per cent and points
CASE WHEN … THEN 1 ELSE 0 ENDcounts matching rows.- Share = part × 100.0 ÷ total.
- Changes in shares are points.
Syntax
SELECT SUM(CASE WHEN condition
THEN 1 ELSE 0 END) * 100.0
/ COUNT(*)
FROM table_name;Example
SELECT SUM(CASE WHEN channel = 'WEBSHOP'
THEN 1 ELSE 0 END) * 100.0
/ COUNT(*)
FROM sales_orders;Common mistake
Calling a points change a per cent change. 27.5% to 26.7% is 0.8 points.
Match finance's number
- Net = invoiced − credit notes.
- Reconcile step by step and size each step.
Example
SELECT SUM(amount) FROM credit_notes WHERE issue_date >= '2025-07-01' AND issue_date < '2025-10-01';
Common mistake
Comparing gross with net. A gap of exactly the credit notes is not an error.
SQL for Analysis / Module 5 / Recap
Recap card
5.1 How the tables fit
How the tables fit
- Event tables (invoices) point to description tables (customers).
- Many invoices to one customer: many to one.
Common mistake
Not knowing which side is "many". That is where double counting starts.
SQL for Analysis / Module 5 / Recap
Recap card
5.2 INNER and LEFT JOIN
INNER and LEFT JOIN
JOIN … ONkeeps matching rows.LEFT JOINkeeps every left row; unmatched get NULL.- Short table aliases:
AS i.
Syntax
SELECT a.col, b.col FROM table_a AS a LEFT JOIN table_b AS b ON a.key = b.key;
Example
SELECT c.customer_ref FROM customers AS c LEFT JOIN customer_invoices AS i ON c.customer_ref = i.customer_ref WHERE i.invoice_ref IS NULL;
Common mistake
A WHERE on the right table after a LEFT JOIN. It throws away the unmatched rows you wanted, unless you test for IS NULL.
SQL for Analysis / Module 5 / Recap
Recap card
5.3 Joins that double count
Joins that double count
- Joining to a "many" table repeats the "one" side.
- Sums of repeated columns are inflated, with no error.
Common mistake
Summing invoice amounts after joining to lines. €30.9m instead of €18.5m.
SQL for Analysis / Module 5 / Recap
Recap card
5.4 Fix the double count
Fix the double count
- Sum the measure of the most detailed table.
- Count with DISTINCT on the key.
- SUM(DISTINCT …) is not a fix.
Syntax
SELECT SUM(detail.amount) FROM header JOIN detail ON header.key = detail.key;
Example
SELECT SUM(l.line_amount) FROM customer_invoices AS i JOIN customer_invoice_lines AS l ON i.invoice_ref = l.invoice_ref;
Common mistake
SUM(DISTINCT amount). Equal amounts on different invoices are merged.
SQL for Analysis / Module 5 / Recap
Recap card
Module 5: Joining tables safely
How the tables fit
- Event tables (invoices) point to description tables (customers).
- Many invoices to one customer: many to one.
Common mistake
Not knowing which side is "many". That is where double counting starts.
INNER and LEFT JOIN
JOIN … ONkeeps matching rows.LEFT JOINkeeps every left row; unmatched get NULL.- Short table aliases:
AS i.
Syntax
SELECT a.col, b.col FROM table_a AS a LEFT JOIN table_b AS b ON a.key = b.key;
Example
SELECT c.customer_ref FROM customers AS c LEFT JOIN customer_invoices AS i ON c.customer_ref = i.customer_ref WHERE i.invoice_ref IS NULL;
Common mistake
A WHERE on the right table after a LEFT JOIN. It throws away the unmatched rows you wanted, unless you test for IS NULL.
Joins that double count
- Joining to a "many" table repeats the "one" side.
- Sums of repeated columns are inflated, with no error.
Common mistake
Summing invoice amounts after joining to lines. €30.9m instead of €18.5m.
Fix the double count
- Sum the measure of the most detailed table.
- Count with DISTINCT on the key.
- SUM(DISTINCT …) is not a fix.
Syntax
SELECT SUM(detail.amount) FROM header JOIN detail ON header.key = detail.key;
Example
SELECT SUM(l.line_amount) FROM customer_invoices AS i JOIN customer_invoice_lines AS l ON i.invoice_ref = l.invoice_ref;
Common mistake
SUM(DISTINCT amount). Equal amounts on different invoices are merged.
