Same Execution Plan. Different Performance. Oracle 26ai and the FILTER Mystery




Oracle 26ai introduced a new clause for aggregate functions - FILTER. Filter allows aggregates to be calculated in a single query for different filtering conditions.

Let's see how it works.

Test data

We create a simple table T and load some data into it, 100,000,000 rows.

drop table if exists t;

create table t as with gen_rows as ( select  mod(rownum, 30) as col from dual connect by level <= 1000000),
q as ( select mod(rownum, 30) as col from dual connect by level <= 100) select rownum id, gen_rows.col col from gen_rows, q; 


The old-style query - CASE

Let's now count the number of rows where col = 10 and at the same time the number of rows where col = 20.
In Oracle versions older than 26ai, we had to use the CASE clause for this.

select count(*),

```
   count( case when col = 10 then 1 else null end ) cnt_10,
   count( case when col = 20 then 1 else null end) cnt_20
```

from t;

COUNT(*)     CNT_10     CNT_20 ---------- ---------- ----------
100000000    3333400    3333300 


Using the FILTER clause

In version 26ai, we get a new feature, the FILTER clause, which simplifies this construction somewhat.

select count(*),

```
   count(*) filter (where col = 10) cnt_10,
   count(*) filter (where col = 20) cnt_20
```

from t;

COUNT(*)     CNT_10     CNT_20 ---------- ---------- ----------
100000000    3333400    3333300 

Looks good. But what about performance?

In my tests on 100,000,000 rows, the query using the CASE clause turns out to be twice as fast! (Of course, results may be slightly different with other data.)

The average execution time for the query using the FILTER clause in my tests is 6-8 seconds, while the query using the CASE clause takes 3-4 seconds. 

But why? Shouldn't these be the same query?
Let's check whether the FILTER transformation actually converts the queries to a CASE clause.
begin

    SYS.DBMS_SQLDIAG.DUMP_TRACE(p_sql_id =>  '89r73qnvn5rjh',
            p_child_number => 0, 
            p_component => 'Compiler',
            p_file_id => 'trace_filter');

end;
/ 

Query after transformation
************************************
SELECT COUNT(*) "COUNT(*)", 
     COUNT(CASE WHEN "EMP"."DEPARTMENT_ID"=10 THEN 1 ELSE NULL END ) "CNT_10",
     COUNT(CASE WHEN "EMP"."DEPARTMENT_ID"=20 THEN 1 ELSE NULL END ) "CNT_20"
FROM "HR"."EMP" "EMP";


Nope. The queries are identical.

Comparing the execution plans

Maybe the Optimizer generated different execution plans?

Plan for the query using CASE.

---------------------------------------------------------------------------

| Id  | Operation          | Name | Rows  | Bytes | Cost (%CPU)| Time     | 
|   0 | SELECT STATEMENT   |      |       |       | 52852 (100)|          |
|   1 |  SORT AGGREGATE    |      |     1 |     3 |            |          |
|   2 |   TABLE ACCESS FULL| T    |   100M|   286M| 52852   (1)| 00:00:03 | 

Column Projection Information (identified by operation id): 

1 - (#keys=0) COUNT(CASE "COL" WHEN 20 THEN 1 ELSE NULL END )[22], COUNT(CASE "COL" WHEN 10 THEN 1 ELSE NULL END )[22], COUNT(*)[22]
2 - (rowset=256) "COL"[NUMBER,22] 

Plan for the query using the FILTER clause.
---------------------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |       |       | 52852 (100)|          |
|   1 |  SORT AGGREGATE    |      |     1 |     3 |            |          |
|   2 |   TABLE ACCESS FULL| T    |   100M|   286M| 52852   (1)| 00:00:03 |
---------------------------------------------------------------------------

Column Projection Information (identified by operation id): 

1 - (#keys=0) COUNT("COL" WHEN 20 THEN 1 ELSE NULL END )[22], COUNT(CASE "COL" WHEN 10 THEN 1 ELSE NULL END )[22], COUNT(*)[22]
2 - "COL"[NUMBER,22] 

Execution plans look identical. There is only one small detail in the Projection section: for the query using the CASE clause, the annotation (rowset=256) appears next to the TABLE ACCESS FULL operation. For the FILTER clause, this parameter is missing. 

What does it mean? 

I assume this is related to whether a row array can be created and passed to the parent node (for vector processing), instead of passing the data row by row. The latter approach is, obviously, slower. 
It looks like when the FILTER clause is used, rows are passed one by one, which can have a significant impact on performance with large tables.

A bug?

With the help of Nigel Bayliss, a bug was reported:
Bug 40060415 - QUERY WITH FILTER CLAUSE SLOWER THAN CASE STATEMENT EQUIVALENT - NOT USING ROWSETS

So be careful with this clause when working with large amounts of data :)



Komentarze