Importance of DETERMINISTIC Function…in continuation to my blog on CBC Latches

This is in continuation to my previous blog on CBC Latch Contention. While writing about the customer issue, I missed out one metrics that is worth mentioning. The Top Queries posted in this blog were mainly from Business Object Reports, which generate the Business Intelligence Data for a branch. When I checked the metrics for one report, with Original Implementation, with 1 Execution of the report, the number of Execution of the query in the function, on S_BRANCH, were around 0.3 Million and the I/O’s for the Report Query was around 14 Million per execution and for the query on S_BRANCH it was just 4 per execution. After Implementing the change, for a single execution of the Original Report Query, the executions of the query in the function were just 87 and the I/O’s of the Original Query dropped from 14 Million to 0.2 Million.

I received several mail responses on this issue and many of them were surprised to know about the behaviour of this Function, using a Scalar Subquery, without using a DETERMINISTIC function. The assumption, as of now, was that the combination of a function created with DETERMINISTIC clause and scalar subquery will give a better performance due to caching effect. In the solution that I provided, the function was not created with DETERMINISTIC clause and it worked absolutely fine and the benefit achieved was due to the Caching effect. This seems to be Undocumented.

For the Performance comparison with and without DETERMINISTIC clause, I recreated the functions as under :

create or replace function get_dname(i_deptno in number)
return varchar2 deterministic as
i_name varchar2(20);
begin
select /*+ ORIGINAL */ dname into i_name from dept where deptno=i_deptno;
return i_name;
end;
/
create or replace function get_dname_o(i_deptno in number)
return varchar2 deterministic as
i_name varchar2(20);
begin
select /*+ OPTIMIZED */ dname into i_name from dept where deptno=i_deptno;
return i_name;
end;
/

With this change, I executed the queries (Original Implementation and Modified) and measured the performance benefits. The performance statistics are as under :

With Original Implementation and without DETERMINISTIC FUNCTION
SQL_ID        SQL_TEXT                                           BUFFER_GETS EXECUTIONS VERSION_COUNT
------------- -------------------------------------------------- ----------- ---------- -------------
dpbdkb6u525g3 SELECT /*+ ORIGINAL */ DNAME FROM DEPT WHERE DEPTN         400        200             1
              O=:B1

With Original Implementation and with DETERMINISTIC FUNCTION
SQL_ID        SQL_TEXT                                           BUFFER_GETS EXECUTIONS VERSION_COUNT
------------- -------------------------------------------------- ----------- ---------- -------------
dpbdkb6u525g3 SELECT /*+ ORIGINAL */ DNAME FROM DEPT WHERE DEPTN          58         29             1
              O=:B1

Clearly, the performance of the DETERMINISTIC function outweighs the performance of the Original Implementation without DETERMINISTIC. The concurrency has reduced from 200 executions to 29 executions. With Scalar subquery Implementation, the performance with or without DERTERMINISTIC is same.

AIOUG Full Day Technical Event in Mumbai, India

I will be presenting a session on “Performance Optimization” during a Full Day Technical Event organized by All India Oracle User Group in Mumbai.

The link for the detailed agenda is as under :

AIOUG TechDay, Mumbai.

Hope to see you all on June 18th 2010.

Cache Buffers Chains Latch Contention…..

Few days back, I got a call from one of my customer about a severe latch contention issue that had been forcing them to restart a database. This phenomenon had been occurring for last past 1 month and for every occurrence the database had to be restarted. This being a critical 24×7 system, for obvious reason, restart was not a solution and when the inhouse dba team gave up on this, I was called upon to investigate the issue.

The database version is 9205 and seeing the statspack during the bad times, it was evident that the contention was on Cache Buffers Chains Latch. In most of the cases, CBC Latch contention come due to inefficient application queries with these consuming huge logical reads per execution. In this case too, as evident from the Statspack, these huge logical reads queries were the primary reason. There were many queries with Millions of Logical Reads and optimizing these would surely give relief as the amount of latching while acquiring block in the cache will come down. While some of the information was available in the Statspack report, I queried x$bh view to check for the block distribution across each CBC Child Latch. For simplicity of the readers, X$BH is an Oracle Internal table that maintains the history of the buffers in the Cache alongwith the hash chains these are allocated to. Usually, the distribution of the blocks or buffers in the cache should be approximately uniform across each hash chain. The Query executed to get the distribution alongwith the output is as under :

This Output is when the system was running perfectly fine without any latch contention.

select * from (
select hladdr, count(*) from x$bh group by hladdr order by 2 desc)
where rownum<=20;

HLADDR             COUNT(*)
---------------- ----------
07000003E2771BA8        197
07000003E661DE70        161
07000003E6305250        157
07000003E661CE90        157
07000003E3F2B0E8        154
07000003E199FA00        153
07000003E4E45FF0        153
07000003E6607150        153
07000003E76A42D0        152
07000003E19A19C0        151
07000003E3DB9F68        151
07000003E5A0F920        151
07000003E78C9D90        151
07000003E76A2310        151
07000003E64E45F0        151
07000003E1A07CE0        150
07000003E4CD7E10        150
07000003E4E45010        150
07000003E4DBA190        150
07000003E3F30048        150

20 rows selected.

As seen from the Good Time output, the buffers protected by each hash chains are nearly uniform. Fortunately, when I was monitoring the system for 2-3 days, I could capture these buffer distribution across each hash chains during Latch Contention and the figures for the top most hash chain looked alarming. The Output captured during the issue is as under :

HLADDR             COUNT(*)
---------------- ----------
07000002570F7300       8734
070000025A786078        364
070000025A5020D8        167
0700000262E9A9E0        167
07000002562DD4D0        164
07000002583D9C00        164
07000002563162C0        161
07000002563BE270        161
07000002571D9AF0        161
0700000262E9C420        161
0700000256341080        161
07000002572C8650        160
07000002583D74A0        160
070000025A838098        160
0700000259557A20        160
07000002562DA010        159
07000002571E2B50        159
07000002582778A0        159
070000025955D600        154
07000002584C1140        151

20 rows selected.

This clearly indicates Hot Block Contention, which means, a block that several processes are trying to access simultaneously. A Long Hash Chain means that a query or set of queries are creating a consistent read image of a particular block and since, a block, even if it has multiple images, will be hashed to a same child latch, a hash chain can grow longer causing latch contention. Next step was to Capture Data relevant to the current issue and therefore the data collected for further investigation was :

  1. Top Blocks / Objects with high Touch Count
  2. Top Running Queries with high Buffer Gets

The First step was to identify the block, and the object it belongs too, with highest Touch Count. This counter is maintained in TCH column of X$BH. The Query used to get this detail is as under :

select a.hladdr, a.file#, a.dbablk, a.tch, a.obj, b.object_name
from   	x$bh a, 
	dba_objects b
where  	(a.obj = b.object_id  or  a.obj = b.data_object_id)
and 	hladdr='07000002570F7300' 
order by 4;

From the output (not mentioned in this blog), I could see two objects S_BRANCH and BA_TMP_HOFF as the top most object. While BA_TMP_HOFF (normal heap table) was contributing to the latch contention with highest touch count, the sessions were waiting on S_BRANCH table due to high concurrency on this table.

The second step was to capture the top running queries with high buffer gets per execution and some of these are mentioned below (I have mentioned only few of them and have cut short the text) :

HASH_VALUE SQL_TEXT                                           EXECUTIONS GETS_PER_EXEC
---------- -------------------------------------------------- ---------- -------------
 603848796 SELECT    FN_NEWBRANCH(A.BRANCH) BRANCH,    A.PROD          1       5294194
           UCT_CODE PRODUCT,    B.PRODUCT_DESCRIPTION PROD_DE
           SCRIPTION,    A.CONTRACT_REF_NO CONTRACT_REF_NO,
             A.USER_REF_NO USER_REF_NO,    C.COLLECTION_REF,
              C.BILL_CCY BILL_CCY,    C.BILL_AMT BILL_AMOUNT,
 
 784509366 SELECT    FN_NEWBRANCH(C.BRANCH) BRANCH,    C.PROD          8       2306090
           UCT_CODE PROD_CODE,    C.CONTRACT_REF_NO,    C.USE
           R_REF_NO,    B.PARTY_ID DRAWER_ID,       B.PARTY_N
           AME DRAWER,    B.PARTY_REFNO DRAWER_REF,    A.PART
           Y_ID DRAWEE_ID,    A.PARTY_NAME DRAWEE,    A.PARTY
 
 322980110 SELECT    FN_NEWBRANCH(CSTBS_CONTRACT.BRANCH),              3        811376
           CSTBS_CONTRACT.PRODUCT_CODE,    CSTMS_PRODUCT.PROD
           UCT_DESCRIPTION,    BCTB_CONTRACT_MASTER.CUSTOMER_
           ID,    STTMS_CUSTOMER.CUSTOMER_NAME1,    CSTBS_CON
           TRACT.CONTRACT_REF_NO,    CSTBS_CONTRACT.USER_REF_

