Unit 01.00: What one row means: finding the grain
Unit ID: SQL-M01-U01 - Estimated active time: 15-18 minutes Objective: state the grain of any table in one sentence, and detect when a query has changed it.
The question that prevents most SQL bugs
Before writing any query, answer this: "What does one row in this table represent?"
That answer is called the grain. Analysts who skip it produce numbers that look plausible and are wrong.
Look at two tables that both describe orders:
| Table | One row is… | Grain |
|---|---|---|
orders | one order placed by one customer | one order |
order_items | one product line inside an order | one order-item |
They sound similar. They are not. orders has 1,000 rows for 1,000 orders. order_items might have 3,400 rows for those same 1,000 orders. If you SUM(price) on the wrong one, you get a different revenue number - and both look like "revenue".
Why this is the #1 source of wrong answers
A revenue figure is only meaningful once you know what you counted. Consider:
-- Grain: one row per order
SELECT COUNT(*) AS order_count
FROM orders;
-- 1,000
-- Grain: one row per order LINE
SELECT COUNT(*) AS line_count
FROM order_items;
-- 3,400
Neither is "the number of sales". One is orders, one is lines. The word "sales" is ambiguous; the grain is not.
Worked example
A manager asks: *"What was our average order value last month?"*
The trap: order_items has a price column, so it is tempting to write AVG(price). That returns the average line price (₹531.91 in our data), not the average order value (₹2,718.53). The manager asked about orders. Run both against the starter database and compare.
Correct approach - collapse to the order grain first, then average:
SELECT AVG(order_total) AS avg_order_value
FROM (
SELECT order_id, SUM(price * quantity) AS order_total
FROM order_items
GROUP BY order_id
) AS per_order;
The inner query changes the grain from *order-item* to *order*. The outer query averages at the grain the question actually asked about.
Non-example
This is not a grain problem:
SELECT COUNT(*) FROM customers WHERE country = 'IN';
One row is one customer, you are counting customers, and the filter does not change the grain. Naming the grain here takes two seconds and confirms there is nothing to worry about. That is the point - it is a cheap check.
Practice
For each table, write the grain as a single sentence beginning "One row is…":
payments(payment_id, order_id, amount, paid_at)page_views(view_id, user_id, url, viewed_at)daily_active_users(date, user_count)
Then answer: which of the three can you SUM a money column on without any further work?
Check your answer
- One row is one payment against one order. (An order may have several payments.)
- One row is one page view by one user at one moment.
- One row is one day, already aggregated.
You can SUM(amount) on payments directly to get total money received - the grain matches the question. Table 3 is already aggregated, so summing user_count across days double-counts people who returned.
Takeaway
State the grain before you write the query. If a query changes the grain (most JOINs and GROUP BYs do), say the new grain out loud before trusting any number from it.
---
