The FILTER Clause for Conditional Aggregation in Window Functions
AI generated
SELECT
JOIN
SQL / Aggregation
The FILTER Clause for Conditional Aggregation
How FILTER (WHERE ...) makes conditional aggregation more readable than nested CASE WHEN constructs

Conditional aggregation is one of the most common needs in reporting queries: how many orders are cancelled, how many completed, how many still open, all in a single row per group. The classic path there runs through COUNT combined with a CASE WHEN expression that returns either a value or null depending on the condition. The SQL standard has offered a noticeably clearer alternative for a while now: the FILTER clause, attached directly to the aggregate function. This article covers how FILTER works, why it beats CASE WHEN for readability in many cases, how it combines with window functions for several parallel metrics, and where database availability draws the line.

9 min read FILTER Clause Conditional Aggregation

1. The classic problem: CASE WHEN inside aggregate functions

To only consider certain rows within an aggregation, most developers intuitively reach for a combination of COUNT or SUM and an inner CASE WHEN expression. The aggregate function then only counts or sums the rows for which the CASE expression returns a value other than null, while every other row effectively drops out of the aggregation, since null values are ignored by COUNT, SUM, and most other aggregate functions.

This pattern works reliably and is available in practically every database, but it carries a noticeable readability cost once several such conditional aggregations sit side by side in the same query. The reader first has to mentally unwrap the CASE expression for every column before the actual aggregate function and its condition become visible, which quickly turns into an unwieldy wall of nested expressions with five or six parallel metrics.


SELECT
    customer_id,
    COUNT(CASE WHEN status = 'cancelled' THEN 1 END) AS cancelled_count,
    COUNT(CASE WHEN status = 'completed' THEN 1 END) AS completed_count,
    SUM(CASE WHEN status = 'completed' THEN total_amount END) AS completed_revenue
FROM orders
GROUP BY customer_id;

2. The FILTER clause: a declarative condition instead of CASE nesting

The FILTER clause is written directly after an aggregate function and contains a WHERE condition that determines which rows feed into the aggregation. Syntactically, FILTER (WHERE condition) is therefore an integral part of the aggregate function itself, not a transformation of the value being aggregated the way CASE WHEN is. This separation makes it immediately clear which aggregate function is combined with which condition, without the reader first having to decode the inner expression.

Functionally, FILTER is exactly equivalent to the COUNT CASE WHEN combination, the result never differs. The difference lies purely in readability and, as shown in the performance section, sometimes in the execution plan the optimizer produces. For teams writing many conditional aggregations in reporting queries, switching to FILTER is therefore usually a pure improvement with no functional downside.


SELECT
    customer_id,
    COUNT(*) FILTER (WHERE status = 'cancelled') AS cancelled_count,
    COUNT(*) FILTER (WHERE status = 'completed') AS completed_count,
    SUM(total_amount) FILTER (WHERE status = 'completed') AS completed_revenue
FROM orders
GROUP BY customer_id;

3. Several parallel metrics in a single query

The real value of the FILTER clause shows up in reporting queries that need to return many different metrics for the same grouping in a single result row, for example a dashboard that shows order count by status, revenue by status, and average order value by status per customer all at once. With CASE WHEN, every additional metric would mean another layer of nesting, with FILTER every row in the SELECT list stays independently readable.

Another practical advantage is that different aggregate functions with different filter conditions can be freely combined without needing to adjust the WHERE clause of the query itself. A single GROUP BY query can thus deliver counts, sums, and averages for any number of different subsets of the same underlying set at once, which is particularly valuable for dashboard backends, where exactly this kind of multi metric query is the norm rather than the exception.


SELECT
    customer_id,
    COUNT(*) FILTER (WHERE status = 'completed') AS completed_orders,
    COUNT(*) FILTER (WHERE status = 'cancelled') AS cancelled_orders,
    ROUND(AVG(total_amount) FILTER (WHERE status = 'completed'), 2)
        AS avg_completed_value,
    SUM(total_amount) FILTER (WHERE order_date >= CURRENT_DATE - INTERVAL '30 days')
        AS revenue_last_30_days
FROM orders
GROUP BY customer_id;

4. FILTER combined with window functions

The FILTER clause is not limited to classic GROUP BY aggregation, it can also be attached to aggregate functions used as window functions with an OVER clause. That makes it possible to compute running totals or moving averages that only consider part of the rows within the window, without needing an additional subquery or a second CASE expression.