POST-INVESTIGATION DISCUSSION WITH THE APPLICATION OWNER

Further, once the Contending Object (S_BRANCH) and Contributor (BA_TMP_HOFF) was found, during the discussion with the Owner of this Production System it was revealed that the load (number of records processed) by a batch processes that is run in 10 parallel streams has gone up by 3 times and all these batch processes heavily access (select and update) BA_TMP_HOFF table.

During the latch contention, almost 95% of the sessions waiting on CBC Latches query S_BRANCH. The application owner further mentioned that, while this table is static, it is heavily queried as it is a BRANCH Master table and is joined to several other tables to get the Name of the Branch. This input was interesting and is a heart of the this blog. Interestingly, the queries captured for the waiting sessions during the latch free waits were of exactly same type i.e. get the branch name from S_BRANCH and were doing 4 Logical I/O’s per executions using a Table Scan via Primary Key Index. Since these were doing only 4 logical I/O’s per execution, these queries were not on top of the list capture above. The top queries captured were based on the Gets Per Executions.

ANALYSIS AND RESOLUTION

It was time implement some of the changes that would give the customer relief from the ongoing issues. WHile optimization of the Batch Processes involving BA_TMP_HOFF table would have required Understanding the Business Logic, Making Changes, Testing and Implementing and would have taken longer, the emphasis was more to implement a change that does not require extensive testing and therefore may take less time. Therefore, the change that would have given some relief was optimizing Queries on S_BRANCH table. The Concern raised by the Application Owner here was “Will Optimization of a Query with 4 Logical I/O’s per Execution give relief from Latch Contention ?”

To resolve the Latch Contention, my focus was not on reducing the I/O’s of the Queries on S_BRANCH from 4 to lesser but was to reduce the concurrency on this object by way of optimization. If you see the top queries captured above (step 2 of the data collected during the latch contention), these queries call a Function FN_NEWBRANCH. The input to this function is Branch Code from child tables and the output is the branch name fetched from S_BRANCH. A Function, and the queries written in the Function, called from the SELECT Statement is executed once for each row fetched by the main body of the query. This means, if the SELECT Query fetches 1000 rows, then the function called in the SELECT Statement, as well as, the queries written in the function will be executed 1000 times. We all know that Latch Contention is a Concurrency Issue and reducing Concurrency by way of Optimization, results in Scalability of the Application. Therefore, my Goal here was to reduce the concurrency on S_BRANCH table and this was done by way of a Simple Change in the query. Since the change was very simple change, it did not require any Application Testing and was therefore immediately Implemented. Since then, it has been almost 20 days, the performance of the production system is stable with No Latch Contention.

OPTIMIZATION

Let us walk through an example that simulates the production concurrency issue. In this test case, I will create two tables EMP and DEPT. DEPT is a Parent table and EMP is a Child Table that references DEPTNO of DEPT. Further, we will create 2 Functions, both doing the same task, to Get the Department Name for a Deptno and these will be called from a SELECT Statement. Since Deptno is a primary key, a query against DEPT to get DNAME will be of 4 Consistent reads. The text of both the Function is same except the Hint in the Queries to easily identify and differentiate from v$sqlarea.

drop table emp;

drop table dept;

exec dbms_random.seed(0);

create table dept as
select 	rownum deptno, dbms_random.string('A',20) dname
from	all_objects
where 	rownum<=100;

alter table dept add constraint pk_dept primary key(deptno);

create table emp as
select	rownum empno, dbms_random.string('A',30) ename,
	round(dbms_random.value(1,100),0) deptno,
	round(dbms_random.value(1000,10000),0) sal
from	all_objects
where 	rownum<=10000;

alter table emp add constraint pk_emp primary key(empno);

alter table emp add constraint deptno_fk foreign key(deptno) references dept;

exec dbms_stats.gather_table_stats(user,'EMP');

exec dbms_stats.gather_table_stats(user,'DEPT');
create or replace function get_dname(i_deptno in number)
return varchar2 as
i_name varchar2(20);
begin
select /*+ ORIGINAL */ dname into i_name from dept where deptno=i_deptno;
return i_name;
end;
/

create or replace function get_dname_o(i_deptno in number)
return varchar2 as
i_name varchar2(20);
begin
select /*+ OPTIMIZED */ dname into i_name from dept where deptno=i_deptno;
return i_name;
end;
/

set autot trace
select get_dname(deptno) deptno,
        ename, sal
from       emp
where deptno in (10,20);

200 rows selected.


Statistics
----------------------------------------------------------
        505  consistent gets <--- I/O's of the Main Body
	...
	...
        200  rows processed <-- Rows Processed 

Optimized Code (calling of Function using Scalar Subquery)

select (select get_dname_o(deptno) from dual) deptno,
	ename, sal
from	emp
where 	deptno in (10,20);

200 rows selected.

Statistics
----------------------------------------------------------
         21  recursive calls
          0  db block gets
        108  consistent gets <-- I/O's of the Main Body
        ...
        ...
        200  rows processed <-- Rows Processed (same as Original)

column sql_text for a50 wrap
select sql_id, sql_text, buffer_gets, executions, version_count
from v$sqlarea where sql_text like 'select get_dname(dept%';

SQL_ID        SQL_TEXT                                           BUFFER_GETS EXECUTIONS VERSION_COUNT
------------- -------------------------------------------------- ----------- ---------- -------------
90wzvddty11u6 select get_dname(deptno) deptno,  ename, sal from          505          1             1
              emp where deptno in (10,20)


select sql_id, sql_text, buffer_gets, executions, version_count
from v$sqlarea where sql_text like 'SELECT /*+ ORIGINAL%';

SQL_ID        SQL_TEXT                                           BUFFER_GETS EXECUTIONS VERSION_COUNT
------------- -------------------------------------------------- ----------- ---------- -------------
dpbdkb6u525g3 SELECT /*+ ORIGINAL */ DNAME FROM DEPT WHERE DEPTN         400        200             1
              O=:B1


select sql_id, sql_text, buffer_gets, executions, version_count
from v$sqlarea where sql_text like 'select (select get_dname%';

SQL_ID        SQL_TEXT                                           BUFFER_GETS EXECUTIONS VERSION_COUNT
------------- -------------------------------------------------- ----------- ---------- -------------
gufauq4hb8tgq select (select get_dname_o(deptno) from dual) dept         108          1             1
              no,         ename, sal from    emp where deptno in
               (10,20)


select sql_id, sql_text, buffer_gets, executions, version_count
from v$sqlarea where sql_text like 'SELECT /*+ OPTIMIZE%';

SQL_ID        SQL_TEXT                                           BUFFER_GETS EXECUTIONS VERSION_COUNT
------------- -------------------------------------------------- ----------- ---------- -------------
14h9s8atq8u26 SELECT /*+ OPTIMIZED */ DNAME FROM DEPT WHERE DEPT           4          2             1
              NO=:B1

The modification in this case was calling a Function using a SCALAR Subquery using Dual. This change brought down the number of executions of the recursive query “SELECT DNAME FROM DEPT WHERE DEPTNO=:B1” from 200 to 2. This is for each deptno. The Query was executed for Deptno in (10,20) and therefore, the function was called only twice and not for each row fetched by the main body of the query. The Original Implementation called the function 200 times as the main body of the query fetched 200 rows. As for the Logical I/O’s, with both the Implementation The I/O for the recursive query on DEPT was 2 per Execution, but the benefits were on Number of Executions with each of these Implementation. The I/O’s of the Main Query on EMP reduced drastically as the number of times a Query on DEPT reduced drastically and this benefit was passed on to the Main Query.

