SQL Coding · Cheat Sheet
NULLs and Aggregation
Cheatsheet: NULLs and Aggregation
One page for Topic 2. NULL means "unknown" — not zero, not empty string, not false. That one sentence explains every behavior below.
Three-valued logic: how NULL behaves
| Expression | Result | Why |
|---|---|---|
discount = NULL | Never true (zero rows) | NULL isn't equal to anything, including itself |
discount != NULL | Never true (zero rows) | Same — no comparison against NULL succeeds |
discount IS NULL | True for missing values | The only correct missing-value check |
discount IS NOT NULL | True for present values | Correct presence check |
country != 'US' | Silently drops NULL rows | "Not US" excludes unknown-country rows instead of including them |
AVG(discount) | Ignores NULL rows entirely | NULL rows aren't averaged as 0 — they're skipped |
SUM(discount) | Skips NULL rows | Same skip rule as AVG |
NULL vs 0 is the most consequential choice for business metrics. If 40% of orders store "no discount" as NULL, AVG(discount_pct) averages only the discounted orders and looks far higher than the true all-orders average. Store 0 instead, and the identical data yields a much lower number.
The COUNT family
| Function | Counts | NULLs included? |
|---|---|---|
COUNT(*) | Every row | Yes |
COUNT(col) | Rows where col is not NULL | No |
COUNT(DISTINCT col) | Unique non-NULL values of col | No |
Fast data-quality check — run both side by side:
SELECT COUNT(*) AS total_rows, COUNT(email) AS rows_with_email
FROM signups
If the numbers differ, some signups have no email — COUNT(*) hides that, COUNT(email) reveals it. And when a dashboard claims "10,000 users have email addresses," verify the query used COUNT(email), not COUNT(*) on a table that happens to be about users.
COALESCE: making the NULL choice explicit
COALESCE(col, fallback) returns the first non-NULL argument — the standard way to treat missing as zero on purpose:
SELECT AVG(COALESCE(discount_pct, 0)) AS avg_discount_all_orders
FROM orders
Now every order counts in the average. Whether that's right depends on what the metric claims to measure — the point is the choice is now visible in the code instead of hidden in storage conventions.
GROUP BY: the grain rule
The GROUP BY clause answers "one row per what?" — that's the grain, and it determines what every number means.
| GROUP BY | Grain | Effect |
|---|---|---|
region | One row per region | Totals across all time |
region, month | One row per region-month | Far more rows; each number smaller and more specific |
Rules:
- Every column in SELECT that isn't inside an aggregate (SUM, COUNT, AVG...) must appear in GROUP BY. Some databases allow violations and silently pick an arbitrary row's value — essentially random per group.
- When two metrics that "should match" don't, check the grain first. One query grouped by month, the other by week — both individually correct, answering different questions.
- A GROUP BY finer than the question asked (region + month + product when asked for region + month) answers a narrower question, even though every number is correct at that grain.
HAVING vs WHERE
| WHERE | HAVING | |
|---|---|---|
| Runs | Before grouping | After aggregation |
| Filters | Individual rows | Entire groups |
| Can reference aggregates? | No — WHERE SUM(revenue) > x errors | Yes — that's its job |
SELECT region, SUM(revenue) AS total_revenue
FROM sales
WHERE order_date >= '2026-01-01' -- drop old rows first
GROUP BY region
HAVING SUM(revenue) > 100000 -- then drop small regions
They run at different stages and cannot be swapped.
Traps
= NULLor!= NULLanywhere in a WHERE clause — that condition returns zero rows every time, with no error or warning.AVG()/SUM()on a column with unstated NULLs — ask whether missing values are stored as NULL or 0; the two give different answers from identical data.!=used to mean "everything except X" — it silently drops the unknowns too.COUNT(*)reported as if it wereCOUNT(some_column)— verify which one the query actually ran.- A non-aggregated SELECT column missing from GROUP BY — arbitrary value per group on lenient databases.
- Comparing metrics from two queries without checking they GROUP BY the same grain.
- Aggregate condition placed in WHERE instead of HAVING — error at best, wrong result at worst.
Aligned to the PostgreSQL documentation and standard SQL semantics.
Make this cheat sheet yours
Personalize this sheet — focus it however you study, or build one from the exact questions you keep getting wrong.