A practical example is a running total of revenue from completed orders per customer, ordered by date, while cancelled orders are consistently ignored in the running total yet still appear as their own row in the result. This combination of FILTER and OVER is also possible with CASE WHEN, but becomes even harder to read due to the additional nesting inside window function syntax than it already is in the plain GROUP BY case.


SELECT
    order_id,
    customer_id,
    order_date,
    status,
    total_amount,
    SUM(total_amount) FILTER (WHERE status = 'completed')
        OVER (PARTITION BY customer_id ORDER BY order_date)
        AS running_completed_revenue
FROM orders
ORDER BY customer_id, order_date;

5. How the optimizer treats FILTER compared to CASE WHEN

In PostgreSQL, the FILTER clause often leads to a more efficient execution plan than the equivalent CASE WHEN formulation, because the optimizer recognizes the filter condition directly as standalone information and can partly use it for a more targeted selection of relevant rows, instead of only resolving the condition inside the aggregate function. With several simultaneously used filter conditions that partially overlap, the optimizer can also combine shared subsets more efficiently internally.

In practice, runtime differences on small to medium data volumes are usually small and rarely the decisive reason to switch. On very large reporting tables with many parallel FILTER conditions, it is still worth checking EXPLAIN ANALYZE, since even small differences in the execution plan can have a noticeable impact on the overall runtime of a reporting query once millions of rows are involved.

6. Database availability: standard SQL vs. proprietary alternatives

The FILTER clause has been part of the SQL standard since SQL:2003 and is fully implemented in PostgreSQL since version 9.4, as well as in SQLite since a comparatively early version. Both databases support the combination with window functions exactly as shown in the previous section, with no restrictions compared to plain GROUP BY usage.

MySQL and Microsoft SQL Server, on the other hand, still do not natively support the FILTER clause today, and the CASE WHEN combination remains the only available solution there. Oracle also has no FILTER in the sense of the SQL standard, but offers the KEEP addition together with special analytic functions as a partially similar but syntactically completely different alternative. Anyone writing database agnostic code should therefore deliberately use FILTER only for PostgreSQL or SQLite specific code paths and stick to CASE WHEN everywhere else.

7. FILTER combined with DISTINCT and multiple conditions

FILTER combines seamlessly with COUNT DISTINCT, for example to count the number of different products a customer bought exclusively in cancelled orders, while other order statuses are ignored for that same expression. This combination of DISTINCT and FILTER within the same aggregate function is also possible with a pure CASE WHEN solution, but noticeably more error prone, since a misplaced parenthesis can quickly produce a semantically wrong but still syntactically valid expression.

More complex filter conditions with several AND and OR combinations can be formulated inside the FILTER clause exactly the way they would inside a normal WHERE clause, including subqueries and function calls. That makes FILTER a full alternative for nearly any conceivable condition, not just simple equality checks like in the earlier examples.


SELECT
    customer_id,
    COUNT(DISTINCT product_id) FILTER (
        WHERE status = 'cancelled' AND total_amount > 50
    ) AS distinct_products_cancelled_high_value
FROM order_items oi
JOIN orders o ON o.order_id = oi.order_id
GROUP BY customer_id;

8. Do not confuse FILTER and WHERE: different evaluation points

A common misunderstanding treats FILTER as a plain replacement for WHERE. In reality the two operate on completely different levels: WHERE removes rows from the query entirely before any grouping happens, and therefore affects every aggregate function in the SELECT list equally. FILTER, on the other hand, only restricts a single aggregate function, while every other aggregate function in the same query still sees the full, unfiltered set of rows for that group.

This difference becomes relevant as soon as a query needs to return both a total count across all rows and a conditional partial count. If the condition is mistakenly placed in a WHERE clause instead of FILTER, not only does the desired partial count disappear, but the intended total count also shrinks down to the filtered subset, and groups that contain only rows outside the condition drop out of the result entirely because of the WHERE clause, whereas with FILTER they correctly survive with a value of zero for the conditional metric.


-- Wrong: WHERE removes rows for the entire query
SELECT customer_id, COUNT(*) AS total_orders
FROM orders
WHERE status = 'completed';  -- total_orders only counts completed!

-- Correct: FILTER only restricts the second metric
SELECT
    customer_id,
    COUNT(*) AS total_orders,
    COUNT(*) FILTER (WHERE status = 'completed') AS completed_orders
FROM orders
GROUP BY customer_id;