With this Optimization, the benefits observed were :

  1. Reduction in Logical I/O’s for top queries captured above – This means reduction in Latch Gets.
  2. Reduction in Concurrency against S_BRANCH – This means reduction in Latch Gets.
  3. Improved Scalability

Consistent Gets Myth…..

Based on the request from some of my regular readers, this is a reposting of my previous blog on viveklsharma.blogspot.com (at present unaccessible).

Any Query executed against a database gets the required rows, which are then fetched to the end user. Everyone is aware with the fact that a table row(s) is/are stored in Oracle Database block(s), hence, when oracle has to get even a single row, a block containing that row has to be read into the buffer cache. From this, it is quite evident that Oracle reads a block at a minimum. Also, as an Optimization Feature, for every query, blocks are fetched to a user from the buffer cache (Parallel Queries are exception). Hence, if a block is not found in the buffer cache, these are reads from the disk into the cache and then given to the user.

In Oracle terminology, a read from disk is termed as a Physical Read or a Disk Reads and, a read from Cache is termed as a Consistent Get or a Logical I/O. As mentioned in my previous paragraph, if a block is not found in the Cache, it is first read from the disk into the cache and fetched to the user. This also means that every logical read includes the count of disk reads.

In this blog, I am going to write on a myth about the calculation assumed and derived by the DBA’s from the value of Consistent Gets.

At one customer site, we were discussing about Application tuning wherein a dba gave a list of Top Queries to the Vendor. The list contained the queries alongwith the statistics called Total I/O in GB and the values in this column was in the range of hundreds of GB to tens of TB. I was surprised to see these values and after the discussion, out of the curiosity, asked the dba about the values calculated in this column. The dba immediately gave me the access to his query that generated the output. The calculation in this column was (buffer_gets*db_block_size)/(1024*1024*1024). It seems that the dba’s assumed that each logical I/O to be a read to a new block and hence, these high values. Hence, if a query does a logical I/O of 1549087, then based on the calculation, the dba’s assumed that a query has read 11 gb (assuming 8k block size) worth of data into the cache. This is not true.

BLOCKS READ ALGORITHM

A Concepual Knowledge on Oracle Blocks Read Algorithm is as under :

  1. User Issues a Query.
  2. Query is Parsed and Optimal Execution path is generated and stored in the Library Cache.
  3. Based on the Execution Path, required Index or Table block is searched in the the Cache.
  4. If the block is not found in the cache, a disk read request is made, and the block is read into the Cache.
  5. The block, from the Cache is then read into a private memory area (UGA) of the User.
  6. Once the Block is read into the private memory of the User, the required row is fetched.

The value of Consistent Read is incremented each time a new block is read into a private memory area of the User, which also means that, a single block, if read multiple times, affect the value of the Consistent Read. While an in-efficient SQL contributes largely to Consistent Read, but one factor that also have significant impact is ARRAY SIZE. I am not going to discuss about In-efficient Queries, but will write on the calculation that undergoes and impact Consistent Gets.

A better way to understand the calculation is by way of an Example and as always, I will run through some of the simple queries and see the impact of the change in ARRAYSIZE to check its impact on Consistent Gets.

I will create a table with 3000 rows and will run a simple query in SQLPLUS prompt. Without an Index on this table, it will be a full table scan.

SQL> create table test_cg  as select * from all_objects where rownum between 1 and 3000;

Table created.

Elapsed: 00:00:01.82

SQL> exec dbms_stats.gather_table_stats(user,'TEST_CG');

PL/SQL procedure successfully completed.

Elapsed: 00:00:00.46

SQL> @table_stats TEST_CG
old   2: where table_name='&1'
new   2: where table_name='TEST_CG'

OWNER                          PAR   NUM_ROWS     BLOCKS LAST_ANAL GLO
------------------------------ --- ---------- ---------- --------- ---
VIVEK                          NO        3000         38 02-MAR-10 YES

1 row selected.

SQL> set autot trace
SQL> select * from test_cg;

3000 rows selected.

Elapsed: 00:00:00.17

Execution Plan
----------------------------------------------------------
Plan hash value: 2626677675

-----------------------------------------------------------------------------
| Id  | Operation         | Name    | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |         |  3000 |   249K|    12   (0)| 00:00:01 |
|   1 |  TABLE ACCESS FULL| TEST_CG |  3000 |   249K|    12   (0)| 00:00:01 |
-----------------------------------------------------------------------------


Statistics
----------------------------------------------------------
          1  recursive calls
          0  db block gets
        238  consistent gets
          0  physical reads
          0  redo size
     300791  bytes sent via SQL*Net to client
       2608  bytes received via SQL*Net from client
        201  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
       3000  rows processed

A simple query with 38 blocks accounted for 238 consistent gets. Does it mean 238*8192 i.e.1.8 MB of data read into the cache ? The answer is plain NO. As mentioned earlier, the blocks are read only once into the cache and is sent to the PGA of the user that require this block. Internally, the calculation for consistent gets is (NUM_ROWS / ARRAYSIZE)+NUM_BLOCKS. In my case, when I executed this query, the arraysize was unchanged and therefore was default, which is 15.

Rows in my Table	: 3000
Blks in my Table	: 38
Default Arraysize	: 15

(3000/15)+38 = 200 + 38 = 238

The calculation matches the Consistent Gets. One value worth discussing here is the number 200 derived from (3000/15). This value means that each of the blocks will be touched multiple times. There are 38 blocks in the table, each of these 38 blocks will be read only once in the cache, but will be touched more than once and therefore, the consistent gets for a single block access will based on the number of times, it will be visited to read all the required rows. Incidently, another statistics that matches our calculation is the “SQL*Net roundtrips to/from client” from the Autotrace output above. The value, with arrays 15 is 201, which means, 200 visits or touch to the block to fetch 3000 rows, and last visit required to check and confirm whether more rows are to be fetched.

Back to our example, let us get into more details of the calculation of Consistent Gets. The table has 3000 rows and 38 blocks. The count of rows in each of these 38 blocks is shown below.

select dbms_rowid.ROWID_BLOCK_NUMBER(rowid) blkno, count(*) cnt
from test_cg
group by dbms_rowid.ROWID_BLOCK_NUMBER(rowid) order by 1;
SQL> /

     BLKNO        CNT
---------- ----------
       641         88
       642         84
       643         81
       644         76
       645         81
       646         80
       647         82
       648         77
.... some lines deleted
.... some lines deleted
       677         78
       678         82
           ----------
sum              3000

The Sequence of Consistent Gets calculation is as under (with default arraysize) :

  1. Get Block 641 in the PGA, fetch 15 Rows – Consistent Gets = 1
  2. Get Block 641 again in the PGA, fetch another 15 rows – Consistent Gets = 2
  3. Get Block 641 again in the PGA, fetch another 15 rows – Consistent Gets = 3
  4. Get Block 641 again in the PGA, fetch another 15 rows – Consistent Gets = 4
  5. Get Block 641 again in the PGA, fetch another 15 rows – Consistent Gets = 5
  6. Get Block 641 again in the PGA, fetch another 13 rows – Consistent Gets = 6
  7. Get Block 642 in the PGA, fetch 2 rows – Consistent Gets = 7
  8. Get Block 642 again in the PGA, fetch another 15 rows – Consistent Gets = 8
  9. and so on….

It is clear from this explanation that a single block is read is multiple times, in our case at at average 6 times and therefore, consistent gets for each of these blocks was around 6 to 7. Run this test case with different arraysize and the calculation should match that shown in this blog. Let us run the query with different values of Arraysize and validate the calculation.

ArraySize = 35 Consistent Gets will be (3000/35)+38 = 86+38 = 124
ArraySize = 40 Consistent Gets will be (3000/40)+38 = 75+38 = 113
ArraySize = 50 Consistent Gets will be (3000/50)+38 = 60+38 = 98

SQL> set arrays 35
SQL>select * from test_cg;

Statistics
----------------------------------------------------------
          0  recursive calls
          0  db block gets
        124  consistent gets
         87  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
       3000  rows processed

SQL> set arrays 40
SQL> select * from test_cg;

3000 rows selected.

Statistics
----------------------------------------------------------
          0  recursive calls
          0  db block gets
        113  consistent gets
         76  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
       3000  rows processed

SQL> set arrays 50
SQL> select * from test_cg;

3000 rows selected.

Statistics
----------------------------------------------------------
          0  recursive calls
          0  db block gets
         98  consistent gets
         61  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
       3000  rows processed

