Some optimistic locking methods, including the SCN method, can detect intervening updates even when column values have not really changed. With the SCN method, we can use flashback queries to confirm that real changes occurred.
Flashback queries are evil
As soon as we mention flashback queries, some wise DBAs will immediately object that such queries harm the “shared pool”. This is a serious objection; let me explain why.
Before executing a SQL statement, Oracle must parse it and establish an execution plan: this is called the “hard parse”. Hard parsing can be an expensive operation, and it can be a bottleneck since it uses resources that must be serialized. To avoid constant parsing, Oracle stores executable forms of SQL cursors and PL/SQL programs in the “library cache”, which is part of the shared pool. This enables “parse once, execute many”, a prerequisite for good performance in OLTP databases.
Imagine a SQL statement that must be hard parsed before every execution. That one statement will use up resources and slow down parsing of other new statements. Not good. Now imagine that the “cursor” resulting from that parse goes into the library cache: the cache will get bigger and bigger and slower to navigate, until finally other cursors start getting aged out. We now have a dysfunctional database that spends its time hard parsing instead of executing statements.
Unfortunately, one of those statements that gets hard parsed every time is the AS OF
flashback query :-(
To demonstrate this, I’ll run a simple AS OF
query in a PL/SQL block, using the same SCN every time. Note that I flush the shared pool beforehand, so there will always be a first hard parse.
declare l_scn number; l_cnt number; begin dbms_flashback.disable; execute immediate 'alter system flush shared_pool'; l_scn := dbms_flashback.get_system_change_number; for i in 1..10 loop select count(*) into l_cnt from T as of scn l_scn; end loop; end; /
Now I’ll query V$SQL_SHARED_CURSOR
to see what cursors could not be “shared” and had to be hard parsed over and over again.
select child_number, parse_calls, executions, reason "Reason: AS OF SCN" from v$sql left join V$SQL_SHARED_CURSOR using(sql_id, child_number) where sql_text like 'SELECT COUNT(*) FROM T%'; CHILD_NUMBER PARSE_CALLS EXECUTIONS Reason: AS OF SCN ------------ ----------- ---------- -------------------------------------------------------------------------------- 0 1 1 <ChildNode><ChildNumber>0</ChildNumber><ID>21</ID><reason>Flashback cursor(1)</r 1 1 1 <ChildNode><ChildNumber>1</ChildNumber><ID>21</ID><reason>Flashback cursor(1)</r 2 1 1 <ChildNode><ChildNumber>2</ChildNumber><ID>21</ID><reason>Flashback cursor(1)</r 3 1 1 <ChildNode><ChildNumber>3</ChildNumber><ID>21</ID><reason>Flashback cursor(1)</r 4 1 1 <ChildNode><ChildNumber>4</ChildNumber><ID>21</ID><reason>Flashback cursor(1)</r 5 1 1 <ChildNode><ChildNumber>5</ChildNumber><ID>21</ID><reason>Flashback cursor(1)</r 6 1 1 <ChildNode><ChildNumber>6</ChildNumber><ID>21</ID><reason>Flashback cursor(1)</r 7 1 1 <ChildNode><ChildNumber>7</ChildNumber><ID>21</ID><reason>Flashback cursor(1)</r 8 1 1 <ChildNode><ChildNumber>8</ChildNumber><ID>21</ID><reason>Flashback cursor(1)</r 9 1 1 Formatted content of first Reason: <ChildNode> <ChildNumber>0</ChildNumber> <ID>21</ID> <reason>Flashback cursor(1)</reason> <size>3x4</size> <Flashback_cursor>0</Flashback_cursor> <As_of_cursor>1</As_of_cursor> <Flashback_session>0</Flashback_session> </ChildNode>
Compare this to a normal query:
declare l_cnt number; begin execute immediate 'alter system flush shared_pool'; for i in 1..10 loop select count(*) into l_cnt from T; end loop; end; / select child_number, parse_calls, executions, reason "Reason: no flashback" from v$sql left join V$SQL_SHARED_CURSOR using(sql_id, child_number) where sql_text like 'SELECT COUNT(*) FROM T%'; CHILD_NUMBER PARSE_CALLS EXECUTIONS Reason: no flashback ------------ ----------- ---------- -------------------- 0 1 10
Flashback queries are evil – not all the time
Instead of adding AS OF SCN
to every table in my query, I can use DBMS_FLASHBACK.ENABLE_AT_SYSTEM_CHANGE_NUMBER
to set the “consistent read” SCN for every table in every query. Let’s see what happens with this technique:
declare l_scn number; l_cnt number; begin dbms_flashback.disable; execute immediate 'alter system flush shared_pool'; l_scn := dbms_flashback.get_system_change_number - 1001; for i in 1..10 loop dbms_flashback.enable_at_system_change_number(l_scn+i); select count(*) into l_cnt from T; dbms_flashback.disable; end loop; end; / select child_number, parse_calls, executions, reason "DBMS_FLASHBACK, increasing SCN" from v$sql left join V$SQL_SHARED_CURSOR using(sql_id, child_number) where sql_text like 'SELECT COUNT(*) FROM T%'; CHILD_NUMBER PARSE_CALLS EXECUTIONS DBMS_FLASHBACK, increasing SCN ------------ ----------- ---------- ------------------------------ 0 10 10
Notice that there are 10 “parse calls” here. Oracle did a “soft parse”, that is it looked in the library cache for a cursor that could be reused, and it found one. This is extra work as compared to a normal query, but not nearly as much as a hard parse. More importantly, the library cache is not flooded with useless child cursors.
Conclusion
It turns out that we can safely use this technique under certain conditions:
- The
FLASHBACK
andSELECT
privileges must be granted on whatever tables we want to query; - The
EXECUTE
privilege must be granted on DBMS_FLASHBACK; - The SCN parameter of
ENABLE_AT_SYSTEM_CHANGE_NUMBER
must be increasing! If it decreases, we get hard parses and extra child cursors.
This should never be a problem in the “optimistic locking” use case.