9. Migrating existing CASE WHEN code to FILTER

When migrating existing reporting queries from CASE WHEN to FILTER, a systematic approach pays off: first identify every aggregate function whose inner expression consists purely of a CASE WHEN construct with exactly one condition and an implicit or explicit ELSE null, since only those cases can be mechanically converted to FILTER without any behavior change. More complex CASE expressions with several WHEN branches and different return values are not pure filter logic and should not be rushed into a rewrite.

Since FILTER simply results in a syntax error on databases without native support, an automated test against every database version used in production is recommended before any migration. In mixed environments where both PostgreSQL and MySQL run side by side, a central query builder or an abstraction layer that automatically switches between FILTER and the equivalent CASE WHEN formulation depending on the target database proves useful.

Approach Syntax Readability with multiple metrics Database availability
CASE WHEN inside aggregate COUNT(CASE WHEN x THEN 1 END) drops with every added metric practically everywhere
FILTER clause COUNT(*) FILTER (WHERE x) stays consistently high PostgreSQL, SQLite
FILTER with window function SUM(x) FILTER (WHERE y) OVER (...) noticeably clearer than the CASE combination PostgreSQL, SQLite
Oracle KEEP addition MAX(x) KEEP (DENSE_RANK FIRST ...) different use case, not a true alternative Oracle only
MySQL / SQL Server no native FILTER CASE WHEN remains the only option not available

Mironsoft

Database optimization, query tuning, and migrations

SQL queries that keep getting slower as the data grows?

We analyze and optimize SQL databases regardless of the system in use, plan safe migrations and schema changes, and teach teams query optimization hands-on.

Query Optimization

Analyze slow queries and speed them up with purpose using indexes and explain plans.

Migration Planning

Execute schema changes and data migrations safely, without downtime.

Team Training

Anchor SQL fundamentals and performance thinking hands-on in the dev team.

10. Summary

The FILTER Clause: Key Takeaways

Declarative instead of nested

FILTER (WHERE ...) separates the filter condition syntactically from the aggregate function instead of hiding it inside a CASE expression.

Multiple metrics in parallel

Reporting queries with many conditional metrics stay noticeably more readable with FILTER than with nested CASE WHEN.

Also usable with window functions

FILTER can be combined directly with an OVER clause, for example for running totals over a subset of rows.

Not available everywhere

PostgreSQL and SQLite support FILTER, MySQL and SQL Server still require the CASE WHEN solution.

11. FAQ: The FILTER Clause: Key Takeaways

1What does the FILTER clause do technically differently from CASE WHEN?
FILTER (WHERE condition) is attached directly to the aggregate function and defines which rows feed into it. CASE WHEN instead transforms the value passed into the aggregate function, but the functional result is identical.
2Does FILTER produce a different result than the equivalent CASE WHEN solution?
No, with a correct formulation both variants produce exactly the same result. The difference lies purely in readability and sometimes in the resulting execution plan.
3Can I combine FILTER with COUNT DISTINCT?
Yes, FILTER combines seamlessly with DISTINCT inside the same aggregate function, for example to count distinct values within a filtered subset.
4Does MySQL support the FILTER clause?
No, MySQL still offers no native FILTER clause today. There the COUNT CASE WHEN combination remains the only available solution for conditional aggregation.
5Does FILTER also work with window functions?
Yes, FILTER can be attached directly to an aggregate function that additionally carries an OVER clause, for example for a running total over only part of the rows in the window.
6Since which PostgreSQL version is FILTER available?
Since PostgreSQL 9.4. The clause is fully implemented in every currently supported PostgreSQL version, including in combination with window functions.
7Does Oracle offer an equivalent to FILTER?
Not directly. Oracle offers KEEP combined with DENSE_RANK as a related but syntactically completely different feature for a different use case, not a true equivalent to FILTER.
8Is the FILTER clause faster than CASE WHEN?
In PostgreSQL the optimizer can sometimes process FILTER conditions more efficiently, but the difference is usually small on small to medium data volumes and not a reason to migrate on its own.
9Can FILTER contain complex conditions with multiple AND combinations?
Yes, any valid WHERE condition is allowed inside the FILTER clause, including multiple AND and OR combinations, subqueries, and function calls.
10How do I safely migrate existing CASE WHEN code to FILTER?
Only simple CASE expressions with exactly one condition and an implicit ELSE null can be mechanically converted. More complex CASE expressions with several branches should be reviewed manually before being rewritten.