Below is the query, that can be executed to check for the approximate value of the Consistent Gets and should help you understand the way this value is calculated. The value of bind variable is to be set to the ARRAYSIZE. The Query and the details of the columns used is as under :

variable b1 number;
exec :b1:=15;
SQL> compute sum of total_cnt on report
SQL> break on report

select	blkno, total_cnt, final_cnt, rows_remaining,
	case when rows_remaining=0 then touch_cnt+1 else touch_cnt end touch_cnt
from (
select	blkno, total_cnt, final_cnt, rows_remaining,
	case when total_cnt = final_cnt then ceil(final_cnt/:b1) else ceil(final_cnt/:b1)+1 end touch_cnt
from	(
select	blkno, cnt total_cnt, 
	case when rownum=1 or lag(rows_remaining) over (order by blkno)=0 
                     then cnt else (cnt-(:b1-lag(rows_remaining) over (order by blkno))) end final_cnt,
	rows_remaining 
from (
select blkno, cnt, rr, 
lead(rr) over(order by blkno) next_rr,
lead(blkno) over(order by blkno) next_blk,
ceil(rr/:b1) touch_cnt,
mod(rr,:b1) rows_remaining
from (
select dbms_rowid.ROWID_BLOCK_NUMBER(rowid) blkno, count(*) cnt, 
	sum(count(*)) over(order by dbms_rowid.ROWID_BLOCK_NUMBER(rowid)) rr 
from test_cg
group by dbms_rowid.ROWID_BLOCK_NUMBER(rowid) order by 1))));

     BLKNO  TOTAL_CNT  FINAL_CNT ROWS_REMAINING  TOUCH_CNT
---------- ---------- ---------- -------------- ----------
       641         88         88             13          6
       642         84         82              7          7
       643         81         73             13          6
       644         76         74             14          6
       645         81         80              5          7
       646         80         70             10          6
       647         82         77              2          7
       648         77         64              4          6
       649         73         62              2          6
       650         79         66              6          6
       651         79         70             10          6
       652         79         74             14          6
       653         81         80              5          7
       654         82         72             12          6
       655         77         74             14          6
       656         81         80              5          7
       657         80         70             10          6
       658         81         76              1          7
       659         78         64              4          6
       660         78         67              7          6
       661         76         68              8          6
       662         78         71             11          6
       663         78         74             14          6
       664         77         76              1          7
       665         81         67              7          6
       666         79         71             11          6
       667         79         75              0          7
       668         77         77              2          6
       669         77         64              4          6
       670         76         65              5          6
       671         80         70             10          6
       672         77         72             12          6
       673         76         73             13          6
       674         75         73             13          6
       675         79         77              2          7
       676         78         65              5          6
       677         78         68              8          6
       678         82         75              0          7
           ---------- ---------- -------------- ----------
sum              3000       2744            284        238

BLKNO		: Block Number
TOTAL_CNT	: Total Rows in the Block
FINAL_CNT	: Final Number of Rows 
		(Example Block 642 has total 84 Rows, but final row count is 82 as 13 rows were read from 641 and balance 
		2 Rows were fetched from this block, this gives 84-2=82)
ROWS_REMAINING	: Incomplete Arraysize Rows from the current block
TOUCH_CNT	: Touch Count for current block and is our Consistent Gets per Block. Final total at the end.

Consistent Gets are not a measure of number of blocks that are read into the cache but number of times, a block was read into the PGA. A block is read only once in the cache and touched multiple times. Our example above showed that with arraysize of 15, an 8k block was read once but was touched 6-7 times and hence had a 6 or 7 consistent gets per block. This does not mean 6*8192 worth of data. The data was read only once.

Row Cache Object Latch Contention : Improper Implementation of Oracle Feature – Day 2 of AIOUG

On Day 2 of AIOUG, I discussed on Library Cache related latches and some causes of the contention on these latches. One such contention is for Row Cache Object latches that protects the Data Dictionary loaded in the Shared Pool. Dictionary Cache is one of the component of a Shared Pool and all the definitions, required to parse a Cursor, is stored in this cache, in the form of a ROW, thus is protected by Row Cache Object Latches.

The Issue

Almost a year back, I was involved in a major performance issue caused due to “Latch: row cache objects” contention. This was a 2 Node RAC setup running 10.2.0.3 database. The issue was normally reported during the peak hour with CPU Utilization above 95%. During this slowness window, there was a major drip in Transaction per Second and this resulted in an increase in pending volume to be entered into the system.

As mentioned earlier, as “Row Cache Object” latches protect Dictionary Cache information, contention on these latches needs investigating the sub-component of the Dictionary Cache and this can be done thru AWR reports. “Dictionary Cache Stats” section in AWR gives the Get Requests, PCT Miss and other required information for each of the sub-component of Dictionary Cache. This section revealed that the GET REQUESTS on DC_USERS was very high. Another alarming statistics was huge version_counts for the application queries. Since, the number of get requests on DC_Users seemed to be abnormally high, the focus was to know for the potential causes of these high numbers and the one related to this application or database. Further investigation drilled down the issue to Virtual Private Database (VPD). The customer here had implemented VPD and this implementation caused the contention on these latches.

Does it mean VPD is an Issue ?

VPD is a very useful feature as it provides an easy and manageable means of implementing row level security, but there are certain implementation portions, that needs to be considered based on the requirement, and it is this piece, if improperly implemented, can cause a major performance issue. In a nutshell, it is not VPD but the way it was being implemented that caused a major issue at the customer site. We will cover this piece of problematic implementation in this blog, with a test case, that will help you to test this on your test system and validate the results.

A brief explanation of VPD

VPD, also called as Row Level Security or Fine Grained Access Control, is a feature, introduced in Oracle 8i, that allows you to define security policies on tables by way of a function that implies a restriction on the rows that a user can SELECT or UPDATE or INSERT or DELETE, based on the privileges defined for that user. For example, assuming a following hierarchy :
Employee S is from South India Region, N from North India Region, W from West India Region, E from East India Region, I from All India Region and finally, APAC for Asia Pacific. With this herierchy, a requirement like :

  1. User S can View and Manipulate all the rows from South India Region, and,
  2. User N can View and Manipulate all the rows from North India Region, and,
  3. User W can View and Manipulate all the rows from West India Region, and,
  4. User E can View and Manipulate all the rows from East India Region, and,
  5. User I can View and Manipulate all the rows from 1 to 4 i.e.South, North, West and East India Region, while,
  6. User A can View and Manipulate all the rows from 1 to 5, including other countries in Asia Pacific Region.

These restrictions can be implemented by using VPD without much of an effort. Application does not require any changes for these implementations, as these restrictions are applied in the background at the runtime. There are three basic components of VPD. These are :
Policy : A declarative command that determines when and how to apply the policy i.e. during Selects, Insertions, Deletion, Updation or combination of any or all of these.
Policy Function : A Pl/SQL function that is called whenever the condition specified in the policy are met.
Predicate : A string that is generated by the policy function, and the applied to the users’ sql statements. This is an additional Filter predicate generated by the policy function and appended to the sql statement executed by the user, at the runtime.

Once these three are defined as a policy, then this policy can be attached to the tables that requires Security to be implemented. While Policy, Policy Function and Predicate are three main building blocks of VPD, there is another component, that is mostly ignored, called as Policy Type. In most of the cases, this is not defined, which means, it is left as default. If not defined, it defaults to a value depending on the database version and therefore, should not be left undefined. Starting Oracle 10g, the policy type defaults to DYNAMIC. While there are other policy types : SHARED Static, CONTEXT-SENSITIVE and SHARED Context Sensitive. The implementation of each of these depend upon the requirement.

Back to the Issue

In this case, it was the implementation of VPD that caused huge Version Count and both of these had a major impact on the performance due to heavy latch contention. Again, it was not the VPD that caused multiple version counts, but the implementation. The Application team, in this case, while implementing VPD, left POLICY_TYPE to default and this means DYNAMIC. DYNAMIC policy_type implies, dynamic builtup of the predicates at runtime, based on the policy function. The issue with Dynamic policy_type is that with each row processed by the query, the policy function is applied, predicate is evaluated and validated whether the row is to be fetched to the user. This means, the query goes through parse-execute-fetch phase for each and every row processed by the query and this significantly increases a Parse Overhead.

