Overview
The FILTER clause is a nice addition to the SQL syntax in Oracle 26ai (23.26.1). It allows to restrict the rows considered by aggregate functions.
An aggregate query divides a set of records into groups, and returns a single result row for each group.
By default, the input for every aggregate function in the query includes all the records of the group.
However, sometimes we want the input to a specific aggregate function in the query to be only a subset of the group’s records. The FILTER clause provides an elegant solution for this requirement.
Example
As an example, I’ll use the EUROVISION_WINNERS table that I created for my Pattern Matching presentation.
This table contains a record for every year of the Eurovision Song Contest, with some details about the winner of that year.
SQL> select * from eurovision_winners order by year;
YEAR WINNER SONG PERFORMER POINTS LANGUAGE HOST
---------- --------------- ----------------------------------- -------------------------------------- ---------- ------------- --------------
1956 Switzerland Refrain Lys Assia French Switzerland
1957 Netherlands Net als toen Corry Brokken 31 Dutch Germany
1958 France Dors, mon amour André Claveau 27 French Netherlands
1959 Netherlands Een beetje Teddy Scholten 21 Dutch France
1960 France Tom Pillibi Jacqueline Boyer 32 French United Kingdom
1961 Luxembourg Nous les amoureux Jean-Claude Pascal 31 French France
1962 France Un premier amour Isabelle Aubret 26 French Luxembourg
1963 Denmark Dansevise Grethe and Jørgen Ingmann 42 Danish United Kingdom
1964 Italy Non ho l'età Gigliola Cinquetti 49 Italian Denmark
1965 Luxembourg Poupée de cire, poupée de son France Gall 32 French Italy
1966 Austria Merci, Chérie Udo Jürgens 31 German Luxembourg
1967 United Kingdom Puppet on a String Sandie Shaw 47 English Austria
1968 Spain La, la, la Massiel 29 Spanish United Kingdom
1969 Netherlands De troubadour Lenny Kuhr 18 Dutch Spain
1970 Ireland All Kinds of Everything Dana 32 English Netherlands
1971 Monaco Un banc, un arbre, une rue Séverine 128 French Ireland
1972 Luxembourg Après toi Vicky Leandros 128 French United Kingdom
1973 Luxembourg Tu te reconnaîtras Anne-Marie David 129 French Luxembourg
1974 Sweden Waterloo ABBA 24 English United Kingdom
1975 Netherlands Ding-a-dong Teach-In 152 English Sweden
1976 United Kingdom Save Your Kisses for Me Brotherhood of Man 164 English Netherlands
1977 France L'oiseau et l'enfant Marie Myriam 136 French United Kingdom
1978 Israel A-Ba-Ni-Bi Izhar Cohen and the Alphabeta 157 Hebrew France
1979 Israel Hallelujah Gali Atari and Milk and Honey 125 Hebrew Israel
1980 Ireland What's Another Year Johnny Logan 143 English Netherlands
1981 United Kingdom Making Your Mind Up Bucks Fizz 136 English Ireland
1982 Germany Ein bißchen Frieden Nicole 161 German United Kingdom
1983 Luxembourg Si la vie est cadeau Corinne Hermès 142 French Germany
1984 Sweden Diggi-Loo Diggi-Ley Herreys 145 Swedish Luxembourg
1985 Norway La det swinge Bobbysocks! 123 Norwegian Sweden
1986 Belgium J'aime la vie Sandra Kim 176 French Norway
1987 Ireland Hold Me Now Johnny Logan 172 English Belgium
1988 Switzerland Ne partez pas sans moi Céline Dion 137 French Ireland
1989 Yugoslavia Rock Me Riva 137 Croatian Switzerland
1990 Italy Insieme: 1992 Toto Cutugno 149 Italian Yugoslavia
1991 Sweden Fångad av en stormvind Carola 146 Swedish Italy
1992 Ireland Why Me? Linda Martin 155 English Sweden
1993 Ireland In Your Eyes Niamh Kavanagh 187 English Ireland
1994 Ireland Rock 'n' Roll Kids Paul Harrington and Charlie McGettigan 226 English Ireland
1995 Norway Nocturne Secret Garden 148 Norwegian Ireland
1996 Ireland The Voice Eimear Quinn 162 English Norway
1997 United Kingdom Love Shine a Light Katrina and the Waves 227 English Ireland
1998 Israel Diva Dana International 172 Hebrew United Kingdom
1999 Sweden Take Me to Your Heaven Charlotte Nilsson 163 English Israel
2000 Denmark Fly on the Wings of Love Olsen Brothers 195 English Sweden
2001 Estonia Everybody Tanel Padar, Dave Benton and 2XL 198 English Denmark
2002 Latvia I Wanna Marie N 176 English Estonia
2003 Turkey Everyway That I Can Sertab Erener 167 English Latvia
2004 Ukraine Wild Dances Ruslana 280 Ukrainian Turkey
2005 Greece My Number One Helena Paparizou 230 English Ukraine
2006 Finland Hard Rock Hallelujah Lordi 292 English Greece
2007 Serbia Molitva Marija Šerifovic 268 Serbian Finland
2008 Russia Believe Dima Bilan 272 English Serbia
2009 Norway Fairytale Alexander Rybak 387 English Russia
2010 Germany Satellite Lena 246 English Norway
2011 Azerbaijan Running Scared Ell & Nikki 221 English Germany
2012 Sweden Euphoria Loreen 372 English Azerbaijan
2013 Denmark Only Teardrops Emmelie de Forest 281 English Sweden
2014 Austria Rise Like a Phoenix Conchita Wurst 290 English Denmark
2015 Sweden Heroes Måns Zelmerlöw 365 English Austria
2016 Ukraine 1944 Jamala 534 Crimean Tatar Sweden
2017 Portugal Amar pelos dois Salvador Sobral 758 Portuguese Ukraine
2018 Israel Toy Netta Barzilai 529 English Portugal
2019 Netherlands Arcade Duncan Laurence 498 English Israel
2021 Italy Zitti e buoni M†neskin 524 Italian Netherlands
2022 Ukraine Stefania Kalush Orchestra 631 Ukrainian Italy
2023 Sweden Tattoo Loreen 583 English United Kingdom
2024 Switzerland The Code Nemo 591 English Sweden
2025 Austria Wasted Love JJ 436 English Switzerland
69 rows selected.
I want to analyze the languages of the winning songs. For each language, I’d like to get:
- The total number of wins
- The average points for wins in the 20th century
- The average points for wins in the 21st century
- The last year in which the winning country also hosted the contest
The first aggregate – the total number of wins – is simple, because it considers all the group’s records.
But for the other aggregates we need to consider only a subset of the group’s records.
Pre-26ai Solution
In versions before 26ai, a common practice to achieve this leveraged the fact that aggregate functions ignore NULLs.
So, instead of using AVG(POINTS) or MAX(YEAR), we apply the functions on expressions that return POINTS or YEAR for the records we want to consider, and NULL for all the records we want to filter out.
SQL> select language, 2 count(*) total_wins, 3 round(avg(case when year<=2000 then points end)) average_points_20th_century, 4 round(avg(case when year>2000 then points end)) average_points_21st_century, 5 max(case when winner=host then year end) last_win_at_home 6 from eurovision_winners 7 group by language 8 order by language; TOTAL AVG_POINTS AVG_POINTS LAST_WIN LANGUAGE WINS 20TH_CENTURY 21ST_CENTURY AT_HOME ------------- ----- ------------ ------------ ---------- Crimean Tatar 1 534 Croatian 1 137 Danish 1 42 Dutch 3 23 English 33 146 341 1994 French 13 94 1973 German 2 96 Hebrew 3 151 1979 Italian 3 99 524 Norwegian 2 136 Portuguese 1 758 Serbian 1 268 Spanish 1 29 Swedish 2 146 Ukrainian 2 456 15 rows selected.
The FILTER Clause
In 26ai, the FILTER clause provides a dedicated solution for this requirement.
We can add the required filter to each individual aggregate function as a Boolean condition:
SQL> select language, 2 count(*) total_wins, 3 round(avg(points) filter (where year<=2000)) average_points_20th_century, 4 round(avg(points) filter (where year>2000)) average_points_21st_century, 5 max(year) filter (where winner=host) last_win_at_home 6 from eurovision_winners 7 group by language 8 order by language; TOTAL AVG_POINTS AVG_POINTS LAST_WIN LANGUAGE WINS 20TH_CENTURY 21ST_CENTURY AT_HOME ------------- ----- ------------ ------------ ---------- Crimean Tatar 1 534 Croatian 1 137 Danish 1 42 Dutch 3 23 English 33 146 341 1994 French 13 94 1973 German 2 96 Hebrew 3 151 1979 Italian 3 99 524 Norwegian 2 136 Portuguese 1 758 Serbian 1 268 Spanish 1 29 Swedish 2 146 Ukrainian 2 456 15 rows selected.
I like it! This syntax conveys the goal that we want to achieve in a more clear and straightforward way.
Under the Hood
Let’s see how the new syntax is implemented. By using DBMS_UTILITY.EXPAND_SQL_TEXT, we can see that the new syntax is simply converted into the pre-26ai solution:
SQL> var x clob SQL> begin 2 dbms_utility.expand_sql_text( 3 input_sql_text => ' 4 select language, 5 count(*) total_wins, 6 round(avg(points) filter (where year<=2000)) average_points_20th_century, 7 round(avg(points) filter (where year>2000)) average_points_21st_century, 8 max(year) filter (where winner=host) last_win_at_home 9 from eurovision_winners 10 group by language 11 order by language', 12 output_sql_text => :x); 13 end; 14 / PL/SQL procedure successfully completed. SQL> print x -- I formatted the output to make it more readable X -------------------------------------------------------------------------------- SELECT "A1"."LANGUAGE" "LANGUAGE", COUNT(*) "TOTAL_WINS", ROUND(AVG(CASE WHEN "A1"."YEAR"<=2000 THEN "A1"."POINTS" ELSE NULL END )) "AVERAGE_POINTS_20TH_CENTURY", ROUND(AVG(CASE WHEN "A1"."YEAR">2000 THEN "A1"."POINTS" ELSE NULL END )) "AVERAGE_POINTS_21ST_CENTURY", MAX(CASE WHEN "A1"."WINNER"="A1"."HOST" THEN "A1"."YEAR" ELSE NULL END ) "LAST_WIN_AT_HOME" FROM "DEMO2"."EUROVISION_WINNERS" "A1" GROUP BY "A1"."LANGUAGE" ORDER BY "A1"."LANGUAGE"
This is an approach that Oracle uses quite often – supporting new syntax by converting the SQL statement into an equivalent statement that was already supported.
I think this is a clever approach. It requires relatively little effort and carries low risk.
If you’d like to see more examples of this approach, I wrote a post about it several years ago: EXPAND_SQL_TEXT – Much More Than Just Expanding Views.
The HAVING Clause
Since aggregate functions can also be used in the HAVING clause, we can obviously use the FILTER clause there as well. Let’s write a query that returns for each language:
- The total number of wins
- The number of unique winners
- The list of years in which the winning country also hosted the contest
But this time we don’t want to get all the languages, but only the ones that had at least 2 wins in the 20th century. We’ll use the HAVING clause for that.
SQL> select language,
2 count(*) total_wins,
3 count(distinct winner) unique_winners,
4 listagg(year,',') within group (order by year) filter (where winner=host) wins_at_home
5 from eurovision_winners
6 group by language
7 having count(*) filter (where year<=2000) >= 2
8 order by language;
TOTAL UNIQUE WINS
LANGUAGE WINS WINNERS AT_HOME
------------- ----- ------- --------------------
Dutch 3 1
English 33 17 1993,1994
French 13 5 1956,1973
German 2 2
Hebrew 3 1 1979
Italian 3 1
Norwegian 2 1
Swedish 2 1
8 rows selected.
Subqueries
Note that one of the documented restrictions is that we cannot use subqueries in the FILTER clause. So, for example, although the following query is supported…
SQL> select language,
2 count(*) total_wins,
3 count(case when not exists(select null from eurovision_winners w2 where w2.winner=w1.winner and w2.year!=w1.year) then 1 end) one_time_winners
4 from eurovision_winners w1
5 group by language
6 order by language;
TOTAL ONE_TIME
LANGUAGE WINS WINNERS
------------- ----- --------
Crimean Tatar 1 0
Croatian 1 1
Danish 1 0
Dutch 3 0
English 33 7
French 13 2
German 2 0
Hebrew 3 0
Italian 3 0
Norwegian 2 0
Portuguese 1 1
Serbian 1 1
Spanish 1 1
Swedish 2 0
Ukrainian 2 0
15 rows selected.
…the equivalent query with FILTER is not supported:
SQL> select language, 2 count(*) total_wins, 3 count(*) filter (where not exists(select null from eurovision_winners w2 where w2.winner=w1.winner and w2.year!=w1.year)) one_time_winners 4 from eurovision_winners w1 5 group by language 6 order by language; count(*) filter (where not exists(select null from eurovision_winners w2 where w2.winner=w1.winner and w2.year!=w1.year)) one_time_winners * ERROR at line 3: ORA-02251: subquery not allowed here
Analytic Functions
The FILTER clause can also be used with aggregate functions that operate as analytic functions.
For example, let’s get for every year, in addition to the winning country and song, also the number of times the same winner won in the 20th century and in the 21st century:
SQL> select w.year, 2 w.winner, 3 w.song, 4 count(*) filter (where year<=2000) over (partition by winner) winner_wins_20th_century, 5 count(*) filter (where year>2000) over (partition by winner) winner_wins_21st_century 6 from eurovision_winners w 7 order by year; WINNER_WINS WINNER_WINS YEAR WINNER SONG 20TH_CENTURY 21ST_CENTURY ---------- --------------- ----------------------------------- ------------ ------------ 1956 Switzerland Refrain 2 1 1957 Netherlands Net als toen 4 1 1958 France Dors, mon amour 4 0 1959 Netherlands Een beetje 4 1 1960 France Tom Pillibi 4 0 1961 Luxembourg Nous les amoureux 5 0 1962 France Un premier amour 4 0 1963 Denmark Dansevise 2 1 1964 Italy Non ho l'età 2 1 1965 Luxembourg Poupée de cire, poupée de son 5 0 1966 Austria Merci, Chérie 1 2 1967 United Kingdom Puppet on a String 4 0 1968 Spain La, la, la 1 0 1969 Netherlands De troubadour 4 1 1970 Ireland All Kinds of Everything 7 0 1971 Monaco Un banc, un arbre, une rue 1 0 1972 Luxembourg Après toi 5 0 1973 Luxembourg Tu te reconnaîtras 5 0 1974 Sweden Waterloo 4 3 1975 Netherlands Ding-a-dong 4 1 1976 United Kingdom Save Your Kisses for Me 4 0 1977 France L'oiseau et l'enfant 4 0 1978 Israel A-Ba-Ni-Bi 3 1 1979 Israel Hallelujah 3 1 1980 Ireland What's Another Year 7 0 1981 United Kingdom Making Your Mind Up 4 0 1982 Germany Ein bißchen Frieden 1 1 1983 Luxembourg Si la vie est cadeau 5 0 1984 Sweden Diggi-Loo Diggi-Ley 4 3 1985 Norway La det swinge 2 1 1986 Belgium J'aime la vie 1 0 1987 Ireland Hold Me Now 7 0 1988 Switzerland Ne partez pas sans moi 2 1 1989 Yugoslavia Rock Me 1 0 1990 Italy Insieme: 1992 2 1 1991 Sweden Fångad av en stormvind 4 3 1992 Ireland Why Me? 7 0 1993 Ireland In Your Eyes 7 0 1994 Ireland Rock 'n' Roll Kids 7 0 1995 Norway Nocturne 2 1 1996 Ireland The Voice 7 0 1997 United Kingdom Love Shine a Light 4 0 1998 Israel Diva 3 1 1999 Sweden Take Me to Your Heaven 4 3 2000 Denmark Fly on the Wings of Love 2 1 2001 Estonia Everybody 0 1 2002 Latvia I Wanna 0 1 2003 Turkey Everyway That I Can 0 1 2004 Ukraine Wild Dances 0 3 2005 Greece My Number One 0 1 2006 Finland Hard Rock Hallelujah 0 1 2007 Serbia Molitva 0 1 2008 Russia Believe 0 1 2009 Norway Fairytale 2 1 2010 Germany Satellite 1 1 2011 Azerbaijan Running Scared 0 1 2012 Sweden Euphoria 4 3 2013 Denmark Only Teardrops 2 1 2014 Austria Rise Like a Phoenix 1 2 2015 Sweden Heroes 4 3 2016 Ukraine 1944 0 3 2017 Portugal Amar pelos dois 0 1 2018 Israel Toy 3 1 2019 Netherlands Arcade 4 1 2021 Italy Zitti e buoni 2 1 2022 Ukraine Stefania 0 3 2023 Sweden Tattoo 4 3 2024 Switzerland The Code 2 1 2025 Austria Wasted Love 1 2 69 rows selected.

One thought on “Aggregation Filters”