Easier filtering of window functions with the Qualify clause in oracle AI Database 26ai

Introduction

Analytic functions also known as window functions, compute an aggregate value over a group of related rows. Unlike traditional aggregate functions they return a result for each row in the result set and not a summary. Prior to 26ai, filtering on the results of an analytic function required the use of a CTE or subquery. With 26ai, the QUALIFY clause helps to filter data without the need for an inline view or subquery when using analytic functions. Consider the below example for illustration

Example

The example below was taken from Leetcode, database problem #185. You are tasked with finding the highest earners for a company’s departments. This is defined as ” an employee who has a salary in the top three unique salaries for that department.”
The tables and sample data for the test are created as follows

SQL> CREATE TABLE IF NOT EXISTS employee (
  2      id           NUMBER,
  3      name         VARCHAR2(255),
  4      salary       NUMBER,
  5      departmentid NUMBER
  6* );

Table EMPLOYEE created.

SQL> 
SQL> CREATE TABLE IF NOT EXISTS department (
  2      id   NUMBER,
  3      name VARCHAR2(255)
  4* ) ;

Table DEPARTMENT created.

SQL> insert into Employee (id, name, salary, departmentId) values ('1', 'Joe', '85000', '1');

1 row inserted.

SQL> insert into Employee (id, name, salary, departmentId) values ('2', 'Henry', '80000', '2');

1 row inserted.

SQL> insert into Employee (id, name, salary, departmentId) values ('3', 'Sam', '60000', '2');

1 row inserted.

SQL> insert into Employee (id, name, salary, departmentId) values ('4', 'Max', '90000', '1');

1 row inserted.

SQL> insert into Employee (id, name, salary, departmentId) values ('5', 'Janet', '69000', '1');

1 row inserted.

SQL> insert into Employee (id, name, salary, departmentId) values ('6', 'Randy', '85000', '1');

1 row inserted.

SQL> insert into Employee (id, name, salary, departmentId) values ('7', 'Will', '70000', '1');

1 row inserted.

SQL> insert into Department (id, name) values ('1', 'IT');

1 row inserted.

SQL> INSERT INTO department (id, name) VALUES ('2','Sales');

1 row inserted.

SQL> 
SQL> commit;

Commit complete.


Pre 26ai solution

The question expects ties to have the same “rank”, so I used the dense rank analytic function for my solution as below;

SQL> l
  1  SELECT 
  2         department as "Department", 
  3         employee   as "Employee", 
  4         salary     as "Salary"
  5  FROM
  6  (
  7   SELECT 
  8       d.name department
  9      ,e.name employee
 10      ,e.salary
 11    ,dense_rank() OVER (PARTITION BY d.name ORDER BY e.salary DESC) as rnk
 12  From
 13                 Employee e 
 14      INNER JOIN Department d on e.departmentid = d.id
 15  ) 
 16* WHERE rnk <= 3
SQL> /

Department Employee Salary 
__________ ________ ______ 
IT         Max       90000 
IT         Joe       85000 
IT         Randy     85000 
IT         Will      70000 
Sales      Henry     80000 
Sales      Sam       60000 

6 rows selected. 

As seen above, I am nesting my initial select with the dense_rank in a subquery and using an outer query to filter on the rnk column for values that are less than or equal to 3. Using previous oracle versions you needed to do something along these lines to filter on the results of an analytic function. It was not possible to filter directly on the analytic function.

26ai solution

With oracle AI database 26ai we can filter on the analytic function using the qualify clause. The cool thing about this is it also let’s us use the alias in the filter, ie “rnk” in this case instead of having to type out the entire expression. This is also possible for the group by and having clauses in this release as well. Using the qualify clause, my query would look something like this

SQL> SELECT
  2       d.name department
  3      ,e.name employee
  4      ,e.salary
  5    ,dense_rank() OVER (PARTITION BY d.name ORDER BY e.salary DESC) as rnk
  6  From
  7                 Employee e
  8      INNER JOIN Department d on e.departmentid = d.id
  9  QUALIFY rnk <= 3
 10* ;

DEPARTMENT EMPLOYEE SALARY RNK 
__________ ________ ______ ___ 
IT         Max       90000   1 
IT         Joe       85000   2 
IT         Randy     85000   2 
IT         Will      70000   3 
Sales      Henry     80000   1 
Sales      Sam       60000   2 

This returns the results of the top earners per department correctly. However there is now an additional column in the results, which would make our solution wrong(on leetcode atleast). Luckily we can put the analytic function expression right in the qualify clause only

QL> l
  1  SELECT
  2       d.name department
  3      ,e.name employee
  4      ,e.salary
  5    -- ,dense_rank() OVER (PARTITION BY d.name ORDER BY e.salary DESC) as rnk
  6  From
  7                 Employee e
  8      INNER JOIN Department d on e.departmentid = d.id
  9* QUALIFY dense_rank() OVER (PARTITION BY d.name ORDER BY e.salary DESC) <= 3
SQL> /

DEPARTMENT EMPLOYEE SALARY 
__________ ________ ______ 
IT         Max       90000 
IT         Joe       85000 
IT         Randy     85000 
IT         Will      70000 
Sales      Henry     80000 
Sales      Sam       60000 

6 rows selected. 

We now get the same results as before without having to use a subquery or CTE.

Alternative to Where?

I wouldn’t quite say so myself (because I don’t know any better) but it is interesting that a query like this:

SQL> l
  1  SELECT  d.name, e.name
  2  FROM  department d 
  3  JOIN  employee   e ON d.id = e.departmentid
  4* WHERE d.name = 'IT'
SQL> /

NAME NAME  
____ _____ 
IT   Joe   
IT   Max   
IT   Janet 
IT   Randy 
IT   Will  

Can also be written like this:

SQL> ed
  1  SELECT  d.name, e.name
  2  FROM  department d 
  3  JOIN  employee   e ON d.id = e.departmentid
  4* QUALIFY d.name = 'IT';
SQL> /

NAME NAME  
____ _____ 
IT   Joe   
IT   Max   
IT   Janet 
IT   Randy 
IT   Will  

both queries returning the same result. It’s interesting but probably not its intended usage.

CONCLUSION

The qualify clause eases filtering on window functions in oracle database, a welcome feature I’m certain many a developer will appreciate. Apparently this has been available in some other RDBMSes like Teradata already, in any case it will reduce the effort to re-write code if you are moving over to oracle from a different vendor (or vice versa 🤷‍♂️). If you’re new to analytics there is a free course on the devgym that explains how to use them.

Thanks for reading, cheers 🎊

References:

https://medium.com/snowflake/how-qualify-works-with-in-depth-explanation-and-examples-bbde9fc742db

https://oracle-base.com/articles/26/qualify-clause-26

Leave a Comment

Your email address will not be published. Required fields are marked *