Based on the customer requirement, a better option would have been either Context-Sensitive or Shared Context-Sensitive, that internally implements Dynamic predicate generation, at runtime, but goes through the Parse phase only when the Context of the User Session changes and not for each and every row processed. Further, in this customer case, just changing the Policy_type from Dynamic to Shared Context-Sensitive did not help, as there was a major design flaw in the policy function. Lets walk thru a test case and at the end, I will present some facts from the customer system. The test case simulates the implementation at the customer site and the change suggested, thereafter to improve the performance. The change, as depicted in the test case, is in the policy function and the change in policy_type is already changed to Shared Context-Sensitive.

TEST CASE

For this test case to work, I have created some set of statistics table that will store the latch activity. Schema VIVEK contains an application table TEST_VPD. I further create USER_X schema and already have SCOTT and OE, as an application user schemas, ITDBA schema, that contains Security Table. This table defines the security policies for each application users. All of the VPD implementations pre-requisites are created in ITDBA schema.

Once the pre-requisites are created, we will execute certain queries, against TEST_VPD, from VIVEK : access to all the rows, OE Schema : access to rows pertaining to rcc_num=10, SCOTT Schema : access to rows pertaining to rcc_num=20 and USER_X Schema : access to rows pertaining to rcc_num=30. All these access restrictions are defined in EMP_SEC table under ITDBA Schema and the predicates are applied, at run time, by way of Policy Function GF_GLOBAL_POLICY_OPT_RSTRCT_VW, again under ITDBA Schema. Once these are done, we will run certain queries against our Statistics table and some data dictionary views to check for the performance. The policy function, GF_GLOBAL_POLICY_OPT_RSTRCT_VW, will then be changed to improve the performance and the queries against TEST_VPD and measure the performance.

connect vivek/vivek

drop table latch_stats_start;
drop table latch_stats_end;
drop table test_vpd;
drop user user_x cascade;
drop user itdba cascade;
drop public synonym as_test_vpd;
drop context rcc_ctx;

exec dbms_random.seed(0);
create global temporary table latch_stats_start as
select name, gets from v$latch;

create global temporary table latch_stats_end as
select name, gets from v$latch;

create table test_vpd as
select	dbms_random.string('U',5)||lpad(rownum,5,'0') pan,
	dbms_random.string('U',20) name,
	case when mod(rownum,10)=0 then 'F' else 'M' end sex,
	round(dbms_random.value(10,100),0) rcc_num
from	all_objects
where	rownum between 1 and 10000;
create unique index pk_pan on test_vpd(pan);
exec dbms_stats.gather_table_stats(user,'test_vpd',cascade=>true,method_opt=>'for all columns size 1');
create public synonym as_test_vpd for test_vpd;
grant select on as_test_vpd to public;

create user user_x identified by vivek;
grant connect,resource to user_x;

connect sys/vivek as sysdba
create user itdba identified by vivek;
grant dba to itdba;
grant execute on dbms_rls to itdba;

connect itdba/vivek

create context rcc_ctx using ITDBA.set_rcc_ctx;

create procedure set_rcc_ctx(p_attr in varchar2, p_val1 in varchar2, p_val2 in varchar2, p_val3 in varchar2)
is
begin
   dbms_session.set_context('RCC_CTX',p_attr,p_val1, p_val2, p_val3);
end;
/

create table itdba.emp_sec (user_id varchar2(20), rcc_num number);

insert into emp_sec values('SCOTT',20);
insert into emp_sec values('OE',10);
insert into emp_sec values('USER_X',30);

CREATE OR REPLACE function ITDBA.GF_GLOBAL_POLICY_OPT_RSTRCT_VW(OBJECT_SCHEMA VARCHAR2, OBJECT_NAME VARCHAR2) return varchar2 is
    L_SEL_FLG VARCHAR2(3);
    L_RCC_NUM NUMBER;
    L_USER_ID  VARCHAR2(20);
    l_predicate varchar2(30);
    v_Restricted_Policy varchar2(1);
begin
    IF  L_USER_ID IS NULL AND USER IN ('ITDBA','SYS','SYSTEM','VIVEK') THEN
       RETURN NULL;
    ELSE
       IF L_USER_ID IS NULL THEN
 	  L_USER_ID:=USER;
       END IF;
     if l_user_id in ('USER_X','SCOTT','OE') then v_Restricted_Policy:='Y'; end if;
     IF v_Restricted_Policy = 'Y' THEN
       select rcc_num into l_rcc_num from itdba.emp_sec
       where user_id = l_user_id;
       set_rcc_ctx('RCC_NUM',l_rcc_num, null, null);
	l_predicate:='RCC_NUM in '||sys_context('RCC_CTX','RCC_NUM');
	 return l_predicate;
     ELSE
       select rcc_num into l_rcc_num from itdba.emp_sec
       where user_id = l_user_id;
       set_rcc_ctx('RCC_NUM',l_rcc_num,l_rcc_num+100,99);
	l_predicate:='RCC_NUM in '||sys_context('RCC_CTX','RCC_NUM');
	 return l_predicate;
     END IF;
    END IF;
Exception
  When others then
       Raise_Application_Error (-20000,SQLERRM);
END;
/
begin
dbms_rls.add_policy(object_schema=>'VIVEK',
   object_name=>'test_vpd',
   policy_name=>'RCC_NUM_POLICY',
   function_schema=>'ITDBA',
   policy_function=>'GF_GLOBAL_POLICY_OPT_RSTRCT_VW',
   statement_types=>'INSERT,SELECT,UPDATE,DELETE',
   update_check=>true,
   policy_type=>dbms_rls.SHARED_CONTEXT_SENSITIVE);
end;
/

Now, since the pre-requisite is done, we will execute a query against TEST_VPD from VIVEK, OE, SCOTT and USER_X Schemas to check the rows returned by each of these users. This will help us validate and confirm whether the policy function is applied as per our requirement.

connect vivek/vivek
column pan for a20
column name for a30

select /*+ VIVEK */ * from as_test_vpd where rcc_num=10 and rownum<=3;

select /*+ VIVEK */ * from as_test_vpd  where rcc_num=20 and rownum<=3;

select /*+ VIVEK */ * from as_test_vpd  where rcc_num=30 and rownum<=3;

## User OE only allowed to see 10
----------------------------------

connect oe/vivek

select /*+ VIVEK */ * from as_test_vpd where rcc_num=10 and rownum<=3;
select /*+ VIVEK */ * from as_test_vpd where rcc_num=20 and rownum<=3;
select /*+ VIVEK */ * from as_test_vpd where rcc_num=30 and rownum<=3;

## User SCOTT only allowed to see 20
-------------------------------------
connect scott/tiger

select /*+ VIVEK */ * from as_test_vpd where rcc_num=10 and rownum<=3;
select /*+ VIVEK */ * from as_test_vpd  where rcc_num=20 and rownum<=3;
select /*+ VIVEK */ * from as_test_vpd where rcc_num=30 and rownum<=3;

## User USER_X only allowed to see 30
---------------------------------------

connect user_x/vivek

select /*+ VIVEK */ * from as_test_vpd where rcc_num=10 and rownum<=3;
select /*+ VIVEK */ * from as_test_vpd  where rcc_num=20 and rownum<=3;
select /*+ VIVEK */ * from as_test_vpd  where rcc_num=30 and rownum<=3;

Now for the performance. This is the Original Implementation.

connect vivek/vivek
alter system flush shared_pool;
connect scott/tiger
insert into vivek.latch_stats_start select name, gets from v$latch;
select /*+ VIVEK */ * from as_test_vpd where rownum<=3;
insert into vivek.latch_stats_end select name, gets from v$latch;
connect oe/vivek
select /*+ VIVEK */ * from as_test_vpd where rownum<=3;
connect user_x/vivek
select /*+ VIVEK */ * from as_test_vpd where rownum<=3; 
SQL>set lines 132
SQL> set pages 1000
SQL> column sql_text for a40 wrap
SQL>
SQL> select sql_id, sql_text, executions, child_latch, version_count from v$sqlarea where sql_text like 'select /*+ VIVEK%';

SQL_ID        SQL_TEXT                                 EXECUTIONS CHILD_LATCH VERSION_COUNT
------------- ---------------------------------------- ---------- ----------- -------------
1n62sv9g6nw77 select /*+ VIVEK */ * from as_test_vpd w          3           2             3
              here rownum<=3 
