Fetch First performing worse than Rownum

I understand recent Hibernate versions use the ‘fetch first n rows’ clause as the default limit handler for Oracle. Having recently moved to Hibernate 6.6, I am facing some trouble with this - the fetch first clause leads to significantly poorer query performance with higher query cost as opposed to the older ‘rownum’ clause. In one particular case, the fetch first query is 244 times as costly as the rownum version. The two queries are otherwise completely identical and are being executed on the exact same database (Oracle 19c SE2, FTR).

I know that using the legacy Oracle dialect would throw me back to the old ‘rownum’ way, but I would not prefer having to go down this route just because I am facing an issue with this particular case.

Any ideas on why this could be happening? Also, it would be great if someone could tell me why the fetch first clause was adopted as the default within Hibernate - e.g. any resources that document that this is somehow categorically better than the older ‘rownum’ way.

I can also post the queries and query plans with costs, if that helps. Thanks!

If I were you, I would ask Oracle to fix that in their database. Having said that, you will have to show us examples of HQL queries and the SQL which performs bad.
There is a possibility to switch back to the rownum rendering also in newer Oracle versions, and for some feature combinations it’s even necessary to do so, but we try to use the new syntax when possible.

Thanks for your input :slight_smile: Here’s the SQL that I get out of hibernate that exhibits the reported performance issue:

SELECT m_table0_.*
FROM m_table m_table0_
LEFT OUTER JOIN m_table m_table1_ ON
(TO_NUMBER(substr(m_table0_.column1, 23))= m_table1_.column2
	OR TO_NUMBER(substr(m_table0_.column7, 23))= m_table1_.column2
		AND (m_table0_.column1 IS NULL))
LEFT OUTER JOIN m_table user2_ ON
(TO_NUMBER(substr(m_table0_.column3, 23))= user2_.column2)
WHERE (m_table0_.column4 IN (12 , 3 , 8 , 4 , 2 , 6 , 0 , 7)
	OR (m_table0_.column4 IN (7 , 8))
		AND m_table0_.column7 = 'value1')
AND (m_table0_.column6 IS NULL
	OR m_table0_.column6 = 0)
AND (m_table0_.column5 NOT IN (50 , 1))
AND m_table0_.column8 <> 'name'
ORDER BY CASE WHEN m_table0_.column4 = '2'
OR m_table0_.column4 = '6' THEN 1
ELSE 2
END, lower(m_table0_.column8) ASC 
NULLS LAST, m_table0_.column9, m_table0_.column2 
FETCH FIRST 1 ROWS ONLY;

Also, when you talk about a “possibility to switch back to the rownum rendering” within newer Oracle versions, does it allow the use of rownum regardless of whether the SQL query uses rownum or fetch first? I ask this because Hibernate will force the use of fetch first, so this Oracle feature would have to override that and use rownum instead.

Please also share a schema and query plans.
Yes, we can always force the rownum rendering if we determine that it is faster.

Here’s the plan from the fetch first query:


-----------------------------------------------------------------------------------------------------
| Id  | Operation                 | Name            | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT          |                 |     1 | 18946 |       |    32M  (1)| 00:21:26 |
|   1 |  SORT ORDER BY            |                 |     1 | 18946 |       |    32M  (1)| 00:21:26 |
|*  2 |   VIEW                    |                 |     1 | 18946 |       |    32M  (1)| 00:21:26 |
|*  3 |    WINDOW SORT PUSHED RANK|                 | 32120 |    13M|    13M|    32M  (1)| 00:21:26 |
|   4 |     MERGE JOIN OUTER      |                 | 32120 |    13M|       |    32M  (1)| 00:21:26 |
|   5 |      NESTED LOOPS OUTER   |                 | 32120 |    13M|       |  1028   (1)| 00:00:01 |
|*  6 |       TABLE ACCESS FULL   | m_table         | 32120 |    12M|       |  1026   (1)| 00:00:01 |
|*  7 |       INDEX UNIQUE SCAN   | SYS_C0015524    |     1 |     6 |       |     0   (0)| 00:00:01 |
|   8 |      BUFFER SORT          |                 |     1 |       |       |    32M  (1)| 00:21:26 |
|   9 |       VIEW                | VW_LAT_18E0BD02 |     1 |       |       |  1024   (1)| 00:00:01 |
|* 10 |        TABLE ACCESS FULL  | m_table         |     1 |   421 |       |  1024   (1)| 00:00:01 |
-----------------------------------------------------------------------------------------------------

Here’s the plan from the ‘rownum’ variant:


---------------------------------------------------------------------------------------------------------------
| Id  | Operation                           | Name            | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                    |                 |     1 | 17893 |       |   135K  (1)| 00:00:06 |
|*  1 |  COUNT STOPKEY                      |                 |       |       |       |            |          |
|   2 |   VIEW                              |                 | 64216 |  1095M|       |   135K  (1)| 00:00:06 |
|*  3 |    SORT ORDER BY STOPKEY            |                 | 64216 |    26M|    27M|   135K  (1)| 00:00:06 |
|   4 |     MERGE JOIN OUTER                |                 | 64216 |    26M|       |   129K  (1)| 00:00:06 |
|   5 |      NESTED LOOPS OUTER             |                 | 32108 |    13M|       |  1028   (1)| 00:00:01 |
|*  6 |       TABLE ACCESS FULL             | m_table         | 32108 |    12M|       |  1026   (1)| 00:00:01 |
|*  7 |       INDEX UNIQUE SCAN             | SYS_C0015524    |     1 |     6 |       |     0   (0)| 00:00:01 |
|   8 |      BUFFER SORT                    |                 |     2 |       |       |   135K  (1)| 00:00:06 |
|   9 |       VIEW                          | VW_LAT_12222494 |     2 |       |       |     4   (0)| 00:00:01 |
|  10 |        CONCATENATION                |                 |       |       |       |            |          |
|* 11 |         FILTER                      |                 |       |       |       |            |          |
|* 12 |          TABLE ACCESS BY INDEX ROWID| m_table         |     1 |   420 |       |     2   (0)| 00:00:01 |
|* 13 |           INDEX UNIQUE SCAN         | SYS_C0015524    |     1 |       |       |     1   (0)| 00:00:01 |
|* 14 |         TABLE ACCESS BY INDEX ROWID | m_table         |     1 |   420 |       |     2   (0)| 00:00:01 |
|* 15 |          INDEX UNIQUE SCAN          | SYS_C0015524    |     1 |       |       |     1   (0)| 00:00:01 |

Also, please let me know if you have any inputs on how to get the ‘rownum forcing’ feature switched on within Oracle - that’d be super helpful.

Hello @orm_s,
Please provide the answers to these questions:

1/ Could you please provide the output of the runtime execution
Have a look at examples C and D of https://blogs.oracle.com/optimizer/post/how-to-generate-a-useful-sql-execution-plan

2/ Would you have this parameter set at the database level?
_optimizer_cbqt_or_expansion=off

If so, could you please by setting it to on at the session level and provide again the output as in 1/

ALTER SESSION SET "_optimizer_cbqt_or_expansion"=on;

Thanks