In partnership with

|
DATA WITH SARAH
|
|
A stakeholder asks:
“Can you send me a list of our active customers and their total revenue?”
On the surface, that sounds simple enough.
You have a customer table, an orders table, maybe an order items table, and you know how to join them.
So you write the query, run it, and get a result.
The only problem is the customer count is much higher than expected.
This is where I usually stop looking at the SQL itself and start looking at the logic behind it.
|
|
Before I continue, just sharing a small ad. A simple click helps support my newsletter, so thank you! 😊
100+ Claude Code hacks to ship code 10X faster
Top engineers at Anthropic and OpenAI say AI now writes 100% of their code.
If you're not using AI, you're spending 40 hours doing what they do in 4.
Sign up for The Code and get:
100+ Claude Code hacks used by top engineers — free
The Code newsletter — learn the latest AI tools, tips, and skills to code faster with AI in 5 minutes a day
Now, back to what I was saying…
|
FIRST CHECK
What does one row actually represent?
|
|
If the customer table has one row per customer, but the orders table has one row per order, the result will naturally create multiple rows for the same customer.
Add an order items table and now you may have several rows for every order too.
That does not automatically mean the join is wrong. It does mean I need to be very clear about the grain of the final dataset and where I am doing my aggregation.
SELECT
COUNT(*) AS total_rows,
COUNT(DISTINCT customer_id) AS customers
FROM customer_revenue;
If I expected one row per customer and I have 12,000 rows for 4,000 customers, I know I need to look more closely.
|
|
|
|
NEXT
Go back to the request.
“Active customers” sounds specific, but it usually is not.
|
Does active mean:
They currently have an open subscription?
They placed an order recently?
They logged in during the last 30 days?
They were active at any point during the reporting period?
|
The same thing applies to “total revenue.”
Lifetime revenue? Revenue this year? Gross revenue? Revenue after refunds?
Before I change the query, I want those definitions clear.
|
|
|
|
THEN
Decide where the aggregation should happen.
If I need one row per customer, I may calculate revenue at the customer level first and join that result back to the customer table.
WITH revenue_by_customer AS (
SELECT
o.customer_id,
SUM(oi.item_revenue) AS total_revenue
FROM orders o
JOIN order_items oi
ON o.order_id = oi.order_id
GROUP BY o.customer_id
)
SELECT
c.customer_id,
r.total_revenue
FROM customers c
LEFT JOIN revenue_by_customer r
ON c.customer_id = r.customer_id;
Now the revenue result already has one row per customer before I join it back. That makes the final dataset much easier to reason through.
|
|
|
|
BEFORE I SEND IT
I still want to validate it.
I would compare the result with something I already trust.
Check the active customer count against an existing report.
Compare total revenue with a finance number.
Look at a handful of customers directly in the source system.
Check unusually high values, missing revenue, duplicate customers, and anything that changed more than expected.
This is one of the reasons I think learning SQL syntax is only part of the job. A query can be completely valid and still answer the wrong version of the question.
|
|
|
|
THE PART I USE ALL THE TIME
Know what needs to be defined before you trust the result.
What does active mean?
Which revenue?
What period?
What is the grain?
Which table is the source of truth?
Can the join duplicate anything?
What should I validate the result against?
|
What is one data request you have received that sounded simple at first and turned into something much bigger?
|
|
And if you missed the free checklist, grab it here:
Affiliate disclosure: I may earn a commission if you purchase through links in this post, at no extra cost to you.