SQL>select sql_id, sql_text, executions, child_latch, child_address from v$sql where sql_text like 'select /*+ VIVEK%';

SQL_ID        SQL_TEXT                                 EXECUTIONS CHILD_LATCH CHILD_AD
------------- ---------------------------------------- ---------- ----------- --------
1n62sv9g6nw77 select /*+ VIVEK */ * from as_test_vpd w          1           2 69EAE56C
              here rownum<=3

1n62sv9g6nw77 select /*+ VIVEK */ * from as_test_vpd w          1           2 69065194
              here rownum<=3

1n62sv9g6nw77 select /*+ VIVEK */ * from as_test_vpd w          1           2 68FEDAE8
              here rownum<=3 
SQL>column predicate for a30
SQL>select sql_id, ADDRESS, PREDICATE from v$vpd_policy where sql_id='1n62sv9g6nw77';

SQL_ID        ADDRESS  PREDICATE
------------- -------- ------------------------------
1n62sv9g6nw77 69EAE56C RCC_NUM in 20
1n62sv9g6nw77 69065194 RCC_NUM in 10
1n62sv9g6nw77 68FEDAE8 RCC_NUM in 30

When the Query was executed from different application users, three in our case, the version_count of the query increased as well. The dynamic performance view V$VPD_POLICY shows the runtime plan, which is Literal in this case and causes a query to be hard parsed. Below mentioned query can be executed to check for the latch statistics.

column name for a30
set lines 200
set pages 1000
select * from (select a.name, b.gets end_gets, a.gets Start_gets, b.gets-a.gets
from	vivek.latch_stats_start a,
	vivek.latch_stats_end b
where	b.name = a.name
order by 4 desc)
where rownum<=10;

MODIFICATION

The code below shows the modification to the function that is applied to the policy. Look for the l_predicate value at the end that is finally returned as an Output to the function.

connect vivek/vivek
CREATE OR REPLACE function ITDBA.GF_GLOBAL_POLICY_OPT_RSTRCT_VW(OBJECT_SCHEMA VARCHAR2, OBJECT_NAME VARCHAR2) return varchar2 is
    L_SEL_FLG VARCHAR2(3);
    L_RCC_NUM NUMBER;
    L_USER_ID  VARCHAR2(20);
    l_predicate varchar2(60);
    v_Restricted_Policy varchar2(1);
begin
    IF  L_USER_ID IS NULL AND USER IN ('ITDBA','SYS','SYSTEM','VIVEK') THEN
       RETURN NULL;
    ELSE
       IF L_USER_ID IS NULL THEN
 	  L_USER_ID:=USER;
       END IF;
     if l_user_id in ('USER_X','SCOTT','OE') then v_Restricted_Policy:='Y'; end if;
     IF v_Restricted_Policy = 'Y' THEN
       select rcc_num into l_rcc_num from itdba.emp_sec
       where user_id = l_user_id;
       set_rcc_ctx('RCC_NUM',l_rcc_num, null, null);
	l_predicate := 'RCC_NUM = sys_context (''RCC_CTX'', ''RCC_NUM'')';
	 return l_predicate;
     ELSE
       select rcc_num into l_rcc_num from itdba.emp_sec
       where user_id = l_user_id;
       set_rcc_ctx('RCC_NUM',l_rcc_num,l_rcc_num+100,99);
       	l_predicate := 'RCC_NUM = sys_context (''RCC_CTX'', ''RCC_NUM'')';
	 return l_predicate;
     END IF;
    END IF;
Exception
  When others then
       Raise_Application_Error (-20000,SQLERRM);
END;
/

Once the change is done, we will execute the query on TEST_VPD from the three application users (USER_X, OE and SCOTT), and compare the latch statistics and version_count.

connect vivek/vivek
alter system flush shared_pool;
connect scott/tiger
insert into vivek.latch_stats_start select name, gets from v$latch;
select /*+ VIVEK */ * from as_test_vpd where rownum<=3;
insert into vivek.latch_stats_end select name, gets from v$latch;
connect oe/vivek
select /*+ VIVEK */ * from as_test_vpd where rownum<=3;
connect user_x/vivek
select /*+ VIVEK */ * from as_test_vpd where rownum<=3;
connect vivek/vivek
Connected.
SQL> set lines 132
SQL> set pages 1000
SQL> column sql_text for a40 wrap
SQL>
SQL> select sql_id, sql_text, executions, child_latch, version_count from v$sqlarea where sql_text like 'select /*+ VIVEK%';

SQL_ID        SQL_TEXT                                 EXECUTIONS CHILD_LATCH VERSION_COUNT
------------- ---------------------------------------- ---------- ----------- -------------
1n62sv9g6nw77 select /*+ VIVEK */ * from as_test_vpd w          3           2             1
              here rownum<=3 
SQL>select sql_id, sql_text, executions, child_latch, child_address from v$sql where sql_text like 'select /*+ VIVEK%';

SQL_ID        SQL_TEXT                                 EXECUTIONS CHILD_LATCH CHILD_AD
------------- ---------------------------------------- ---------- ----------- --------
1n62sv9g6nw77 select /*+ VIVEK */ * from as_test_vpd w          3           2 68BEE27C
              here rownum <=3 
SQL>column predicate for a30
SQL>select sql_id, ADDRESS, PREDICATE from v$vpd_policy where sql_id='1n62sv9g6nw77';

