Module 13 · SQL for Data Analysis
CTEs
Break complex questions into named steps with WITH, and avoid double-counting when combining totals.
About 30 minutes
The problem
The finance director asks for something that sounds simple:
"How much does each customer still owe us?"
What a customer owes is what we charged for delivered shipments, minus what they've paid. Charges live in shipments; payments live in payments, and one shipment can have several payments. Getting this right takes several steps, and doing it in one tangled query is how analysts end up with wrong numbers.
The concept
A CTE (common table expression) is a named, temporary result you define at the top of a query with WITH, and then use like a table.
SQL
WITH step_one AS (
SELECT …
),
step_two AS (
SELECT … FROM step_one …
)
SELECT … FROM step_two;A CTE does the same job as a subquery in FROM, but you read it top to bottom, like a recipe, and you can use each step more than once. CTEs only exist while the query runs.
Example
WITH charged AS (
SELECT customer_id, SUM(freight_charge) AS total_charged
FROM shipments
WHERE status = 'Delivered'
GROUP BY customer_id
),
paid AS (
SELECT s.customer_id, SUM(p.amount) AS total_paid
FROM payments AS p
JOIN shipments AS s ON s.shipment_id = p.shipment_id
GROUP BY s.customer_id
)
SELECT
c.company_name,
ch.total_charged,
COALESCE(pd.total_paid, 0) AS total_paid,
ch.total_charged - COALESCE(pd.total_paid, 0) AS balance_owed
FROM charged AS ch
JOIN customers AS c ON c.customer_id = ch.customer_id
LEFT JOIN paid AS pd ON pd.customer_id = ch.customer_id
ORDER BY balance_owed DESC
LIMIT 10;Walkthrough
chargedadds up the charges for delivered shipments, one row per customer.paidadds up payments, one row per customer. Payments don't store the customer, so we join toshipmentsto find it.- The final query joins the two summaries to
customersand subtracts. LEFT JOIN paidkeeps customers who haven't paid anything, andCOALESCE(pd.total_paid, 0)turns their NULL into 0 so the subtraction works.
Why two separate summaries? If you joined shipments and payments first and then summed, every shipment with two payments would have its charge counted twice. Aggregating each table on its own and then joining the totals avoids that double-counting. It's one of the most common mistakes in real reports.
Practice
Practice
Using a CTE named monthly with one row per month of 2025 (month as 'YYYY-MM' and shipments as the count), return only the months with more than 130 shipments.
Ctrl + Enter to run. Tab indents; press Esc then Tab to leave the editor.
Challenge
Challenge · optional
Which shipments are delivered but have received no payment at all? Show shipment_id, customer company_name and freight_charge, largest charge first. Use a CTE for the set of shipments that have payments.
Ctrl + Enter to run. Tab indents; press Esc then Tab to leave the editor.
Check your understanding
Answer every question to check.