A question about mixing the (relatively new) “fetch first” syntax with “select for update” appeared a few days ago on the Oracle Developer Forum. The requirement was for a query something like:
select * from t1 order by n1 fetch first 10 rows only for update ;
The problem with this query is that it results in Oracle raising error ORA-02014: cannot select FOR UPDATE from view with DISTINCT, GROUP BY, etc. The error doesn’t seem to be particularly relevant, of course, until you remember that “fetch first” creates an inline view using the analytic row_number() under the covers.
One suggested solution was to use PL/SQL to open a cursor with a pure select then use a loop to lock each row in turn. This would need a little defensive programming, of course, since each individual “select for update” would be running at a different SCN from the driving loop, and there would be some risk of concurrency problems (locking, or competing data change) occuring.
There is a pure – thought contorted – SQL solution though where we take the driving SQL and put it into a subquery that generates the rowids of the rows we want to lock, as follows:
select /*+ qb_name(main) */ * from t1 where t1.rowid in ( select /*+ qb_name(inline) unnest no_merge */ t1a.rowid from t1 t1a order by t1a.n1 fetch first 10 rows only ) for update ;
The execution plan for this query is critical – so once you can get it working it would be a good idea to create a baseline (or SQL Patch) and attach it to the query. It is most important that the execution plan should be the equivalent of the following:
select /*+ qb_name(main) */ * from t1 where t1.rowid in ( select /*+ qb_name(inline) unnest no_merge */ t1a.rowid from t1 t1a order by t1a.n1 fetch first 10 rows only ) for update Plan hash value: 1286935441 --------------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem | --------------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | 10 |00:00:00.01 | 190 | | | | | 1 | FOR UPDATE | | 1 | | 10 |00:00:00.01 | 190 | | | | | 2 | BUFFER SORT | | 2 | | 20 |00:00:00.01 | 178 | 2048 | 2048 | 2048 (0)| | 3 | NESTED LOOPS | | 1 | 10 | 10 |00:00:00.01 | 178 | | | | |* 4 | VIEW | | 1 | 10 | 10 |00:00:00.01 | 177 | | | | |* 5 | WINDOW SORT PUSHED RANK | | 1 | 10000 | 10 |00:00:00.01 | 177 | 2048 | 2048 | 2048 (0)| | 6 | TABLE ACCESS FULL | T1 | 1 | 10000 | 10000 |00:00:00.01 | 177 | | | | | 7 | TABLE ACCESS BY USER ROWID| T1 | 10 | 1 | 10 |00:00:00.01 | 1 | | | | --------------------------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 4 - filter("from$_subquery$_003"."rowlimit_$$_rownumber"<=10) 5 - filter(ROW_NUMBER() OVER ( ORDER BY "T1A"."N1")<=10)
Critically you need the VIEW operation to be the driving query of a nested loop join that does the “table access by user rowid” joinback. In my case the query has used a full tablescan to identify the small number of rowids needed – in a production system that would be the part of the statement that should first be optimised.
It’s an unfortunate feature of this query structure (made messier by the internal rewrite for the analytic function) that it’s not easy to generate a correct set of hints to force the plan until after you’ve already managed to get the plan. Here’s the outline information that shows the messiness of the hints I would have needed:
Outline Data ------------- /*+ BEGIN_OUTLINE_DATA IGNORE_OPTIM_EMBEDDED_HINTS OPTIMIZER_FEATURES_ENABLE('12.2.0.1') DB_VERSION('12.2.0.1') ALL_ROWS OUTLINE_LEAF(@"INLINE") OUTLINE_LEAF(@"SEL$A3F38ADC") UNNEST(@"SEL$1") OUTLINE(@"INLINE") OUTLINE(@"MAIN") OUTLINE(@"SEL$1") NO_ACCESS(@"SEL$A3F38ADC" "from$_subquery$_003"@"SEL$1") ROWID(@"SEL$A3F38ADC" "T1"@"MAIN") LEADING(@"SEL$A3F38ADC" "from$_subquery$_003"@"SEL$1" "T1"@"MAIN") USE_NL(@"SEL$A3F38ADC" "T1"@"MAIN") FULL(@"INLINE" "T1A"@"INLINE") END_OUTLINE_DATA */
You’ll notice that my /*+ unnest */ hint is now modified – for inclusion at the start of the query – to /*+ unnest(@sel1) */ rather than the /*+ unnest(@inline) */ that you might have expected. That’s the side effect of the optimizer doing the “fetch first” rewrite before applying “missing” query block names. If I wanted to write a full hint set into the query itself (leaving the qb_name() hints in place but removing the unnest and merge I had originally) I would need the following:
/*+ unnest(@sel$1) leading(@sel$a3f38adc from$_subquery$_003@sel$1 t1@main) use_nl( @sel$a3f38adc t1@main) rowid( @sel$a3f38adc t1@main) */
I did make a bit of a fuss about the execution plan. I think it’s probably very important that everyone who runs this query gets exactly the same plan and the plan should be this nested loop. Although there’s a BUFFER SORT at operation 2 that is probably ensuring that every would get the same data in the same order regardless of the execution plan before locking any of it, I would be a little worried that different plans might somehow be allowed to lock the data in a different order, thus allowing for deadlocks.