SQL_ID        ADDRESS  PREDICATE
------------- -------- ------------------------------
1n62sv9g6nw77 68BEE27C RCC_NUM = sys_context ('RCC_CT
                       X', 'RCC_NUM')

Improved performance can be clearly seen from the version_count, output of V$VPD_POLICY view and from the latch statistics. Once this was implemented, the customer reported a significant performance improvement, for which the real facts are shown below.



Optimizing Performance Part 2 Matrix


Optimizing Performance Part 2 Top 5 waits

CUSTOMER MATRIX – BEFORE

 

TOP 5 WAITS

 



Optimizing Performance Part 2 Achievement

CUSTOMER MATRIX – AFTER

 

To summarize, Oracle provided features enable us to implement these with great ease. Implementing these require careful study of the requirement and proper implementation, that does not compromise with the performance.

Library Cache : Causes of Multiple Version Count for an SQL (Day 1 of AIOUG Event)

I am just back from the 3 day technical event at Bangalore, India. This event gave me an oppurtunity to meet and speak to a legend, and one of my Guru, Thomas Kyte. His presentations were remarkable. This event also gave me an oppurtunity to meet many of my readers. My purpose of initiating blogs (I started in Dec 2006) was to educate Oracle Community and It is very satisfying to know that, whatever you write, reaches the Community. Many readers were disappointed since my previous blog url is still inaccessible, and I had to assure them, that all those posts, would be re-posted on this blog. This means, in India, I too have a good fan following. 🙂

I had three sessions span across each of the three days, and Day 1 of the event, I discussed mostly on Library Cache and some of the issues that have an impact on performance, causing Library Cache related Latch Contention. As usual, to prove the point, I demonstrated each of these with some scripts executed on my 10.2.0.1 database. Day 2, also began with a brief discussion on this issue and I demonstrated some more facts, including an issue due to “Improper Implementation of Virtual Private Database”. This post is a detailed discussion on what I presented and demonstrated at the Event on Day 1, as it gives a brief understanding of Parent & Child Cursors. Day 2, I started with the same topic and my next few blogs will cover the test cases and explanations that I present on the second day.

In short, this blog should give you a conceptual knowledge on Library Cache, Parent Cursors, Child Cursors and will also explain about Bind Graduation. Once the concepts of these are cleared, the next blog will feature some more issues that cause creation of multiple child cursors and relate to a Real Life Experience.

We all know that the purpose of Library Cache is to maximize sharing of resources required to execute SQL statements from multiple sessions. In Oracle, these SQL statements are called as CURSORS and therefore, cursors should not be misinterpreted as implicit and explicit cursors defined in pl/sql blocks.This Shareability reduces the memory requirements and the parse time because parsed representations of an SQL may already be stored in the Library Cache. Two SQL statements will not be shared unless their SQL text is completely identical. Any difference in spacing or casing will make two SQL statements be considered different and none of their data structures can be shared. Assume following queries :

  1. select * from emp where ename=:b1;
  2. SELECT * FROM EMP WHERE ENAME=:b1;
  3. select * from emp e where e.ename=:b1;
  4. select * from emp where ename=:b1;

Although the four queries are using Bind Variables, only two of them are sharable, while the other two (2 & 3) will not be shared. The shared representation of a SQL cursor is stored in two parts: the Parent Cursor and the Child Cursor.

PARENT CURSOR

It stores the name i.e. the SQL text of the Cursor. When two statements are identical textually, they will share a same Parent Cursor. As an example, query no.1 and 4 above, will share a same Parent Cursor.

CHILD CURSOR

The Child Cursor holds other required information, like: the identity of the objects referenced by the SQL Cursor; the names, type and length of the bind variables used; the session value of the session parameters that have an impact on the optimizer decisions and the execution plan of the Cursor.

What this means is, two textually similar statements, sharing same parent cursor, will also share a same child cursor if they share each of the above mentioned information. This also infers that a parent cursor, will have atleast (minimum) one or more than one child cursor. The ratio is 1:1 (one parent : one child) or 1:M (one parent : multiple child).

The demonstration in this blog are all the same that I presented during the event and are run on 10.2.0.1 database. The output of some of the queries may differ in 10.2.0.2 and above, but the issue remains same.

Number of Library Cache CHILD Latches
SQL> show parameter cpu


SQL> show parameter cpu

NAME                                 TYPE                             VALUE
------------------------------------ -------------------------------- ------------------------------
cpu_count                            integer                          24

select count(*) from v$latch_children where name='library cache';

  COUNT(*)
----------
        29

I executed this query on a production system with 24 CPU, and the number of Child Latches, automatically pre-decided and allocated at run time by Oracle kernel, was set to next smallest prime number to the number of CPU’s and can go upto 67 max. Therefore, even if I have a 80 CPU machine, the number of Child Library Cache latches would be limited to 67. Library Cache is a parent Latch with multiple child latches. In this case, we have One Parent Library Cache Latch and 29 Child Latches.

DEMONSTRATION OF 1:1 ratio

I will execute 4 queries out of them, 3 will be on EMP table and 1 on DEPT. While a query on DEPT will fetch different resultset, the other three will fetch the same resultset, but, will share different parent cursors. Therefore, even though, the execution plans of these three are same (FTS on EMP), these will be mapped to different parent cursor as these do not match textually. Also note that these 4 hashed (internal algorithm) to different child latches.

  1. select /*+ VIVEK */ * from emp;
  2. select /*+ VIVEK */ * from dept;
  3. SELECT /*+ VIVEK */ * FROM EMP;
  4. select /*+ VIVEK */ * from emp e;

Dynamic view V$SQLAREA maintains single row per parent cursor, whereas, V$SQL maintains each row for a Child Cursor. One the four queries, mentioned above, were executed, the output form V$SQLAREA and V$SQL was as under :

SQL> column sql_text for a40 wrap
SQL> set lines 132
SQL> select sql_id, sql_text, executions, child_latch from v$sqlarea where upper(sql_text) like 'SELECT /*+ VIVEK%';

SQL_ID        SQL_TEXT                                 EXECUTIONS CHILD_LATCH
------------- ---------------------------------------- ---------- -----------
bsmb6z005wgyr select /*+ VIVEK */ * from emp e                  1           1
c9qspg124hjk4 SELECT /*+ VIVEK */ * FROM EMP                    1           3
00fdja7qcpswr select /*+ VIVEK */ * from dept                   1           3
2p65wk92f3vpq select /*+ VIVEK */ * from emp                    1           2

SQL> select sql_id, sql_text, executions, child_latch from v$sql where upper(sql_text) like 'SELECT /*+ VIVEK%';

SQL_ID        SQL_TEXT                                 EXECUTIONS CHILD_LATCH
------------- ---------------------------------------- ---------- -----------
bsmb6z005wgyr select /*+ VIVEK */ * from emp e                  1           1
c9qspg124hjk4 SELECT /*+ VIVEK */ * FROM EMP                    1           3
00fdja7qcpswr select /*+ VIVEK */ * from dept                   1           3
2p65wk92f3vpq select /*+ VIVEK */ * from emp                    1           2

This demonstration also shows that after we executed these queries, a child cursor automatically, these can be mapped from SQL_ID. For each SQL_ID in V$SQLAREA, we have a corresponding row in V$SQL (1:1).

DEMONSTRATION OF 1:M ratio

While there are multiple reasons for the scenario where 1 Parent Cursor can have Multiple Child Cursors, I demostrated few of them and these were :

  1. Optimizer & Bind Mismatch – DAY 1
  2. Bind Graduation (Different Bind Length) – DAY 1
  3. Cursor_Sharing=SIMILAR (further divided into two scenarios) – DAY 2
  4. Improper Implemention of Oracle features VPD – DAY 2

While this blog covers DAY 1, the third scenario will feature in my next blog, and the fourth on VPD will follows after that. Day 2 Blogs relates to some of my Real Life Challenges and I will relate these to those experiences.

OPTIMIZER & BIND MISMATCH

We all know and understand the importance of bind variables. Bind Variables are crucial in a Transaction processing (OLTP) databases, and one of the primary responsilibility of an Application Developer is to make sure that application queries make use of Bind Variables.

This demonstration uses Bind Variables, therefore, during the presentation, one of the Attendee asked me a question (during Q&A session) that since we have too many child cursors with bind Implementation, does it mean that we need to change these to Literals ? Please note that, Literals are very bad for Transaction Processing System and this test case demonstrates an issue, not due to Bind Variables, but the way these are either implemented or some other settings that make a sharable parent cursor partially sharable.

In this test case, I will execute textually similar query, with different Bind Length and Optimizer Settings. Since the queries are textually same, they will be mapped to a single SQL_ID and to a same child latch, but will have multiple child cursors (version_count).

FLUSHING OF SHARED_POOL IS NOT RECOMMENDED ON PRODUCTION. THIS IS A TEST INSTANCE, I AM FLUSHING TO DEMONSTRATE THE EXECUTIONS AND VERSION_COUNT THAT MATCH THE TEST CASE.

SQL> alter system flush shared_pool;

System altered.

SQL> connect x/x
Connected.
SQL> variable b1 varchar2(10);
SQL> exec :b1:='KING';

PL/SQL procedure successfully completed.

SQL> select /*+ VIVEK */ * from emp where ename=:b1;

     EMPNO ENAME      JOB              MGR HIREDATE         SAL       COMM     DEPTNO
---------- ---------- --------- ---------- --------- ---------- ---------- ----------
      7839 KING       PRESIDENT            17-NOV-81       5000                    10

SQL> REM Same Query with Different Optimizer Settings
SQL> alter session set optimizer_index_cost_adj=10;

Session altered.

SQL> select /*+ VIVEK */ * from emp where ename=:b1;

     EMPNO ENAME      JOB              MGR HIREDATE         SAL       COMM     DEPTNO
---------- ---------- --------- ---------- --------- ---------- ---------- ----------
      7839 KING       PRESIDENT            17-NOV-81       5000                    10

SQL> connect x/x
Connected.
SQL> REM Optimizer Setting Default
SQL> variable b1 varchar2(40);
SQL> exec :b1:='KING';

PL/SQL procedure successfully completed.

SQL> select /*+ VIVEK */ * from emp where ename=:b1;

     EMPNO ENAME      JOB              MGR HIREDATE         SAL       COMM     DEPTNO
---------- ---------- --------- ---------- --------- ---------- ---------- ----------
      7839 KING       PRESIDENT            17-NOV-81       5000                    10

SQL> connect y/y
Connected.
SQL> select * from tab;

TNAME                          TABTYPE  CLUSTERID
------------------------------ ------- ----------
EMP                            TABLE

SQL> variable b1 varchar2(10);
SQL> exec :b1:='KING';

PL/SQL procedure successfully completed.

SQL> select /*+ VIVEK */ * from emp where ename=:b1;

     EMPNO ENAME      JOB              MGR HIREDATE         SAL       COMM     DEPTNO
---------- ---------- --------- ---------- --------- ---------- ---------- ----------
      7839 KING       PRESIDENT            17-NOV-81       5000                    10

SQL> connect vivek/vivek
Connected.
SQL> column sql_text for a30 wrap
SQL> set lines 132
SQL> select sql_id, sql_text, executions, child_latch, version_count from v$sqlarea where sql_text like 'select /*+ VIVEK%';

SQL_ID        SQL_TEXT                       EXECUTIONS CHILD_LATCH VERSION_COUNT
------------- ------------------------------ ---------- ----------- -------------
f2q4bgycc2b1y select /*+ VIVEK */ * from emp          4           1             4
               where ename=:b1

SQL> select sql_id, sql_text, executions, plan_hash_value, child_latch from v$sql where sql_text lik
e 'select /*+ VIVEK%';

SQL_ID        SQL_TEXT                       EXECUTIONS PLAN_HASH_VALUE CHILD_LATCH
------------- ------------------------------ ---------- --------------- -----------
f2q4bgycc2b1y select /*+ VIVEK */ * from emp          1      3956160932           1
               where ename=:b1

f2q4bgycc2b1y select /*+ VIVEK */ * from emp          1      3956160932           1
               where ename=:b1

f2q4bgycc2b1y select /*+ VIVEK */ * from emp          1      3956160932           1
               where ename=:b1

f2q4bgycc2b1y select /*+ VIVEK */ * from emp          1      3956160932           1
               where ename=:b1

SQL> select sql_id, address, child_address, optimizer_mismatch, bind_mismatch, translation_mismatch
  2  from v$sql_shared_cursor where sql_id='f2q4bgycc2b1y';

SQL_ID        ADDRESS  CHILD_AD O B T
------------- -------- -------- - - -
f2q4bgycc2b1y 6A2CF72C 69334384 N N N
f2q4bgycc2b1y 6A2CF72C 6A25D5DC Y N N
f2q4bgycc2b1y 6A2CF72C 684F5684 Y Y N
f2q4bgycc2b1y 6A2CF72C 69144D8C Y N Y

One Parent Cursor, check the SQL_ID and ADDRESS from V$SQLAREA & V$SQL, multiple Child Cursors with Child_Number in V$SQL. The CHILD_ADDRESS from V$SQL can be mapped to CHILD_ADDRESS of V$SQL_SHARED_CURSOR to get the reason for the mismatch between already stored child cursor.

In this test case, we executed 4 queries. While each of these were textually similar, they shared same library cache parent cursor. When the first query was executed, it created a parent cursor and a child cursor. When second query was executed, as it was textually similar, it shared a same parent cursor but, since optimizer_index_cost_adj was set to a non-default value, optimizer assumed that this setting might require another plan and creates a new child cursor. Another query was again textually same, but was executed on a different object, schema Y and the fourth one, was with a modified bind length. In this case, while optimizer assumed that the plan would be different, it created a new child cursor, but if you look at the PLAN_HASH_VALUE form V$SQL, the plan for each of these is same i.e. FTS.

Now, for the another test on BIND GRADUATION. In order to limit the creation of number of child cursors, Oracle limits and maps the size of a Bind Length (for VARCHAR2 only) to a maximum limit. Lets execute the queries with different bind length and check the number of child cursors for each of these.

SQL> alter system flush shared_pool;

System altered.

SQL> variable b1 varchar2(10);
SQL> exec :b1:='KING';

PL/SQL procedure successfully completed.

SQL> select /*+ VIVEK */ * from emp where ename=:b1;

     EMPNO ENAME      JOB              MGR HIREDATE         SAL       COMM     DEPTNO
---------- ---------- --------- ---------- --------- ---------- ---------- ----------
      7839 KING       PRESIDENT            17-NOV-81       5000                    10

SQL> variable b1 varchar2(30);
SQL> exec :b1:='KING';

PL/SQL procedure successfully completed.

SQL> select /*+ VIVEK */ * from emp where ename=:b1;

     EMPNO ENAME      JOB              MGR HIREDATE         SAL       COMM     DEPTNO
---------- ---------- --------- ---------- --------- ---------- ---------- ----------
      7839 KING       PRESIDENT            17-NOV-81       5000                    10

SQL> variable b1 varchar2(40);
SQL> exec :b1:='KING';

PL/SQL procedure successfully completed.

SQL> select /*+ VIVEK */ * from emp where ename=:b1;

     EMPNO ENAME      JOB              MGR HIREDATE         SAL       COMM     DEPTNO
---------- ---------- --------- ---------- --------- ---------- ---------- ----------
      7839 KING       PRESIDENT            17-NOV-81       5000                    10

SQL> variable b1 varchar2(80);
SQL> exec :b1:='KING';

PL/SQL procedure successfully completed.

SQL> select /*+ VIVEK */ * from emp where ename=:b1;

     EMPNO ENAME      JOB              MGR HIREDATE         SAL       COMM     DEPTNO
---------- ---------- --------- ---------- --------- ---------- ---------- ----------
      7839 KING       PRESIDENT            17-NOV-81       5000                    10

SQL> variable b1 varchar2(140);
SQL> exec :b1:='KING';

PL/SQL procedure successfully completed.

SQL> select /*+ VIVEK */ * from emp where ename=:b1;

     EMPNO ENAME      JOB              MGR HIREDATE         SAL       COMM     DEPTNO
---------- ---------- --------- ---------- --------- ---------- ---------- ----------
      7839 KING       PRESIDENT            17-NOV-81       5000                    10

SQL> variable b1 varchar2(2040);
SQL> exec :b1:='KING';

PL/SQL procedure successfully completed.

SQL> select /*+ VIVEK */ * from emp where ename=:b1;

     EMPNO ENAME      JOB              MGR HIREDATE         SAL       COMM     DEPTNO
---------- ---------- --------- ---------- --------- ---------- ---------- ----------
      7839 KING       PRESIDENT            17-NOV-81       5000                    10

SQL> column sql_text for a30 wrap
SQL> set lines 132
SQL> select sql_id, sql_text, executions, child_latch, version_count from v$sqlarea where sql_text like 'select /*+ VIVEK%';

SQL_ID        SQL_TEXT                       EXECUTIONS CHILD_LATCH VERSION_COUNT
------------- ------------------------------ ---------- ----------- -------------
f2q4bgycc2b1y select /*+ VIVEK */ * from emp          6           1             4
               where ename=:b1

SQL> select sql_id, sql_text, executions, child_latch from v$sql where sql_text like 'select /*+ VIVEK%';

SQL_ID        SQL_TEXT                       EXECUTIONS CHILD_LATCH
------------- ------------------------------ ---------- -----------
f2q4bgycc2b1y select /*+ VIVEK */ * from emp          2           1
               where ename=:b1

f2q4bgycc2b1y select /*+ VIVEK */ * from emp          2           1
               where ename=:b1

f2q4bgycc2b1y select /*+ VIVEK */ * from emp          1           1
               where ename=:b1

f2q4bgycc2b1y select /*+ VIVEK */ * from emp          1           1
               where ename=:b1


SQL> select * from v$sql_shared_cursor where sql_id='f2q4bgycc2b1y';

SQL_ID        ADDRESS  CHILD_AD B
------------- -------- -------- -
f2q4bgycc2b1y 6A15BC84 694FF7E8 N
f2q4bgycc2b1y 6A15BC84 68768DC8 Y
f2q4bgycc2b1y 6A15BC84 6A258CF8 Y
f2q4bgycc2b1y 6A15BC84 69BA5E00 Y

SQL> select address, position, datatype, max_length, bind_name
  2  from v$sql_bind_metadata
  3  where address in
  4  (select child_address from v$sql where sql_id='f2q4bgycc2b1y');

ADDRESS    POSITION   DATATYPE MAX_LENGTH BIND_NAME
-------- ---------- ---------- ---------- ------------------------------
694FF7E8          1          1         32 B1
68768DC8          1          1        128 B1
6A258CF8          1          1       2000 B1
69BA5E00          1          1       4000 B1

In this case, I executed the queries with different bind length. For six executions, there were 4 child cursors and the bind graduation limited these to the maximum value as exposed by V$SQL_BIND_METADATA.

This was the end of DAY 1 of my presentation and I tried to give a conceptual knowledge on CURSORS. More on this in my next 2 blogs.

This is my new Blog URL

Hi,

I have changed from blogspot to wordpress.com. For unknown reason, my previous blogspot site http://viveklsharma.blogspot.com is not accessible. I have tried contacting support to get the issue resolved, but have not yet received any response. I cannot keep away from writing, so thought of switching over to this.

While, I would try to get my previous blogspot re-activated, for any reason, if this is not done, for the benefit of my reasone, I will repost some of the important and well appreciated write-ups on this site.