Conditional aggregation applies CASE inside an aggregate to calculate selected categories or measures in one grouped query.
A dashboard often needs paid count, pending count and revenue beside one another. Repeated queries can answer them separately, but conditional aggregation expresses related measures over the same input while making each category rule visible.
This reference adds detail to the main course. You can read it independently: the small database below supplies every table and row used in the examples. Start with the question each query answers, then compare the returned values with the data rather than memorising a line of syntax.
Set up the practice database
Run this setup once in a new practice database, before the examples. It uses four customers and six sales. Ana has two sales, Ben has one, Cara has two, and Dev has none.
Sale 106 has no customer link; sale 104 has an unknown amount. These deliberate test cases expose missing information and unmatched relationships.
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name VARCHAR(40) NOT NULL,
city VARCHAR(40),
manager_id INTEGER
);
CREATE TABLE sales (
id INTEGER PRIMARY KEY,
customer_id INTEGER,
amount DECIMAL(10,2),
status VARCHAR(20) NOT NULL,
created_on DATE NOT NULL
);
INSERT INTO customers (id,name,city,manager_id) VALUES
(1,'Ana','Leeds',NULL),(2,'Ben','Leeds',1),
(3,'Cara','Porto',1),(4,'Dev',NULL,2);
INSERT INTO sales (id,customer_id,amount,status,created_on) VALUES
(101,1,10,'paid','2026-01-01'),
(102,1,30,'pending','2026-01-02'),
(103,2,20,'paid','2026-01-02'),
(104,3,NULL,'pending','2026-02-01'),
(105,3,20,'paid','2026-02-02'),
(106,NULL,5,'paid','2026-02-03');An amount of zero would be a known value. The NULL amount here means the value is unknown. A customer with no matching sale differs from a sale whose customer_id is NULL.
The primary keys identify individual records even when amounts or cities repeat. Dates use year-month-day format. The examples specify sorting whenever order matters; insertion order alone does not guarantee display order.
How Conditional aggregation works
CASE chooses a value for each input row, and the surrounding aggregate combines those chosen values. SUM(CASE WHEN status='paid' THEN 1 ELSE 0 END) counts paid records by adding one for a match and zero for a nonmatch. The same shape can add amounts instead of ones. COUNT(CASE WHEN status='paid' THEN 1 END) also counts paid rows because the omitted ELSE yields NULL for nonmatches and COUNT(expression) ignores NULL.
Do not add ELSE 0 inside that COUNT expression when you intend to count only matches. Zero is non-NULL, so COUNT(CASE ... ELSE 0 END) counts every input row. SUM and COUNT treat zero differently because their jobs differ: SUM adds its numeric contribution, whereas COUNT counts its presence. Read both the CASE and aggregate together before assuming two expressions are equivalent.
WHERE conditions versus CASE conditions
Place a condition in WHERE when it should restrict every measure in the report. Place it inside CASE when it should restrict only one measure while other rows remain available for other columns. WHERE status='paid' followed by a pending-count CASE always produces zero pending matches, because the pending rows were removed before aggregation. A shared date range belongs in WHERE when all status measures refer to the same period.
Categories may overlap if their conditions overlap. A high-value count and a paid count can both count the same sale, which is valid when they are separate measures but misleading if presented as mutually exclusive buckets. Use a single ordered CASE when each row must enter exactly one category. Missing amounts deserve a separate bucket if completeness matters. A condition amount >= 20 is unknown on sale 104, so its ELSE branch receives the row unless you write a dedicated IS NULL branch first.
MySQL, PostgreSQL, SQL Server and SQLite
CASE inside COUNT or SUM is supported by all four systems. PostgreSQL and SQLite also support aggregate FILTER syntax, but MySQL and SQL Server do not share that syntax. CASE is the portable form for these conditional measures. Boolean-expression arithmetic is another dialect-sensitive shortcut to avoid in a shared reference.
Check the engine-specific syntax and version requirements in PostgreSQL aggregate expressions.
Practice task and worked solution
Count known amounts and missing amounts as two separate columns using conditional aggregation.
Try the task before reading its worked solution. The supplied rows make the expected answer checkable by hand.
SELECT
SUM(CASE WHEN amount IS NOT NULL THEN 1 ELSE 0 END) AS known_count,
SUM(CASE WHEN amount IS NULL THEN 1 ELSE 0 END) AS missing_count
FROM sales;known_count | missing_count
5 | 1The two predicates are complementary. Every sale contributes exactly one to one column, so five plus one equals the six-row population. A known zero would contribute to known_count, not missing_count, which preserves the difference between absence and a real numeric value.
Replace the paid-count SUM with COUNT(CASE WHEN status='paid' THEN 1 ELSE 0 END) and predict six. Remove ELSE 0 and predict four. Then add a shared paid-status WHERE to the multi-status summary and explain why pending_count becomes zero: its records have been removed before every CASE expression gets a chance to evaluate them.
SQL Conditional Aggregation: SUM CASE and COUNT CASE Syntax
Conditional aggregation
SELECT
SUM(CASE WHEN status='paid' THEN 1 ELSE 0 END) AS paid_count,
SUM(CASE WHEN status='pending' THEN 1 ELSE 0 END) AS pending_count,
SUM(CASE WHEN status='paid' THEN amount ELSE 0 END) AS paid_total
FROM sales;SQL Conditional Aggregation: SUM CASE and COUNT CASE Summary
- Put
CASEinside an aggregate to count or total only some rows, while other columns use all rows. SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END)counts matches;SUM(CASE WHEN ... THEN amount END)totals them.- With
COUNT(CASE ...), non-matches must produceNULL, so leave outELSE 0. - A condition in
WHERErestricts every measure; a condition insideCASErestricts only its own column. - PostgreSQL and SQLite also support
COUNT(*) FILTER (WHERE ...);CASEis the portable form.
SQL Conditional Aggregation: SUM CASE and COUNT CASE Examples
Each example shows the code, the output it produces and what each part does.
Paid and pending counts with the paid total
Each sale contributes to the count matching its status, while only paid amounts contribute to the paid total. All six rows remain available to the independent expressions. The unknown pending amount does not change either status count.
SELECT
SUM(CASE WHEN status='paid' THEN 1 ELSE 0 END) AS paid_count,
SUM(CASE WHEN status='pending' THEN 1 ELSE 0 END) AS pending_count,
SUM(CASE WHEN status='paid' THEN amount ELSE 0 END) AS paid_total
FROM sales;paid_count | pending_count | paid_total
4 | 2 | 55Common SQL Conditional Aggregation: SUM CASE and COUNT CASE Mistakes
COUNT with ELSE 0 counts every row
COUNT(CASE WHEN status='paid' THEN 1 ELSE 0 END) returns six, not four, because all six chosen values are non-NULL. Use SUM with one and zero or COUNT with a NULL nonmatch. This mistake can survive a test containing only paid rows, so include nonmatching examples.
SQL Conditional Aggregation: SUM CASE and COUNT CASE in Real Projects
An operations summary in one query
A single operations summary can show the number of paid and pending transactions with the corresponding known amounts. Independent conditions make the metrics easy to audit. If the categories are expected to partition the dataset, check that their counts add to the total and provide a category for unexpected statuses.
List whether report categories are exclusive or overlapping. Paid count and high-value count may legitimately share a transaction, while paid and pending status buckets are intended to partition this dataset. Keep a total count and an unexpected-status measure when the source vocabulary can evolve, so newly introduced statuses do not silently disappear from a dashboard.
SQL Conditional Aggregation: SUM CASE and COUNT CASE FAQ
Does CASE inside SUM create groups?
No. GROUP BY still determines the grouping grain; CASE only controls each row's contribution. Without GROUP BY you get one summary row, and with GROUP BY customer_id you get the same measures per customer.
Can one record contribute to several measures?
Yes, when the separate conditions overlap, for example "paid" and "over 20". That can be exactly what you want, but make it intentional, and don't expect overlapping measures to add up to the total.
Why did every record get counted?
Inspect whether COUNT received a non-NULL ELSE value such as 0. COUNT counts every non-NULL value, including zeros, so COUNT(CASE WHEN ... THEN 1 ELSE 0 END) counts all rows. Remove the ELSE, or switch to SUM.
How do I turn rows into columns with conditional aggregation?
That's a pivot: one SUM(CASE WHEN month = '2026-01' THEN amount END) AS jan column per category. It works in every database but needs one expression per column, so the list of columns must be known in advance. The pivoting lesson covers it in detail.
Can I calculate a percentage with conditional aggregation?
Yes. Divide a conditional count by the total count in the same query: 100.0 * SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) / COUNT(*). Using 100.0 keeps the division decimal, and NULLIF(COUNT(*), 0) guards an empty group.
SQL Conditional Aggregation: SUM CASE and COUNT CASE Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
What are the known_count and missing_count in the NULL-bucket exercise?
SQL Conditional Aggregation: SUM CASE and COUNT CASE Quiz
2 questions to check what you learned.
Which expression counts only matching rows?
- COUNT(CASE WHEN condition THEN 1 ELSE 0 END)
- COUNT(0), which counts every present constant
- COUNT(CASE WHEN condition THEN 1 END)
Where should a condition go if it affects only one summary column?
- Inside that column’s CASE expression
- In a shared WHERE that removes rows from every measure
- In the final ORDER BY, which decides contribution categories