<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" media="screen" href="/~d/styles/rss2full.xsl"?><?xml-stylesheet type="text/css" media="screen" href="http://feeds.feedburner.com/~d/styles/itemcontent.css"?><rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:openSearch="http://a9.com/-/spec/opensearch/1.1/" xmlns:georss="http://www.georss.org/georss" version="2.0"><channel><atom:id>tag:blogger.com,1999:blog-34560741</atom:id><lastBuildDate>Tue, 14 Jul 2009 11:38:48 +0000</lastBuildDate><title>The Momen Blog</title><description>Views expressed here are solely that of my own. Please make sure that you test the code/queries that appear on my blog before applying them on a production environment.</description><link>http://momendba.blogspot.com/</link><managingEditor>noreply@blogger.com (Asif Momen)</managingEditor><generator>Blogger</generator><openSearch:totalResults>64</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage><atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="self" href="http://feeds.feedburner.com/TheMomenBlog" type="application/rss+xml" /><item><guid isPermaLink="false">tag:blogger.com,1999:blog-34560741.post-8762740235741150147</guid><pubDate>Tue, 14 Jul 2009 11:35:00 +0000</pubDate><atom:updated>2009-07-14T14:38:48.880+03:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">Oracle 10g</category><category domain="http://www.blogger.com/atom/ns#">Performance Tuning</category><title>Who says Full Table Scans are Evil?</title><description>&lt;p&gt;&lt;/p&gt;Recently, I had a developer complaining that a query which normally runs very fast is now taking huge amount of time to complete. 
 &lt;p&gt;&lt;/p&gt;
Following is the culprit query and its execution plan:
&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt;
SELECT  DISTINCT SUBSTR(A.MF_NO,4) MF_NO,
 B.SRT_SEQ,
 SUBSTR(B.MAIN_CODE,1,6) MAIN_CODE
  FROM  DETAIL_TBL A,
        MASTER_TBL B    
  WHERE A.MRF_ID   = B.MRF_ID    
    AND A.MRF_CODE = B.MRF_CODE    
    AND TRIM(SUBSTR(A.MF_NO,4)) IN (SELECT LPAD(MF_NO,4,0) 
                                     FROM REMOTE_TBL@RMLINK 
                                    WHERE USER_LEVEL = 'RO'  
                                      AND STATUS_CODE &lt;&gt; 'D');

Execution Plan
----------------------------------------------------------------------------------------------------------------------
| Id  | Operation                    | Name                  | Rows  | Bytes | Cost (%CPU)| Time     | Inst   |IN-OUT|
----------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |                       |       |       |  6646 (100)|          |        |      |
|   1 |  HASH UNIQUE                 |                       |     1 |    62 |  6646   (2)| 00:01:34 |        |      |
|   2 |   TABLE ACCESS BY INDEX ROWID| MASTER_TBL            |     1 |    30 |     5   (0)| 00:00:01 |        |      |
|   3 |    NESTED LOOPS              |                       |     1 |    62 |  6645   (2)| 00:01:34 |        |      |
|*  4 |     HASH JOIN                |                       |     1 |    32 |  6640   (2)| 00:01:33 |        |      |
|   5 |      REMOTE                  | REMOTE_TBL            |     1 |    12 |    24   (0)| 00:00:01 |  REMDB | R-&gt;S |
|   6 |      TABLE ACCESS FULL       | DETAIL_TBL            |  4159K|    79M|  6589   (2)| 00:01:33 |        |      |
|*  7 |     INDEX RANGE SCAN         | SYS_C0011692          |     3 |       |     2   (0)| 00:00:01 |        |      |
----------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   4 - access(TRIM(SUBSTR("A"."MF_NO",:SYS_B_3))=LPAD("MF_NO",:SYS_B_4,TO_CHAR(:SYS_B_5)))
   7 - access("A"."MRF_ID"="B"."MRF_ID" AND "A"."MRF_CODE"="B"."MRF_CODE")

&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
Using AWR SQL report (awrsqrpt.sql) for the time when the query was performing better shows that a Full Table Scan was performed on the MASTER_TBL.
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;
Execution Plan
----------------------------------------------------------
Plan hash value: 1931109479

----------------------------------------------------------------------------------------------------------------------
| Id  | Operation            | Name                  | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     | Inst   |IN-OUT|
----------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT     |                       |  2919K|   172M|       | 42564   (2)| 00:09:56 |        |      |
|   1 |  HASH UNIQUE         |                       |  2919K|   172M|   424M| 42564   (2)| 00:09:56 |        |      |
|*  2 |   HASH JOIN          |                       |  2919K|   172M|    11M| 15147   (2)| 00:03:33 |        |      |
|   3 |    TABLE ACCESS FULL | MASTER_TBL            |   289K|  8494K|       |  5590   (2)| 00:01:19 |        |      |
|*  4 |    HASH JOIN         |                       |  1225K|    37M|       |  6833   (2)| 00:01:36 |        |      |
|   5 |     REMOTE           | REMOTE_TBL            |    29 |   348 |       |    24   (0)| 00:00:01 |  REMDB | R-&gt;S |
|   6 |     TABLE ACCESS FULL| DETAIL_TBL            |  4191K|    79M|       |  6781   (2)| 00:01:35 |        |      |
----------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - access("A"."MRF_ID"="B"."MRF_ID" AND "A"."MRF_CODE"="B"."MRF_CODE")
   4 - access(TRIM(SUBSTR("A"."MF_NO",4))=LPAD("MF_NO",4,'0'))

&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;

When we add a Full Table Scan (FTS) hint on the MASTER_TBL, the same query ran in less than 20 seconds. We can modify the query to include FTS hint as it is a home grown application.
&lt;p&gt;&lt;/p&gt;

 &lt;pre name="code" class="sql"&gt;
SELECT  /*+ FULL(A) */ DISTINCT SUBSTR(A.MF_NO,4) MF_NO,
 B.SRT_SEQ,
 SUBSTR(B.MAIN_CODE,1,6) MAIN_CODE
  FROM  DETAIL_TBL A,
        MASTER_TBL B    
  WHERE A.MRF_ID   = B.MRF_ID    
    AND A.MRF_CODE = B.MRF_CODE    
    AND TRIM(SUBSTR(A.MF_NO,4)) IN (SELECT LPAD(MF_NO,4,0) 
                                     FROM REMOTE_TBL@RMLINK 
                                    WHERE USER_LEVEL = 'RO'  
                                      AND STATUS_CODE &lt;&gt; 'D');


Execution Plan
----------------------------------------------------------
Plan hash value: 1931109479

----------------------------------------------------------------------------------------------------------------------
| Id  | Operation            | Name                  | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     | Inst   |IN-OUT|
----------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT     |                       |  2919K|   172M|       | 42564   (2)| 00:09:56 |        |      |
|   1 |  HASH UNIQUE         |                       |  2919K|   172M|   424M| 42564   (2)| 00:09:56 |        |      |
|*  2 |   HASH JOIN          |                       |  2919K|   172M|    11M| 15147   (2)| 00:03:33 |        |      |
|   3 |    TABLE ACCESS FULL | MASTER_TBL            |   289K|  8494K|       |  5590   (2)| 00:01:19 |        |      |
|*  4 |    HASH JOIN         |                       |  1225K|    37M|       |  6833   (2)| 00:01:36 |        |      |
|   5 |     REMOTE           | REMOTE_TBL            |    29 |   348 |       |    24   (0)| 00:00:01 |  REMDB | R-&gt;S |
|   6 |     TABLE ACCESS FULL| DETAIL_TBL            |  4191K|    79M|       |  6781   (2)| 00:01:35 |        |      |
----------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - access("A"."MRF_ID"="B"."MRF_ID" AND "A"."MRF_CODE"="B"."MRF_CODE")
   4 - access(TRIM(SUBSTR("A"."MF_NO",4))=LPAD("MF_NO",4,'0'))

&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;

This worked as an emergency painkiller before the routine lab tests are performed and an appropriate action is decided. 
&lt;p&gt;&lt;/p&gt;

In the bad execution plan, to access the master table (MASTER_TBL), optimizer is doing an index range scan (“SYS_C0011692”) and the cardinality reported is “3”. This is where the real problem is hiding. Drilling into the tables, columns and indexes for both the tables revealed this information:
&lt;p&gt;&lt;/p&gt;


&lt;pre name="code" class="sql"&gt;
Table Name: MASTER_TBL
Primary Key: MRF_ID, MRF_CODE, ORDNO
Num Rows: 287,456

Table Name: CHILD_TBL
Primary Key: MRF_ID, MRF_CODE, ORDNO
Num Rows: 4,159,964
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;

Oops… there are nearly 300,000 records in the MASTER_TBL, but the execution plan indicates a cardinality of merely 3. So, there is something’s serious wrong somewhere. First suspect is table and index statistics. As suspected, statistics on MASTER_TBL were botched up. These were the steps performed on the MASTER_TBL:
&lt;p&gt;&lt;/p&gt;

1) Data was cleaned
2) Initial data load was run
3) Statistics were collected
4) Remaining major chunk of the data loaded
&lt;p&gt;&lt;/p&gt;

After collecting fresh statistics on the MASTER_TBL things got back on track. Also, data belonging to columns “MRF_ID” &amp; “MRF_CODE” is evenly spread across all the used blocks in MASTER_TBL &amp; CHILD_TBL tables. Moreover, there is no limiting condition on these columns which could encourage optimizer to go with an index access path. So, it would make no sense for the optimizer to go with an index access path.
&lt;p&gt;&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/34560741-8762740235741150147?l=momendba.blogspot.com'/&gt;&lt;/div&gt;</description><link>http://momendba.blogspot.com/2009/07/who-says-full-table-scans-are-evil.html</link><author>noreply@blogger.com (Asif Momen)</author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-34560741.post-5794089153381377379</guid><pubDate>Sun, 17 May 2009 10:44:00 +0000</pubDate><atom:updated>2009-05-17T22:25:35.783+03:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">Database</category><category domain="http://www.blogger.com/atom/ns#">Cardinality</category><category domain="http://www.blogger.com/atom/ns#">Performance Tuning</category><title>Cardinality Analysis - A Review</title><description>&lt;p&gt;&lt;/p&gt;
After reading Michelle Deng’s “Tuning by Cardinality Feedback” paper on &lt;a href="http://jonathanlewis.wordpress.com/2009/05/11/cardinality-feedback/"&gt;Jonathan's blog&lt;/a&gt;, I thought of re-creating the whole scenario to actually feel what is going on. 
&lt;p&gt;&lt;/p&gt;

I did this test on Oracle 10g Release 2 (10.2.0.3) database with a block size of 16K. We first create the table and populate it in such a way that we match exactly with Michelle’s data.
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;
SQL&gt; select * from v$version;

BANNER
----------------------------------------------------------------
Oracle Database 10g Enterprise Edition Release 10.2.0.3.0 - 64bi
PL/SQL Release 10.2.0.3.0 - Production
CORE    10.2.0.3.0      Production
TNS for IBM/AIX RISC System/6000: Version 10.2.0.3.0 - Productio
NLSRTL Version 10.2.0.3.0 - Production

Elapsed: 00:00:00.03
SQL&gt;

SQL&gt; show parameter db_block_size

NAME                                 TYPE        VALUE
------------------------------------ ----------- ----------
db_block_size                        integer     16384
SQL&gt;

&lt;p&gt;&lt;/p&gt;
&lt;/pre&gt;
I performed couple of tests with different data distributions and here’s the first test case:
&lt;p&gt;&lt;/p&gt;

&lt;span style="font-weight:bold;"&gt;Test Case 1&lt;/span&gt;
&lt;p&gt;&lt;/p&gt;

&lt;span style="font-weight:bold;"&gt;Create and Populate Data&lt;/span&gt;
&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt;
drop table prod_fctr_mv purge;

create table prod_fctr_mv(
 extract_mkt_ssk varchar2(5), 
 mkt_ssk     varchar2(20), 
 mkt_id      number(16), 
 hier_type    varchar2(20), 
 txt       varchar2(500));

insert into prod_fctr_mv 
 select '00006', '00006', 3, 'CORP', 
        rpad('*', 285, '*') 
   from dual 
  connect by level &lt;= 42;


insert into prod_fctr_mv
  select
         lpad(mod(level, 100), 5, '0') extract_mkt_ssk ,
         lpad(mod(level, 73), 5, '0') mkt_ssk ,
         mod(level, 91) mkt_id ,
         decode(mod(level, 5), 0, 'CORP', 1, 'CALL', 2, 'CORP', 
                               3, 'WALL', 'CORP') hier_type ,
         rpad('*', 285, '*')
    from dual
   connect by level &lt;= 4965 - 42 ;


update prod_fctr_mv set mkt_ssk = null 
 where mkt_ssk &lt;&gt; '00006' 
   and rownum &lt;= 296;

update (select mkt_id 
          from prod_fctr_mv 
         where mkt_id &lt;&gt; 3) 
   set mkt_id = 3
 where rownum &lt;= 72;

update (select extract_mkt_ssk  
          from prod_fctr_mv 
         where extract_mkt_ssk &lt;&gt; '00006'
           and mkt_id &lt;&gt; 3) 
   set extract_mkt_ssk = '00006' 
 where rownum &lt;= 77;

select count(*) 
  from prod_fctr_mv
 where mkt_ssk  = '00006' ;

update (select mkt_ssk 
          from prod_fctr_mv 
         where mkt_ssk &lt;&gt; '00006' 
           and mkt_ssk is not null ) 
   set mkt_ssk = '00006' 
 where rownum &lt;= 59;

update (select hier_type 
          from prod_fctr_mv 
         where hier_type = 'CORP' 
           and mkt_id &lt;&gt; 3
           and extract_mkt_ssk &lt;&gt; '00006')
   set hier_type = decode(mod(rownum, 2), 0, 'TALL', 'BALL') 
 where rownum &lt;= 554;

commit;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;

&lt;span style="font-weight:bold;"&gt;Verify Data&lt;/span&gt;
&lt;p&gt;&lt;/p&gt;

Now that the data is populated, let’s see did we get on the right track.
&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt;
SQL&gt; exec dbms_stats.gather_table_stats(user, 'prod_fctr_mv');

PL/SQL procedure successfully completed.

Elapsed: 00:00:01.42
SQL&gt; 
SQL&gt; select table_name, num_rows, blocks, empty_blocks, avg_space, 
  2         chain_cnt, avg_row_len
  3   from user_tables
  4  where table_name = 'PROD_FCTR_MV';

TABLE_NAME                       NUM_ROWS     BLOCKS EMPTY_BLOCKS  AVG_SPACE  CHAIN_CNT AVG_ROW_LEN
------------------------------ ---------- ---------- ------------ ---------- ---------- -----------
PROD_FCTR_MV                         4965        108            0          0          0         305

Elapsed: 00:00:00.01
SQL&gt; 
SQL&gt; 
SQL&gt; select column_name, num_distinct, density, num_buckets, 
  2         num_nulls, histogram
  3    from user_tab_columns
  4   where table_name = 'PROD_FCTR_MV';

COLUMN_NAME                    NUM_DISTINCT    DENSITY NUM_BUCKETS  NUM_NULLS HISTOGRAM
------------------------------ ------------ ---------- ----------- ---------- ---------------
TXT                                       1          1           1          0 NONE
HIER_TYPE                                 5 .000100705           5          0 FREQUENCY
MKT_ID                                   91 .000100705          91          0 FREQUENCY
MKT_SSK                                  73 .000107089          73        296 FREQUENCY
EXTRACT_MKT_SSK                         100 .000100705         100          0 FREQUENCY

Elapsed: 00:00:00.07
SQL&gt; 
SQL&gt; 
SQL&gt; select count(decode(mkt_id, 3, 1)) mkt_id,
  2         count(decode(extract_mkt_ssk, '00006', 1)) extract_mkt_ssk,
  3         count(decode(mkt_ssk, '00006', 1)) mkt_ssk,
  4         count(decode(hier_type, 'CORP', 1)) hier_type
  5    from prod_fctr_mv ;

    MKT_ID EXTRACT_MKT_SSK    MKT_SSK  HIER_TYPE
---------- --------------- ---------- ----------
       169             169        169       2441

Elapsed: 00:00:00.01
SQL&gt; 
SQL&gt; select count(*) 
  2    from prod_fctr_mv
  3   where mkt_id  = 3
  4     and extract_mkt_ssk = '00006'
  5     and mkt_ssk = '00006'
  6     and hier_type = 'CORP';

  COUNT(*)
----------
        42

Elapsed: 00:00:00.03
SQL&gt; 
SQL&gt;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;

&lt;span style="font-weight:bold;"&gt;Search an optimal value for OPTIMIZER_DYNAMIC_SAMPLING parameter&lt;/span&gt;
&lt;p&gt;&lt;/p&gt;

Yea, the total number of rows, the count for the key columns, count for the query in question, and the statistics, they all match to what was presented by Michelle.
&lt;p&gt;&lt;/p&gt;

The real fun starts here:
&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt;
SQL&gt; set autotrace traceonly exp
SQL&gt; 
SQL&gt; select count(*) 
  2    from prod_fctr_mv
  3   where mkt_id  = 3
  4     and extract_mkt_ssk = '00006'
  5     and mkt_ssk = '00006'
  6     and hier_type = 'CORP';
Elapsed: 00:00:00.00

Execution Plan
----------------------------------------------------------
Plan hash value: 2899207340

-----------------------------------------------------------------------------------
| Id  | Operation          | Name         | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |              |     1 |    20 |    42   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE    |              |     1 |    20 |            |          |
|*  2 |   TABLE ACCESS FULL| PROD_FCTR_MV |     1 |    20 |    42   (0)| 00:00:01 |
-----------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter("MKT_ID"=3 AND "EXTRACT_MKT_SSK"='00006' AND
              "MKT_SSK"='00006' AND "HIER_TYPE"='CORP')
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;

Delete the statistics and run the same query:
&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt;
SQL&gt; exec dbms_stats.delete_table_stats(user, 'prod_fctr_mv');

PL/SQL procedure successfully completed.

Elapsed: 00:00:00.39
SQL&gt; 
SQL&gt; select count(*) 
  2    from prod_fctr_mv
  3   where mkt_id  = 3
  4     and extract_mkt_ssk = '00006'
  5     and mkt_ssk = '00006'
  6     and hier_type = 'CORP';
Elapsed: 00:00:00.00

Execution Plan
----------------------------------------------------------
Plan hash value: 2899207340

-----------------------------------------------------------------------------------
| Id  | Operation          | Name         | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |              |     1 |    41 |    42   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE    |              |     1 |    41 |            |          |
|*  2 |   TABLE ACCESS FULL| PROD_FCTR_MV |    72 |  2952 |    42   (0)| 00:00:01 |
-----------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter("MKT_ID"=3 AND "EXTRACT_MKT_SSK"='00006' AND
              "MKT_SSK"='00006' AND "HIER_TYPE"='CORP')

Note
-----
   - dynamic sampling used for this statement

SQL&gt;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;

Optimizer has considered dynamic sampling but still the number of rows reported is now “72”, which is nearly twice than the actual number of rows. Let’s gradually increase the value of OPTIMIZER_DYNAMIC_SAMPLING initialization parameter and check the plan.
&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt;
SQL&gt; alter session set optimizer_dynamic_sampling=3;

Session altered.

Elapsed: 00:00:00.00
SQL&gt; @t
Elapsed: 00:00:00.00

Execution Plan
----------------------------------------------------------
Plan hash value: 2899207340

-----------------------------------------------------------------------------------
| Id  | Operation          | Name         | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |              |     1 |    41 |    42   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE    |              |     1 |    41 |            |          |
|*  2 |   TABLE ACCESS FULL| PROD_FCTR_MV |    72 |  2952 |    42   (0)| 00:00:01 |
-----------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter("MKT_ID"=3 AND "EXTRACT_MKT_SSK"='00006' AND
              "MKT_SSK"='00006' AND "HIER_TYPE"='CORP')

Note
-----
   - dynamic sampling used for this statement

SQL&gt; alter session set optimizer_dynamic_sampling=4;

Session altered.

Elapsed: 00:00:00.00
SQL&gt;  @t
Elapsed: 00:00:00.00

Execution Plan
----------------------------------------------------------
Plan hash value: 2899207340

-----------------------------------------------------------------------------------
| Id  | Operation          | Name         | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |              |     1 |    41 |    42   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE    |              |     1 |    41 |            |          |
|*  2 |   TABLE ACCESS FULL| PROD_FCTR_MV |    72 |  2952 |    42   (0)| 00:00:01 |
-----------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter("MKT_ID"=3 AND "EXTRACT_MKT_SSK"='00006' AND
              "MKT_SSK"='00006' AND "HIER_TYPE"='CORP')

Note
-----
   - dynamic sampling used for this statement

SQL&gt; alter session set optimizer_dynamic_sampling=5;

Session altered.

Elapsed: 00:00:00.00
SQL&gt;   @t
Elapsed: 00:00:00.00

Execution Plan
----------------------------------------------------------
Plan hash value: 2899207340

-----------------------------------------------------------------------------------
| Id  | Operation          | Name         | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |              |     1 |    41 |    42   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE    |              |     1 |    41 |            |          |
|*  2 |   TABLE ACCESS FULL| PROD_FCTR_MV |    72 |  2952 |    42   (0)| 00:00:01 |
-----------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter("MKT_ID"=3 AND "EXTRACT_MKT_SSK"='00006' AND
              "MKT_SSK"='00006' AND "HIER_TYPE"='CORP')

Note
-----
   - dynamic sampling used for this statement

SQL&gt; alter session set optimizer_dynamic_sampling=6;

Session altered.

Elapsed: 00:00:00.01
SQL&gt; @t
Elapsed: 00:00:00.00

Execution Plan
----------------------------------------------------------
Plan hash value: 2899207340

-----------------------------------------------------------------------------------
| Id  | Operation          | Name         | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |              |     1 |    41 |    42   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE    |              |     1 |    41 |            |          |
|*  2 |   TABLE ACCESS FULL| PROD_FCTR_MV |    42 |  1722 |    42   (0)| 00:00:01 |
-----------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter("MKT_ID"=3 AND "EXTRACT_MKT_SSK"='00006' AND
              "MKT_SSK"='00006' AND "HIER_TYPE"='CORP')

Note
-----
   - dynamic sampling used for this statement

SQL&gt;
SQL&gt; set autotrace off
SQL&gt;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;

Finally, when OPTIMIZER_DYNAMIC_SAMPLING parameter reached to a value of “6”, the plan is now reporting correct cardinality.
&lt;p&gt;&lt;/p&gt;

&lt;span style="font-weight:bold;"&gt;Test Case 2&lt;/span&gt;
&lt;p&gt;&lt;/p&gt;

Let’s run through the entire test again with a slight modification. Instead of inserting all the 42 rows together, we will insert them scattered in between.
&lt;p&gt;&lt;/p&gt;

&lt;span style="font-weight:bold;"&gt;Create and Populate Data&lt;/span&gt;
&lt;p&gt;&lt;/p&gt;

In this test case, we will insert one row of our interest after every 100 rows. So, here goes the script:
&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt;
drop table prod_fctr_mv purge;

create table prod_fctr_mv(
 extract_mkt_ssk varchar2(5), 
 mkt_ssk     varchar2(20), 
 mkt_id      number(16), 
 hier_type    varchar2(20), 
 txt       varchar2(500));

begin
  for i in 1..42 loop
    insert into prod_fctr_mv 
     select '00006', '00006', 3, 'CORP', 
            rpad('*', 285, '*') 
       from dual 
      connect by level &lt;= 1;

    insert into prod_fctr_mv
      select
            lpad(mod(level, 100), 5, '0') extract_mkt_ssk ,
            lpad(mod(level, 73), 5, '0') mkt_ssk ,
            mod(level, 91) mkt_id ,
            decode(mod(level, 5), 0, 'CORP', 1, 'CALL', 2, 'CORP', 
                                  3, 'WALL', 'CORP') hier_type ,
            rpad('*', 285, '*')
       from dual
      connect by level &lt;= 100 ;
  end loop;

  commit;
end;
/

select count(*) 
  from prod_fctr_mv;

  insert into prod_fctr_mv
    select
          lpad(mod(level, 100), 5, '0') extract_mkt_ssk ,
          lpad(mod(level, 73), 5, '0') mkt_ssk ,
          mod(level, 91) mkt_id ,
          decode(mod(level, 5), 0, 'CORP', 1, 'CALL', 2, 'CORP', 
                                3, 'WALL', 'CORP') hier_type ,
          rpad('*', 285, '*')
     from dual
    connect by level &lt;= 723 ;

update prod_fctr_mv set mkt_ssk = null 
 where mkt_ssk &lt;&gt; '00006' 
   and rownum &lt;= 296;

update (select mkt_id 
          from prod_fctr_mv 
         where mkt_id &lt;&gt; 3
           and extract_mkt_ssk &lt;&gt; '00006') 
   set mkt_id = 3
 where rownum &lt;= 35;

update (select extract_mkt_ssk  
          from prod_fctr_mv 
         where extract_mkt_ssk &lt;&gt; '00006'
           and mkt_id &lt;&gt; 3) 
   set extract_mkt_ssk = '00006' 
 where rownum &lt;= 77;

update (select mkt_ssk 
          from prod_fctr_mv 
         where mkt_ssk &lt;&gt; '00006' 
           and mkt_ssk is not null ) 
   set mkt_ssk = '00006' 
 where rownum &lt;= 33;

update (select hier_type 
          from prod_fctr_mv 
         where hier_type = 'CORP' 
           and mkt_id &lt;&gt; 3
           and extract_mkt_ssk &lt;&gt; '00006')
   set hier_type = decode(mod(rownum, 2), 0, 'TALL', 'BALL') 
 where rownum &lt;= 554;

commit;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;

&lt;span style="font-weight:bold;"&gt;Verify Data&lt;/span&gt;
&lt;p&gt;&lt;/p&gt;

Let us run through the same queries to verify data population. 
&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt;
SQL&gt; exec dbms_stats.gather_table_stats(user, 'prod_fctr_mv');

PL/SQL procedure successfully completed.

Elapsed: 00:00:01.09
SQL&gt; select table_name, num_rows, blocks, empty_blocks, avg_space, 
  2         chain_cnt, avg_row_len
  3   from user_tables
  4  where table_name = 'PROD_FCTR_MV';

TABLE_NAME                       NUM_ROWS     BLOCKS EMPTY_BLOCKS  AVG_SPACE  CHAIN_CNT AVG_ROW_LEN
------------------------------ ---------- ---------- ------------ ---------- ---------- -----------
PROD_FCTR_MV                         4965        108            0          0          0         305

Elapsed: 00:00:00.01
SQL&gt; 
SQL&gt; 
SQL&gt; select column_name, num_distinct, density, num_buckets, 
  2         num_nulls, histogram
  3    from user_tab_columns
  4   where table_name = 'PROD_FCTR_MV';

COLUMN_NAME                    NUM_DISTINCT    DENSITY NUM_BUCKETS  NUM_NULLS HISTOGRAM
------------------------------ ------------ ---------- ----------- ---------- ---------------
TXT                                       1          1           1          0 NONE
HIER_TYPE                                 5 .000100705           5          0 FREQUENCY
MKT_ID                                   91 .000100705          91          0 FREQUENCY
MKT_SSK                                  73 .000107089          73        296 FREQUENCY
EXTRACT_MKT_SSK                         100 .000100705         100          0 FREQUENCY

Elapsed: 00:00:00.03
SQL&gt; 
SQL&gt; select count(decode(mkt_id, 3, 1)) mkt_id,
  2         count(decode(extract_mkt_ssk, '00006', 1)) extract_mkt_ssk,
  3         count(decode(mkt_ssk, '00006', 1)) mkt_ssk,
  4         count(decode(hier_type, 'CORP', 1)) hier_type
  5    from prod_fctr_mv ;

    MKT_ID EXTRACT_MKT_SSK    MKT_SSK  HIER_TYPE
---------- --------------- ---------- ----------
       169             169        169       2441

Elapsed: 00:00:00.01
SQL&gt; 
SQL&gt; select count(*) 
  2    from prod_fctr_mv
  3   where mkt_id  = 3
  4     and extract_mkt_ssk = '00006'
  5     and mkt_ssk = '00006'
  6     and hier_type = 'CORP';

  COUNT(*)
----------
        42

Elapsed: 00:00:00.01
SQL&gt;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;

&lt;span style="font-weight:bold;"&gt;Search an optimal value for OPTIMIZER_DYNAMIC_SAMPLING parameter&lt;/span&gt;
&lt;p&gt;&lt;/p&gt;

We managed to get the number of records and other statistics same as that of the original document. Now, we will hunt for the optimal value of OPTIMIZER_DYNAMIC_SAMPLING parameter.   
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;
SQL&gt; set autotrace traceonly exp
SQL&gt; 
SQL&gt; @t
Elapsed: 00:00:00.00

Execution Plan
----------------------------------------------------------
Plan hash value: 2899207340

-----------------------------------------------------------------------------------
| Id  | Operation          | Name         | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |              |     1 |    20 |    42   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE    |              |     1 |    20 |            |          |
|*  2 |   TABLE ACCESS FULL| PROD_FCTR_MV |     1 |    20 |    42   (0)| 00:00:01 |
-----------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter("MKT_ID"=3 AND "EXTRACT_MKT_SSK"='00006' AND
              "MKT_SSK"='00006' AND "HIER_TYPE"='CORP')

SQL&gt; exec dbms_stats.delete_table_stats(user, 'prod_fctr_mv');

PL/SQL procedure successfully completed.

Elapsed: 00:00:00.25
SQL&gt; 
SQL&gt; @t
Elapsed: 00:00:00.00

Execution Plan
----------------------------------------------------------
Plan hash value: 2899207340

-----------------------------------------------------------------------------------
| Id  | Operation          | Name         | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |              |     1 |    41 |    42   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE    |              |     1 |    41 |            |          |
|*  2 |   TABLE ACCESS FULL| PROD_FCTR_MV |    36 |  1476 |    42   (0)| 00:00:01 |
-----------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter("MKT_ID"=3 AND "EXTRACT_MKT_SSK"='00006' AND
              "MKT_SSK"='00006' AND "HIER_TYPE"='CORP')

Note
-----
   - dynamic sampling used for this statement

SQL&gt;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;

We get a better cardinality reported by the Optimizer at the default level of dynamic sampling (OPTIMIZER_DYNAMIC_SAMPLING=2). 
&lt;p&gt;&lt;/p&gt;

&lt;span style="font-weight:bold;"&gt;Conclusion&lt;/span&gt;
&lt;p&gt;&lt;/p&gt;

In the above test cases the Oracle optimizer started displaying correct number of rows when OPTIMIZER_DYNAMIC_SAMPLING was set to 6 with interested data being inserted together while the default setting for OPTIMIZER_DYNAMIC_SAMPLING worked when data was almost equally distributed. 
&lt;p&gt;&lt;/p&gt;

Michelle’s data distribution might be such that the Optimizer was able report correct number of rows at OPTIMIZER_DYNAMIC_SAMPLING=4.
&lt;p&gt;&lt;/p&gt;

Lessons learned include:
&lt;p&gt;&lt;/p&gt;

1) Column correlation could really mislead Optimizer, and
&lt;p&gt;&lt;/p&gt;
2) Data distribution plays equally critical role.

&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/34560741-5794089153381377379?l=momendba.blogspot.com'/&gt;&lt;/div&gt;</description><link>http://momendba.blogspot.com/2009/05/cardinality-analysis-review.html</link><author>noreply@blogger.com (Asif Momen)</author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-34560741.post-4994868826848541274</guid><pubDate>Wed, 15 Apr 2009 11:07:00 +0000</pubDate><atom:updated>2009-04-15T14:11:26.338+03:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">Oracle 10g</category><category domain="http://www.blogger.com/atom/ns#">ORA-07445</category><title>ORA-07445 When Querying V$SQL_PLAN View</title><description>On one of our production databases, I was looking for a particular index usage as how often was it used and at what times it was used. To achieve this, I did a little mining on AWR repository tables and found out that it was used 11 times in the last one month (AWR retention period). 
&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt;
SQL&gt; select * from v$version;

BANNER
----------------------------------------------------------------
Oracle Database 10g Enterprise Edition Release 10.2.0.4.0 - 64bi
PL/SQL Release 10.2.0.4.0 - Production
CORE    10.2.0.4.0      Production
TNS for IBM/AIX RISC System/6000: Version 10.2.0.4.0 - Productio
NLSRTL Version 10.2.0.4.0 - Production

Elapsed: 00:00:00.01
SQL&gt;


SQL&gt; select sp.sql_id,
  2         sp.options,
  3         count(1) cnt
  4    from dba_hist_sql_plan sp,
  5         dba_hist_sqlstat ss
  6    where sp.sql_id = ss.sql_id
  7      and sp.object_owner  =  'PAYMAST' 
  8      and sp.operation like '%INDEX%'
  9      and sp.object_name = 'IDX_COMMIT_DET'
 10  group by sp.sql_id, 
 11           sp.options
 12  order by sp.sql_id, 
 13           sp.options;

SQL_ID        OPTIONS                               CNT
------------- ------------------------------ ----------
1b4xf87ntvfgn SAMPLE FAST FULL SCAN                   1
8qaj7c6wqcyvg SAMPLE FAST FULL SCAN                   7
9jcrv4kunuhg4 SAMPLE FAST FULL SCAN                   1
afzq942kp848t RANGE SCAN                              1
ar6c0r4s4kntp RANGE SCAN                              1

Elapsed: 00:00:00.54
SQL&gt;          
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;

However, querying V$SQL_PLAN view for the same index threw ORA-03113 error. Hmm, I connected to the database and ran the same query again and it resulted in the same error. 
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;
SQL&gt; select * from V$SQL_PLAN where  operation  ='INDEX' and object_name ='IDX_COMMIT_DET';

ADDRESS          HASH_VALUE SQL_ID        PLAN_HASH_VALUE CHILD_ADDRESS    CHILD_NUMBER TIMESTAMP       
---------------- ---------- ------------- --------------- ---------------- ------------ ------------
FILTER_PREDICATES                                                                                       
----------------------------------------------------------------------------------------------------
REMARKS                                                                                                 
----------------------------------------------------------------------------------------------------
07000000D98392B0 2773750041 afzq942kp848t       380836487 07000000D986D420            0 15-APR2009 12:2
"VOU_TYPE_NAME"=:B1                                                                                     


ERROR:
ORA-03113: end-of-file on communication channel



ERROR:
ORA-03114: not connected to ORACLE


Elapsed: 00:00:03.82
SQL&gt;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;

Oracle has bumped some information into the alert log and here it is:
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;
Errors in file /dbdata1/oradba/admin/PAYPROD/udump/payprod_ora_6488244.trc:
ORA-07445: exception encountered: core dump [msqsub+0008] [SIGSEGV] [Address not mapped to object] [0x35B000000000070] [] []
&lt;/pre&gt;

&lt;p&gt;&lt;/p&gt;

and it created a 18MB trace file in the USER_DUMP_DEST which has all the gory details for the Oracle Support to work on. Below is an excerpt from the trace file:
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;
*** 2009-04-15 12:27:46.646
ksedmp: internal or fatal error
ORA-07445: exception encountered: core dump [msqsub+0008] [SIGSEGV] [Address not mapped to object] [0x35B000000000070] [] []
Current SQL statement for this session:
select * from V$SQL_PLAN where  operation  ='INDEX' and object_name ='IDX_COMMIT_DET'
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;

The ORA-00600/ORA-07445 Lookup Tool in Oracle Metalink reports “A description for this ORA-07445 error is not yet available”.
&lt;p&gt;&lt;/p&gt;

I have to end up by lodging an SR and wait for Oracle Support’s response.
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/34560741-4994868826848541274?l=momendba.blogspot.com'/&gt;&lt;/div&gt;</description><link>http://momendba.blogspot.com/2009/04/ora-07445-when-querying-vsqlplan-view.html</link><author>noreply@blogger.com (Asif Momen)</author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">4</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-34560741.post-874252633232881647</guid><pubDate>Tue, 17 Mar 2009 11:56:00 +0000</pubDate><atom:updated>2009-03-17T15:00:24.970+03:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">Oracle 10g</category><category domain="http://www.blogger.com/atom/ns#">Data Guard</category><title>Strange Data Guard Issue</title><description>We have recently failed over one of our database to the DRC. At that time the production archivelog sequence was 80,000 plus. After staying there for couple of hours we recreated a physical standby database on the primary and switched back to original primary location. Everything went fine without any hoo-ha. 
&lt;p&gt;&lt;/p&gt;
The failover and switchover took place on February 20, 2009. We have an automated job on all our standby databases to check whether the Standby is lagging behind the Primary, if so, then it automatically restores the missing archivelogs and applies them.
&lt;p&gt;&lt;/p&gt;
This job was working fine until day before yesterday (March 15, 2009) but started reporting errors thereafter. It failed with “RMAN-20242” error. 
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;
Starting restore at 15-MAR-09
released channel: ch12
RMAN-00571: ===========================================================
RMAN-00569: =============== ERROR MESSAGE STACK FOLLOWS ===============
RMAN-00571: ===========================================================
RMAN-03002: failure of restore command at 03/15/2009 07:26:59
RMAN-06004: ORACLE error from recovery catalog database: 
RMAN-20242: specification does not match any archive log in the recovery catalog
&lt;/pre&gt;


&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;

When notified, I queried V$ARCHIVE_GAP and the output of this query took me to a surprise:
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;
SQL&gt; select * from v$version;

BANNER
----------------------------------------------------------------
Oracle Database 10g Enterprise Edition Release 10.2.0.3.0 - 64bi
PL/SQL Release 10.2.0.3.0 - Production
CORE    10.2.0.3.0      Production
TNS for IBM/AIX RISC System/6000: Version 10.2.0.3.0 - Production
NLSRTL Version 10.2.0.3.0 - Production

SQL&gt;
SQL&gt; select * from v$archive_gap;

   THREAD# LOW_SEQUENCE# HIGH_SEQUENCE#
---------- ------------- --------------
         1           776          82468

SQL&gt; 
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;

Well, all of a sudden Oracle thinks that it is 81 thousand archive logs behind the primary. I tried to switching couple of archive logs on the production and the gap was still intact. I don't really know from where did Oracle took this information.
&lt;p&gt;&lt;/p&gt;

The automated job was trying to restore archives between 776 and 82468 and was kicked out by the recovery catalog database saying, there are no such archive logs (in fact there exist no such archivelogs).
&lt;p&gt;&lt;/p&gt;

I created a standby controlfile on the production database and replaced the controlfile on the standby database with the new one. The standby database seems to be happy with this version of controlfile and have stopped reporting any gap and the automated job is also leading a happy life.
&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt;
SQL&gt; select * from v$archive_gap;

no rows selected

SQL&gt;
&lt;/pre&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/34560741-874252633232881647?l=momendba.blogspot.com'/&gt;&lt;/div&gt;</description><link>http://momendba.blogspot.com/2009/03/strange-data-guard-issue.html</link><author>noreply@blogger.com (Asif Momen)</author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-34560741.post-8310157777697931971</guid><pubDate>Sat, 07 Feb 2009 14:18:00 +0000</pubDate><atom:updated>2009-02-13T04:11:24.559+03:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">Encryption</category><category domain="http://www.blogger.com/atom/ns#">PL/SQL</category><category domain="http://www.blogger.com/atom/ns#">Performance Tuning</category><title>Working with DBMS_CRYPTO Package</title><description>Oracle Database 10g provides means to encrypt and decrypt your sensitive data using the built-in API called DBMS_CRYPTO. Using DBMS_CRYPTO API one can encrypt sensitive information like SSN, Credit Card Numbers, Debit Card Numbers, etc stored in the database as these are not supposed to be stored in plain text for security reasons.
The DBMS_CRYPTO toolkit is easy to use and is intended to replace DBMS_OBFUSCATION_TOOLKIT which was introduced in 8.1.6. 

&lt;p&gt;&lt;/p&gt;
The concept behind any encryption toolkit is to have two things:
 1) An algorithm, the actual logic, and 
 2) The encryption key
&lt;p&gt;&lt;/p&gt; 
Using these combination's we can safely encrypt our data.
&lt;p&gt;&lt;/p&gt;
In this blog entry, let us see how DBMS_CRYPTO can be used to encrypt data and also consider its performance implications.
&lt;p&gt;&lt;/p&gt;
To start with, lets create a test table and insert couple of records in plain text:
&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt;
SQL&gt; select * from v$version;

BANNER
----------------------------------------------------------------
Oracle Database 10g Enterprise Edition Release 10.2.0.4.0 - Prod
PL/SQL Release 10.2.0.4.0 - Production
CORE    10.2.0.4.0      Production
TNS for 32-bit Windows: Version 10.2.0.4.0 - Production
NLSRTL Version 10.2.0.4.0 - Production

SQL&gt;
SQL&gt; create table emp(empid number, ename varchar2(100), ssn varchar2(80));

Table created.

SQL&gt; insert into emp values (1, 'SMITH',        123456701);

1 row created.

SQL&gt; insert into emp values (2, 'ALLEN',        123456702);

1 row created.

SQL&gt; insert into emp values (3, 'WARD',         123456703);

1 row created.

SQL&gt; commit;
SQL&gt; set line 10000
SQL&gt; column ename format a30
SQL&gt; column ssn format a40
SQL&gt; select * from emp;

     EMPID ENAME                          SSN
---------- ------------------------------ -------------
         1 SMITH                          123456701
         2 ALLEN                          123456702
         3 WARD                           123456703

SQL&gt;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
The SSN has been inserted into the table in plain text and can be read by anybody having SELECT privilege on EMP table. Now, lets create a package using DBMS_CRYPTO API to handle encryption and decryption.
&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt;
SQL&gt; Create or replace package cryptit is
  2    Function encrypt_data( p_data IN VARCHAR2 ) Return RAW DETERMINISTIC;
  3    Function decrypt_data( p_data IN RAW ) Return VARCHAR2 DETERMINISTIC;
  4  End cryptit;
  5  /

Package created.

SQL&gt;
SQL&gt; Create or replace package body cryptit is
  2    V_Key       RAW(128) := UTL_RAW.cast_to_raw('testkey1');          -- Key
  3
  4    Function encrypt_data( p_data IN VARCHAR2 ) Return RAW DETERMINISTIC
  5    IS
  6      l_data RAW(2048) := utl_raw.cast_to_raw(p_data);
  7      l_encrypted RAW(2048);
  8    BEGIN
  9      NULL;
 10      l_encrypted := dbms_crypto.encrypt                        -- Algorithm
 11                     ( src =&gt; l_data,
 12                       typ =&gt; DBMS_CRYPTO.DES_CBC_PKCS5,
 13                       key =&gt; V_KEY );
 14
 15      Return l_encrypted;
 16    END encrypt_data;
 17
 18    Function decrypt_data( p_data IN RAW ) Return VARCHAR2 DETERMINISTIC
 19    IS
 20      l_decrypted RAW(2048);
 21    BEGIN
 22      l_decrypted := dbms_crypto.decrypt                              -- Algorithm
 23                      ( src =&gt; p_data,
 24                        typ =&gt; DBMS_CRYPTO.DES_CBC_PKCS5,
 25                        key =&gt; V_KEY );
 26
 27      Return utl_raw.cast_to_varchar2(l_decrypted);
 28    END decrypt_data;
 29  End cryptit;
 30  /

Package body created.

SQL&gt;
&lt;p&gt;&lt;/p&gt;
&lt;/pre&gt;
The package body consists of functions to encrypt and decrypt data along with a key. As these functions will always output the same value for a specific input, we can make them Deterministic functions. (A Deterministic Function always returns the same result any time they are called with a specific set of input values.)
&lt;p&gt;&lt;/p&gt;
Let's now make use of these functions to encrypt our sensitive data.
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;
SQL&gt; update emp set ssn = cryptit.encrypt_data(ssn);

3 rows updated.

SQL&gt; select * from emp;

     EMPID ENAME                          SSN
---------- ------------------------------ ----------------------------------
         1 SMITH                          5F3168C22E54060DE7D97B31F7E38BB6
         2 ALLEN                          5F3168C22E54060D0C2527FBBD3DCD6C
         3 WARD                           5F3168C22E54060D8166D3757932A112

SQL&gt;
SQL&gt;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
When inserting new records we simply make use of "cryptit.encrypt_data" function in the INSERT statement.
&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt;
SQL&gt; insert into emp values( 4, 'MOMEN', cryptit.encrypt_data(123456704));

1 row created.

SQL&gt; commit;

Commit complete.

SQL&gt; select * from emp;

     EMPID ENAME                          SSN
---------- ------------------------------ ----------------------------------------
         1 SMITH                          5F3168C22E54060DE7D97B31F7E38BB6
         2 ALLEN                          5F3168C22E54060D0C2527FBBD3DCD6C
         3 WARD                           5F3168C22E54060D8166D3757932A112
         4 MOMEN                          5F3168C22E54060D2CFF7E7A35B14187

SQL&gt;

SQL&gt; column decrypted_ssn format a15
SQL&gt; select empid, ename, cryptit.decrypt_data(ssn) decrypted_ssn, ssn from emp;

     EMPID ENAME                          DECRYPTED_SSN   SSN
---------- ------------------------------ --------------- ----------------------------------------
         1 SMITH                          123456701       5F3168C22E54060DE7D97B31F7E38BB6
         2 ALLEN                          123456702       5F3168C22E54060D0C2527FBBD3DCD6C
         3 WARD                           123456703       5F3168C22E54060D8166D3757932A112
         4 MOMEN                          123456704       5F3168C22E54060D2CFF7E7A35B14187

SQL&gt;
&lt;/pre&gt;

&lt;p&gt;&lt;/p&gt;
All is fine as far as encryption is concerned, but lets see what happens when SSN is used in the WHERE clause of queries.
We will create an index on SSN and run a query against it.
&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt;
SQL&gt; create index t_ssn on emp(ssn);

Index created.

SQL&gt; set autotrace on exp
SQL&gt; select * from emp where ssn = cryptit.encrypt_data('123456701');

     EMPID ENAME                          SSN
---------- ------------------------------ ----------------------------------------
         1 SMITH                          5F3168C22E54060DE7D97B31F7E38BB6


Execution Plan
----------------------------------------------------------
Plan hash value: 2894032564

-------------------------------------------------------------------------------------
| Id  | Operation                   | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |       |     1 |   107 |     2   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID| EMP   |     1 |   107 |     2   (0)| 00:00:01 |
|*  2 |   INDEX RANGE SCAN          | T_SSN |     1 |       |     1   (0)| 00:00:01 |
-------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - access("SSN"=RAWTOHEX("CRYPTIT"."ENCRYPT_DATA"('123456701')))

Note
-----
   - dynamic sampling used for this statement

SQL&gt;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
Well, the index "T_SSN" has been selected by the optimizer to be cost efficient. 
When a range of values is to be scanned, optimizer falls flat on its face as it has no idea and picks up FTS.
&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt;
SQL&gt; select * from emp where cryptit.decrypt_data(ssn) between '123456702' and '123456704';

     EMPID ENAME                          SSN
---------- ------------------------------ ----------------------------------------
         2 ALLEN                          5F3168C22E54060D0C2527FBBD3DCD6C
         3 WARD                           5F3168C22E54060D8166D3757932A112
         4 MOMEN                          5F3168C22E54060D2CFF7E7A35B14187


Execution Plan
----------------------------------------------------------
Plan hash value: 3956160932

--------------------------------------------------------------------------
| Id  | Operation         | Name | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |      |     1 |   107 |     3   (0)| 00:00:01 |
|*  1 |  TABLE ACCESS FULL| EMP  |     1 |   107 |     3   (0)| 00:00:01 |
--------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - filter("CRYPTIT"."DECRYPT_DATA"(HEXTORAW("SSN"))&gt;='123456702'
              AND "CRYPTIT"."DECRYPT_DATA"(HEXTORAW("SSN"))&lt;='123456704')

Note
-----
   - dynamic sampling used for this statement
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
We can also create Function Based Indexes on encrypted columns such that data is accessed faster with a better execution plan. This is the reason I have marked both ENCRYPT_DATA and DECRYPT_DATA as DETERMINISTIC functions.
&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt;
SQL&gt; create index f_ssn_idx on emp(cryptit.decrypt_data(ssn));

Index created.

SQL&gt; select * from emp where cryptit.decrypt_data(ssn) between '123456702' and '123456704';

     EMPID ENAME                          SSN
---------- ------------------------------ ----------------------------------------
         2 ALLEN                          5F3168C22E54060D0C2527FBBD3DCD6C
         3 WARD                           5F3168C22E54060D8166D3757932A112
         4 MOMEN                          5F3168C22E54060D2CFF7E7A35B14187


Execution Plan
----------------------------------------------------------
Plan hash value: 9274740

-----------------------------------------------------------------------------------------
| Id  | Operation                   | Name      | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |           |     1 |   107 |     2   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID| EMP       |     1 |   107 |     2   (0)| 00:00:01 |
|*  2 |   INDEX RANGE SCAN          | F_SSN_IDX |     1 |       |     1   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - access("CRYPTIT"."DECRYPT_DATA"(HEXTORAW("SSN"))&gt;='123456702' AND
              "CRYPTIT"."DECRYPT_DATA"(HEXTORAW("SSN"))&lt;='123456704')

Note
-----
   - dynamic sampling used for this statement

SQL&gt;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
The optimizer has rightly picked up the Function based index to access requested data quickly. 
&lt;p&gt;&lt;/p&gt;
DBMS_CRYPTO is a great way to encrypt sensitive data and we can also create indexes on the encrypted columns to speedup our queries. Lastly, remember to wrap the CRYPTIT package body so that the key is not exposed. 
&lt;p&gt;&lt;/p&gt;

DBMS_CRYPTO can also be used to encrypted data recursively. Let us try to encrypt the already encrypted SSN from the above example.
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;
SQL&gt; select * from emp;

     EMPID ENAME    SSN
---------- -------- --------------------------------------------------------------------------------
         1 SMITH    5F3168C22E54060DE7D97B31F7E38BB6
         2 ALLEN    5F3168C22E54060D0C2527FBBD3DCD6C
         3 WARD     5F3168C22E54060D8166D3757932A112
         4 MOMEN    5F3168C22E54060D2CFF7E7A35B14187

SQL&gt; update emp set ssn = cryptit.encrypt_data(ssn);

4 rows updated.

SQL&gt; select * from emp;

     EMPID ENAME    SSN
---------- -------- --------------------------------------------------------------------------------
         1 SMITH    455E357D80B1C81F91D52088DA0FE03A0C133D129CFC3879EE7A8DE993FD459DB5C9E1EBEF37062C
         2 ALLEN    455E357D80B1C81F91D52088DA0FE03A98FC410A71CE64D57935691692D433B770C5F8AF07C9113F
         3 WARD     455E357D80B1C81F91D52088DA0FE03A9D32D14FE64F41DD66BE7FEF4C31384F032C16826EAA4830
         4 MOMEN    455E357D80B1C81F91D52088DA0FE03A04D5C0AD79E689EABD46F8EC335308E32D6F7E94167F6EDF

SQL&gt; commit;

Commit complete.

SQL&gt; select empid, ename, cryptit.decrypt_data(cryptit.decrypt_data(ssn)) decrypted_ssn, ssn from emp;

     EMPID ENAME    DECRYPTED_SSN   SSN
---------- -------- --------------- --------------------------------------------------------------------------------
         1 SMITH    123456701       455E357D80B1C81F91D52088DA0FE03A0C133D129CFC3879EE7A8DE993FD459DB5C9E1EBEF37062C
         2 ALLEN    123456702       455E357D80B1C81F91D52088DA0FE03A98FC410A71CE64D57935691692D433B770C5F8AF07C9113F
         3 WARD     123456703       455E357D80B1C81F91D52088DA0FE03A9D32D14FE64F41DD66BE7FEF4C31384F032C16826EAA4830
         4 MOMEN    123456704       455E357D80B1C81F91D52088DA0FE03A04D5C0AD79E689EABD46F8EC335308E32D6F7E94167F6EDF

SQL&gt;
&lt;/pre&gt;

For information on DBMS_CRYPTO check &lt;a href=”http://download.oracle.com/docs/cd/B19306_01/appdev.102/b14258/d_crypto.htm#i1005082”&gt;Oracle documentation&lt;/a&gt;.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/34560741-8310157777697931971?l=momendba.blogspot.com'/&gt;&lt;/div&gt;</description><link>http://momendba.blogspot.com/2009/02/working-with-dbmscrypto.html</link><author>noreply@blogger.com (Asif Momen)</author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-34560741.post-1484212146832639126</guid><pubDate>Sun, 01 Feb 2009 11:00:00 +0000</pubDate><atom:updated>2009-02-01T14:04:05.470+03:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">PL/SQL</category><category domain="http://www.blogger.com/atom/ns#">Performance Tuning</category><title>Performance Impact of NOT NULL Constraint in PL/SQL</title><description>&lt;p&gt;&lt;/p&gt;
A NOT NULL constraint in PL/SQL requires that a variable contains value, if no value is specified for that variable then an error will occur. But, using this constraint in PL/SQL, we have to pay a small performance price. 
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
Let us create two procedures and perform a small test, the first procedure (TEST_NOTNULL) will be contain "NOT NULL" constraint while the second procedure (TEST_NULL) will be without it.
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;
SQL&gt; select * from v$version;

BANNER
----------------------------------------------------------------
Oracle Database 10g Enterprise Edition Release 10.2.0.4.0 - Prod
PL/SQL Release 10.2.0.4.0 - Production
CORE    10.2.0.4.0      Production
TNS for 32-bit Windows: Version 10.2.0.4.0 - Production
NLSRTL Version 10.2.0.4.0 - Production

SQL&gt;
SQL&gt; create or replace procedure test_notnull(p_iterations in number) is
  2    i  number;
  3    x  number NOT NULL := 0;
  4    t1 number;
  5  begin
  6    t1 := dbms_utility.get_time;
  7    for i in 1..p_iterations loop
  8      x := x + i;
  9    end loop;
 10    dbms_output.put_line( 'Time taken : ' || to_char(dbms_utility.get_time - t1));
 11  end;
 12  /

Procedure created.

SQL&gt;
SQL&gt;
SQL&gt; create or replace procedure test_null(p_iterations in number) is
  2    i  number;
  3    x  number := 0;
  4    t1 number;
  5  begin
  6    t1 := dbms_utility.get_time;
  7    for i in 1..p_iterations loop
  8      x := x + i;
  9      if x is null then
 10        dbms_output.put_line('i know this will never happen, but still ... ');
 11      end if;
 12    end loop;
 13    dbms_output.put_line( 'Time taken : ' || to_char(dbms_utility.get_time - t1));
 14  end;
 15  /

Procedure created.

SQL&gt;
SQL&gt;
SQL&gt;
SQL&gt; set serveroutput on
SQL&gt; exec perf_notnull(1000000);
Time taken : 20

PL/SQL procedure successfully completed.

SQL&gt; exec perf_null(1000000);
Time taken : 17

PL/SQL procedure successfully completed.

SQL&gt;
&lt;/pre&gt;

&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
Executing these procedures, we notice that TEST_NOTNULL procedure consumes a little more than its counterpart TEST_NULL. Yes, we are looping heavily as the point is to compare performance of NOT NULL contraint in PL/SQL. 
&lt;p&gt;&lt;/p&gt;
The reason the NOT NULL Constraint incurs performance cost is: 
As x is constriant by NOT NULL, the value of the expression "x + i" is first assigned to a temporary variable, this temporary variable is validated against nullity. If the variable is not null, its value is assigned to "x", otherwise an exception is raised.  So, this is where the extra time is consumed when one applies NOT NULL constraint in PL/SQL. 
&lt;p&gt;&lt;/p&gt;
An alternative approach is to manually test for nullity and raise an exception as we did in TEST_NULL procedure. 
&lt;p&gt;&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/34560741-1484212146832639126?l=momendba.blogspot.com'/&gt;&lt;/div&gt;</description><link>http://momendba.blogspot.com/2009/02/performance-impact-of-not-null.html</link><author>noreply@blogger.com (Asif Momen)</author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">1</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-34560741.post-2393175832292848964</guid><pubDate>Wed, 21 Jan 2009 11:30:00 +0000</pubDate><atom:updated>2009-01-21T14:43:41.230+03:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">OCM</category><category domain="http://www.blogger.com/atom/ns#">Oracle University</category><title>"ORA-01403: no data found" on Oracle University's Website</title><description>Hi,
&lt;p&gt;&lt;/p&gt;
Today while looking for information on Oracle Certified Master (OCM), I stumbled upon "ORA-01403" error on OU's website. Here is how I ended up with this error.
&lt;p&gt;&lt;/p&gt;
This &lt;a href="http://education.oracle.com/pls/web_prod-plq-dad/db_pages.getpage?page_id=4&amp;dc=D46327GC20&amp;p_org_id=1001&amp;lang=US"&gt;page&lt;/a&gt; lists availability of seats in US Cities, when I tried to change my country to "Saudi Arabia" (from the drop-down list on the upper-right corner of the page), it gave me "ORA-01403" error.
&lt;p&gt;&lt;/p&gt;

&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://1.bp.blogspot.com/_gZsupYKwzXg/SXcKAmAtA0I/AAAAAAAAAFw/FLQlSsuW8oU/s1600-h/OU+error.JPG"&gt;&lt;img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 320px; height: 71px;" src="http://1.bp.blogspot.com/_gZsupYKwzXg/SXcKAmAtA0I/AAAAAAAAAFw/FLQlSsuW8oU/s320/OU+error.JPG" border="0" alt=""id="BLOGGER_PHOTO_ID_5293710892317082434" /&gt;&lt;/a&gt;
&lt;p&gt;&lt;/p&gt;

It's time now to add the EXCEPTION section.
&lt;p&gt;&lt;/p&gt;

Regards&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/34560741-2393175832292848964?l=momendba.blogspot.com'/&gt;&lt;/div&gt;</description><link>http://momendba.blogspot.com/2009/01/ora-01403-no-data-found-on-oracle.html</link><author>noreply@blogger.com (Asif Momen)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://1.bp.blogspot.com/_gZsupYKwzXg/SXcKAmAtA0I/AAAAAAAAAFw/FLQlSsuW8oU/s72-c/OU+error.JPG" height="72" width="72" /><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">2</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-34560741.post-3758101764600838886</guid><pubDate>Mon, 08 Dec 2008 04:44:00 +0000</pubDate><atom:updated>2008-12-08T07:53:11.201+03:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">Error Message</category><title>"Heh, heh, that"s a joke, son"</title><description>&lt;p&gt;&lt;/p&gt;
Well, the title of this post is the error message from "&lt;a href="http://download.oracle.com/docs/cd/B19306_01/server.102/b14219/plsus.htm#sthref13413"&gt;Oracle® Database Error Messages: 10g Release 2 (10.2)&lt;/a&gt;" document.
&lt;p&gt;&lt;/p&gt;
&lt;span style="font-weight:bold;"&gt;PLS-00101:&lt;/span&gt; reserved for future use 
&lt;p&gt;&lt;/p&gt;
&lt;span style="font-weight:bold;"&gt;Cause:&lt;/span&gt; This error message is not used yet.(Heh, heh, that"s a joke, son.)
&lt;span style="font-weight:bold;"&gt;Action:&lt;/span&gt; none
&lt;p&gt;&lt;/p&gt;

Although, it has been reserved for future use but somehow it got leaked into Oracle Database 10g documentation. However, this same error is missing from &lt;a href="http://download.oracle.com/docs/cd/B28359_01/server.111/b28278/plsus.htm#sthref16547"&gt;Oracle 11g Database Error Messages&lt;/a&gt;. I think its reserved to be used with Oracle 12g, who knows?


Has anybody encountered this error ??
&lt;p&gt;&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/34560741-3758101764600838886?l=momendba.blogspot.com'/&gt;&lt;/div&gt;</description><link>http://momendba.blogspot.com/2008/12/heh-heh-thats-joke-son.html</link><author>noreply@blogger.com (Asif Momen)</author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">1</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-34560741.post-6957057651419200254</guid><pubDate>Sun, 23 Nov 2008 08:06:00 +0000</pubDate><atom:updated>2008-11-23T11:10:27.984+03:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">Dynamic Sampling</category><category domain="http://www.blogger.com/atom/ns#">Database</category><title>Dynamic Sampling Myths Dispelled</title><description>There are couple of myths associated with Dynamic Sampling, I thought of eliminating them with the help of this post. Two myths associated with DS are:
&lt;p&gt;&lt;/p&gt;
i) Default number of blocks sampled are 32 in Oracle 10g and 
ii) Tables having less than 32 blocks are not considered by the optimizer for dynamic sampling.
&lt;p&gt;&lt;/p&gt;

&lt;b&gt;Myth 1: Default number of blocks sampled are 32 in Oracle 10g&lt;/b&gt;
&lt;p&gt;&lt;/p&gt;

The default number of dynamic sampling blocks are 32. This is set using a hidden parameter “_optimizer_dyn_smp_blks” (ofcourse one should not change it). Here’s the query to see the default settings:
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;

SQL&gt; column "Parameter" format a30
SQL&gt; column "Session Value" format a20
SQL&gt; column "Instance Value" format a20
SQL&gt;
SQL&gt; select a.ksppinm  "Parameter", b.ksppstvl "Session Value", c.ksppstvl "Instance Value"
  2    from x$ksppi a, x$ksppcv b, x$ksppsv c
  3   where a.indx = b.indx and a.indx = c.indx
  4     and ksppinm= '_optimizer_dyn_smp_blks';

Parameter                      Session Value        Instance Value
------------------------------ -------------------- --------------------
_optimizer_dyn_smp_blks        32                   32

&lt;/pre&gt; 
&lt;p&gt;&lt;/p&gt;

Starting with Oracle 10g, by default “optimizer_dynamic_sampling” initialization parameter is set to “2”, meaning optimizer will dynamically sample all unanalyzed tables. The number of blocks sampled is two times the default number of dynamic sampling blocks. So at this level, Optimizer will sample 64 blocks.
&lt;p&gt;&lt;/p&gt;

To verify how many blocks are sampled, we need to dig into the 10053 trace output. For the purpose of this test, I will create a test table and dump some data into it. Later enable 10053 trace, execute the query and disable the trace.
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;

SQL&gt; drop table test purge;

Table dropped.

SQL&gt; create table test as select * from all_objects where 1=2;

Table created.

SQL&gt; create index t_object_id on test(object_id);

Index created.

SQL&gt; insert into test select * from all_objects;

35023 rows created.

SQL&gt; commit;

Commit complete.

SQL&gt; alter session set tracefile_identifier = 'blksize';

Session altered.

SQL&gt; alter session set events '10053 trace name context forever, level 1';

Session altered.

SQL&gt; select * from test where object_id = 123;

no rows selected

SQL&gt; alter session set events '10053 trace name context off';

Session altered.

SQL&gt; disc
Disconnected from Oracle Database 10g Enterprise Edition Release 10.2.0.4.0 - Production
With the Partitioning, OLAP, Data Mining and Real Application Testing options
SQL&gt;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;

Digging into the trace, we see the following: &lt;p&gt;&lt;/p&gt;


&lt;pre name="code" class="sql"&gt;
** Executed dynamic sampling query:
    level : 2
    sample pct. : 12.701613
    actual sample size : 4097
    filtered sample card. : 0
    orig. card. : 82
    block cnt. table stat. : 496
    block cnt. for sampling: 496
    max. sample block cnt. : 64
    sample block cnt. : 63
    min. sel. est. : 0.01000000
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;


Well, it’s clear for the trace that at level 2, the maximum number of blocks sampled (max. sample block cnt) are 64 and the actual number of blocks sampled (sample block cnt) are 63. (For some reason, Oracle always samples one block less)
&lt;p&gt;&lt;/p&gt;

So, this myth has been uncovered and we know how many blocks are sampled.
&lt;p&gt;&lt;/p&gt;


&lt;b&gt;Myth 2: Tables having less than 32 blocks are not considered by the optimizer for dynamic sampling&lt;/b&gt;
&lt;p&gt;&lt;/p&gt;

It is widely understood that, tables having less than 32 blocks are ignored by the optimizer for dynamic sampling.
&lt;p&gt;&lt;/p&gt;

Let’s continue using the same table as in case 1. However, it contains data, so we will truncate it. After truncating, verify number of blocks allocated by querying USER_SEGMENTS view.
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;

SQL&gt; truncate table test;

Table truncated.

SQL&gt; select blocks from user_segments where segment_name = 'TEST';

    BLOCKS
----------
         8
&lt;/pre&gt;

This table has only 8 blocks allocated. Let’s see whether optimizer considers dynamic sampling on this table by executing a simple query.

&lt;pre name="code" class="sql"&gt;
SQL&gt; set autotrace traceonly exp
SQL&gt; select * from test where object_id = 123;

Execution Plan
----------------------------------------------------------
Plan hash value: 1357081020

--------------------------------------------------------------------------
| Id  | Operation         | Name | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |      |     1 |   128 |     2   (0)| 00:00:01 |
|*  1 |  TABLE ACCESS FULL| TEST |     1 |   128 |     2   (0)| 00:00:01 |
--------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - filter("OBJECT_ID"=123)

Note
-----
   - dynamic sampling used for this statement

SQL&gt; set autotrace off
SQL&gt;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;


Yes, Oracle optimizer did consider dynamic sampling although the number of blocks were less than 32.
&lt;p&gt;&lt;/p&gt;

So, in this post we were able to uncover myths associated with dynamic sampling through very simple and reproducible examples.
&lt;p&gt;&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/34560741-6957057651419200254?l=momendba.blogspot.com'/&gt;&lt;/div&gt;</description><link>http://momendba.blogspot.com/2008/11/dynamic-sampling-myths-dispelled.html</link><author>noreply@blogger.com (Asif Momen)</author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-34560741.post-7163198005738092926</guid><pubDate>Wed, 05 Nov 2008 11:11:00 +0000</pubDate><atom:updated>2008-11-05T17:26:22.209+03:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">SQL</category><category domain="http://www.blogger.com/atom/ns#">Tuning</category><title>Analytic Functions: A Savior</title><description>Hi,
&lt;p&gt;&lt;/p&gt;
Yesterday, I have come across yet another performance issue. I received a complaint from one of our developers that they have a business report which is running extremely slow.
&lt;p&gt;&lt;/p&gt;

I received his mail and attached was the query:
&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt;
SELECT   
  XLT.VOU_DET.VOU_DATE,
  XLT.VOU_DET.VALUE_DATE,
  XLT.VOU_DET.DESCRIPTION,
  XLT.VOU_DET.PIX_CODE,
  VOU_TYPE.DESCR,
  XLT.VOU_DET.PIX_BRN,
  XLT.VOU_DET.DR_BAL_ORIG,
  XLT.VOU_DET.CR_BAL_ORIG,
  XLT.VOU_DET.CLOSING_BAL_ORIG,
  XLT.VOU_LOOKUP.NAME,
  XLT.VOU_DET.VOU_SEQ,
  (SELECT TO_CHAR(X.OPEN_BAL_ORIG) 
     FROM XLT.VOU_DET X 
    WHERE X.ASOF_DATE BETWEEN to_date('01-05-2007', 'dd-mm-yyyy') 
                          AND to_date('30-09-2008', 'dd-mm-yyyy')
      AND X.VOU_CODE  = '9900016WXYRT01'
      AND X.VOU_SEQ = 1
      AND ROWNUM = 1) OPEN_BAL_ORIG,
  (SELECT count(  XLT.VOU_DET.DR_BAL_ORIG) 
     FROM (select DR_BAL_ORIG 
              From XLT.VOU_DET X 
             WHERE X.ASOF_DATE BETWEEN to_date('01-05-2007', 'dd-mm-yyyy') 
                                   AND to_date('30-09-2008', 'dd-mm-yyyy')
               AND X.VOU_CODE  = '9900016WXYRT01'
               AND X.DR_BAL_ORIG &lt;&gt;0)) DR_BAL_ORIG_CNT,
  (SELECT count(  XLT.VOU_DET.CR_BAL_ORIG) 
     FROM (select CR_BAL_ORIG 
             From XLT.VOU_DET X 
            WHERE X.ASOF_DATE BETWEEN to_date('01-05-2007', 'dd-mm-yyyy') 
                                  AND to_date('30-09-2008', 'dd-mm-yyyy')
              AND X.VOU_CODE  = '9900016WXYRT01'
              AND X.CR_BAL_ORIG &lt;&gt;0)) CR_BAL_ORIG_CNT 
FROM
  XLT.VOU_DET,
  XLT.X_VOU_TYPE  VOU_TYPE,
  XLT.VOU_LOOKUP
WHERE XLT.VOU_DET.VOU_TYPE_ID=VOU_TYPE.VOU_TYPE_ID
  AND XLT.VOU_DET.VOU_REF(+)=XLT.VOU_LOOKUP.CUST_CODE_WNG
  AND XLT.VOU_DET.ASOF_DATE  BETWEEN  to_date('01-05-2007', 'dd-mm-yyyy') AND to_date('30-09-2008', 'dd-mm-yyyy')
  AND XLT.VOU_DET.VOU_CODE  =  '9900016WXYRT01'
ORDER BY
  XLT.VOU_DET.VOU_SEQ,
  XLT.VOU_DET.ASOF_DATE;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
This looks like a case of missing Analytic functions. 
&lt;p&gt;&lt;/p&gt;
The three SELECT statements within the query can be easily replaced with simple Analytic functions. The first one is to fetch the opening balance:
&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt;
  (SELECT TO_CHAR(X.OPEN_BAL_ORIG) 
     FROM XLT.VOU_DET X 
    WHERE X.ASOF_DATE BETWEEN to_date('01-05-2007', 'dd-mm-yyyy') 
                          AND to_date('30-09-2008', 'dd-mm-yyyy')
      AND X.VOU_CODE  = '9900016WXYRT01'
      AND X.VOU_SEQ = 1
      AND ROWNUM = 1) OPEN_BAL_ORIG,
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
and could be easily rewritten as:
&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt;
first_value(OPEN_BAL_ORIG) 
      over (partition by VOU_CODE 
                Order by ASOF_DATE, decode(VOU_SEQ, 0, 99, VOU_SEQ)) VOU_SEQ,
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
The second and third are the number of Debit and Credit transactions respectively and can also be written as:
&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt;
  count(decode(DR_BAL_ORIG, null, null, 0, null, 1)) 
      over (partition by  VOU_CODE) DR_BAL_ORIG_cnt,
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
and
&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt;
  count(decode(CR_BAL_ORIG, null, null, 0, null, 1)) 
      over (partition by  VOU_CODE) CR_BAL_ORIG_cnt
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
Now, its time to execute the modified query and (1) compare the result set, (2) measure time taken, (3) and compare statistics with the original query.
&lt;p&gt;&lt;/p&gt;
Well, the result set was compared and was convincing. The enhanced query completes in less than 20 seconds while the original took just over 17 minutes. So, the new query is 173 times faster than the original one.
&lt;p&gt;&lt;/p&gt;
Lastly, let’s compare statistics. 
&lt;p&gt;&lt;/p&gt;
Statistics of Original query:
&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt;
SQL&gt; set autotrace traceonly stat
SQL&gt; @tuneme

661 rows selected.

Elapsed: 00:17:26.91

Statistics
----------------------------------------------------------
          0  recursive calls
          0  db block gets
    2066944  consistent gets
     753812  physical reads
        116  redo size
      68506  bytes sent via SQL*Net to client
        842  bytes received via SQL*Net from client
         46  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
        661  rows processed

SQL&gt; 
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
Statistics of Modified query:
&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt;
SQL&gt; set autotrace traceonly exp stat
SQL&gt; @tunedquery

661 rows selected.

Elapsed: 00:00:17.35
Statistics
----------------------------------------------------------
          2  recursive calls
          0  db block gets
      22514  consistent gets
       2580  physical reads
          0  redo size
      68003  bytes sent via SQL*Net to client
        547  bytes received via SQL*Net from client
         46  SQL*Net roundtrips to/from client
          2  sorts (memory)
          0  sorts (disk)
        661  rows processed

SQL&gt;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
Instead of “2,066,944” consistent gets and “753,812” physical reads, it took merely “22,514” consistent gets and “2,580” physical reads. That’s 98% reduction in LIO’s and 99%+ reduction in PIO.
&lt;p&gt;&lt;/p&gt;
Again Analytics Functions is the winner over traditional query writing style.
&lt;p&gt;&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/34560741-7163198005738092926?l=momendba.blogspot.com'/&gt;&lt;/div&gt;</description><link>http://momendba.blogspot.com/2008/11/analytic-funcions-savior.html</link><author>noreply@blogger.com (Asif Momen)</author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">5</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-34560741.post-2000571382865153413</guid><pubDate>Mon, 03 Nov 2008 09:49:00 +0000</pubDate><atom:updated>2008-11-03T13:43:16.074+03:00</atom:updated><title>How to read Data between different Characterset Databases - Part 2</title><description>&lt;p&gt;&lt;/p&gt;
Hazem Ameen has left a comment on my previous &lt;a href="http://momendba.blogspot.com/2008/10/how-to-read-data-between-different.html"&gt;post&lt;/a&gt;, where he says he is able to read/write data over a database link between two databases having different charactersets.
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;

&lt;span style="font-style:italic;"&gt;We have your same environment and reading/writing over db link is fine. Oracle does character-set conversion between the 2 Arabic character-set as expected.

Client NLS_LANG: America_America.ar8mswin1256
Server1: ar8mswin1256
Server 2: ar8iso8859p6
I can only suspect your client NLS_LANG

Our version is 10.2.0.4 on Linux. Client is MS Windows Vista 10.2.0.4&lt;/span&gt;


&lt;p&gt;&lt;/p&gt;



&lt;p&gt;&lt;/p&gt;
&lt;span style="font-style:italic;"&gt;reading/writing over db link is fine&lt;/span&gt;
&lt;p&gt;&lt;/p&gt;


What I would assert from this is, although you have databases with two different charactersets, your client characterset is SAME. This is the reason you are able to read/write over the db link. But, in our case, clients connecting to database A have same characterset as A, while clients connecting to database B have same characterset as B.

Here's a test case with my client characterset set to "AR8MSWIN1256".

&lt;p&gt;&lt;/p&gt;
Create tables in both the databases:
&lt;p&gt;&lt;/p&gt;

&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://2.bp.blogspot.com/_gZsupYKwzXg/SQ7VVWAA8hI/AAAAAAAAAD0/4nFUvD2Jll4/s1600-h/feed_data.JPG"&gt;&lt;img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 320px; height: 228px;" src="http://2.bp.blogspot.com/_gZsupYKwzXg/SQ7VVWAA8hI/AAAAAAAAAD0/4nFUvD2Jll4/s320/feed_data.JPG" border="0" alt=""id="BLOGGER_PHOTO_ID_5264379577102299666" /&gt;&lt;/a&gt;


Create database links in both databases and query data from other side:
&lt;p&gt;&lt;/p&gt;


&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://3.bp.blogspot.com/_gZsupYKwzXg/SQ7VfSfNlwI/AAAAAAAAAD8/WFB1mEngOL8/s1600-h/query.JPG"&gt;&lt;img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 320px; height: 228px;" src="http://3.bp.blogspot.com/_gZsupYKwzXg/SQ7VfSfNlwI/AAAAAAAAAD8/WFB1mEngOL8/s320/query.JPG" border="0" alt=""id="BLOGGER_PHOTO_ID_5264379747958101762" /&gt;&lt;/a&gt;


In the above example, we were able to read data over db links meaning client characterset's are playing the vital role. 
&lt;p&gt;&lt;/p&gt;
This means, all your client's characterset is set to "AR8MSWIN1256" for both the types of databases. This is the reason you are able to read/write over a db link.
&lt;p&gt;&lt;/p&gt;
I hope I was clear in my examples. Thanks for leaving your comment.

&lt;p&gt;&lt;/p&gt;

Regards
&lt;p&gt;&lt;/p&gt;
Regards
Asif Momen&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/34560741-2000571382865153413?l=momendba.blogspot.com'/&gt;&lt;/div&gt;</description><link>http://momendba.blogspot.com/2008/11/how-to-read-data-between-different.html</link><author>noreply@blogger.com (Asif Momen)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://2.bp.blogspot.com/_gZsupYKwzXg/SQ7VVWAA8hI/AAAAAAAAAD0/4nFUvD2Jll4/s72-c/feed_data.JPG" height="72" width="72" /><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-34560741.post-3244598305899006708</guid><pubDate>Tue, 14 Oct 2008 11:48:00 +0000</pubDate><atom:updated>2008-10-15T14:03:13.718+03:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">Characterset</category><category domain="http://www.blogger.com/atom/ns#">Database</category><title>How to read Data between different Characterset Databases</title><description>We have two 24x7 production databases with different charactersets. Yes, both charactersets are related to Arabic language. Database A’s characterset is AR8MSWIN1256 while database B’s characterset is AR8ISO8859P6.
&lt;p&gt;&lt;/p&gt;

We had a requirement wherein database A will read Arabic data from database B. If you create a database link and try to query data from database B it will appear to be garbage, as shown below:
&lt;p&gt;&lt;/p&gt;
&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://1.bp.blogspot.com/_gZsupYKwzXg/SPXMFU_4KeI/AAAAAAAAADc/GgUuR5Xl1VA/s1600-h/garbage.JPG"&gt;&lt;img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;" src="http://1.bp.blogspot.com/_gZsupYKwzXg/SPXMFU_4KeI/AAAAAAAAADc/GgUuR5Xl1VA/s320/garbage.JPG" border="0" alt=""id="BLOGGER_PHOTO_ID_5257332531932965346" /&gt;&lt;/a&gt;

&lt;p&gt;&lt;/p&gt;

whereas when you run the same query on database B, this is how the output looks:
&lt;p&gt;&lt;/p&gt;

&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://2.bp.blogspot.com/_gZsupYKwzXg/SPXMRmncxGI/AAAAAAAAADk/rqYWcN-CGM0/s1600-h/real.JPG"&gt;&lt;img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;" src="http://2.bp.blogspot.com/_gZsupYKwzXg/SPXMRmncxGI/AAAAAAAAADk/rqYWcN-CGM0/s320/real.JPG" border="0" alt=""id="BLOGGER_PHOTO_ID_5257332742820775010" /&gt;&lt;/a&gt;

&lt;p&gt;&lt;/p&gt;

Even you are unaware of Arabic language, you may visually compare and see the output is a garbage when queried from database A.
&lt;p&gt;&lt;/p&gt;

DUMP function comes handy under these situations. It returns internal representation of an expression. When used to against the “name” failed, I get this output. 
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;
SQL&gt; select dump(name) from test_iso@testdb;

DUMP(NAME)
--------------------------------------------------------------------
Typ=1 Len=13: 227,205,227,207,32,194,213,221,32,227,196,227,228

&lt;/pre&gt;

&lt;p&gt;&lt;/p&gt;

where, “Typ” refers to datatype, “Len” means the length of the string, and everything after “:” is comma separated ASCII values of all the characters in the “name” field.
&lt;p&gt;&lt;/p&gt;

I wrote a small procedure where convert all the comma separated ASCII values to character string.
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;
create or replace function read_name (p_id in number) return varchar2 is
  t_str varchar2(2000);
  l_name varchar2(2000);
  l_output varchar2(2000);
begin
  select 'select '||'chr('||replace(substr(dump(name, 1010), instr(dump(name, 1010), ':')+2), ',', ')|| chr(')||') from dual' 
    into t_str from test_iso@testdb
   where id = p_id;
 
--  dbms_output.put_line(t_str);
  execute immediate t_str into l_output;  
  return l_output;
end;
/
&lt;/pre&gt;

&lt;p&gt;&lt;/p&gt;
Now upon using this function, I am able to query remote table in a readable format.
&lt;p&gt;&lt;/p&gt;


Here’s the output:
&lt;p&gt;&lt;/p&gt;
&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://4.bp.blogspot.com/_gZsupYKwzXg/SPXMaxQ99SI/AAAAAAAAADs/n_5hDCTJ5o0/s1600-h/perfect.JPG"&gt;&lt;img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;" src="http://4.bp.blogspot.com/_gZsupYKwzXg/SPXMaxQ99SI/AAAAAAAAADs/n_5hDCTJ5o0/s320/perfect.JPG" border="0" alt=""id="BLOGGER_PHOTO_ID_5257332900298093858" /&gt;&lt;/a&gt;
&lt;p&gt;&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/34560741-3244598305899006708?l=momendba.blogspot.com'/&gt;&lt;/div&gt;</description><link>http://momendba.blogspot.com/2008/10/how-to-read-data-between-different.html</link><author>noreply@blogger.com (Asif Momen)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://1.bp.blogspot.com/_gZsupYKwzXg/SPXMFU_4KeI/AAAAAAAAADc/GgUuR5Xl1VA/s72-c/garbage.JPG" height="72" width="72" /><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">4</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-34560741.post-2992557868801450032</guid><pubDate>Sun, 21 Sep 2008 11:54:00 +0000</pubDate><atom:updated>2008-09-21T15:03:01.054+03:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">Database</category><category domain="http://www.blogger.com/atom/ns#">SQL</category><category domain="http://www.blogger.com/atom/ns#">Tuning</category><title>Analytic Functions: The most ignored ones</title><description>Dear Readers,
&lt;p&gt;&lt;/p&gt;
Analytic Functions were first introduced in Oracle 8i, way back in 1999. Tom Kyte’s book “Expert One-on-One Oracle” has a dedicated chapter on this topic. Not only Tom’s book, any book on SQL (I have seen so far) has a separate chapter written on Analytic Functions. Yet, the developer community seems to be paying least attention in trying to understand and use them. I think before educating students, tutors should be trained to emphasize the importance of Analytic Functions in day to day life of a developer. 
&lt;p&gt;&lt;/p&gt;
Anyways, here’s a similar case on one of our very busy OLTP database. 
&lt;p&gt;&lt;/p&gt;
This SQL statement topped in the AWR report consuming nearly 84% of database time and was executed more than 1200 times during a 45-minutes AWR report.
&lt;p&gt;&lt;/p&gt;
A query is required to fetch TECH_PAC_ID for a customer along with the number of records for that customer.
&lt;p&gt;&lt;/p&gt;
Here’s the original query, its Explain Plan and Statistics:
&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt;
SQL&gt; set autotrace on
SQL&gt; SELECT Tech_PAC_ID, Users_USER_CD, TOT_PAC 
  2   FROM (SELECT Tech_PAC_ID, Users_USER_CD 
  3          FROM Tech, Users 
  4         WHERE Tech_STS = 'Y' AND Users_USER_CD = UPPER('ABX65842' )
  5           AND Users_USER_CD = Tech_ETI_USER_CD) DET, 
  6        (SELECT COUNT(*) TOT_PAC 
  7           FROM Tech, Users 
  8          WHERE Tech_STS = 'Y' AND Users_USER_CD = UPPER('ABX65842' ) 
  9            AND Users_USER_CD = Tech_ETI_USER_CD) TOT;

TECH_PAC_N USERS_USER_CD        TOT_PAC
---------- -------------------- ----------
236XXX123  ABX65842                     5

Elapsed: 00:00:00.06

Execution Plan
----------------------------------------------------------
Plan hash value: 3349818188

------------------------------------------------------------------------------------------------
| Id  | Operation                       | Name         | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                |              |     1 |    54 |  1280   (1)| 00:00:16 |
|   1 |  MERGE JOIN CARTESIAN           |              |     1 |    54 |  1280   (1)| 00:00:16 |
|*  2 |   TABLE ACCESS BY INDEX ROWID   | TECH         |     1 |    18 |     4   (0)| 00:00:01 |
|   3 |    NESTED LOOPS                 |              |     1 |    41 |   640   (1)| 00:00:08 |
|*  4 |     TABLE ACCESS FULL           | USERS        |     1 |    23 |   636   (1)| 00:00:08 |
|*  5 |     INDEX RANGE SCAN            | TECH_USER_CD |     3 |       |     1   (0)| 00:00:01 |
|   6 |   BUFFER SORT                   |              |     1 |    13 |  1276   (1)| 00:00:16 |
|   7 |    VIEW                         |              |     1 |    13 |   640   (1)| 00:00:08 |
|   8 |     SORT AGGREGATE              |              |     1 |   104 |            |          |
|*  9 |      TABLE ACCESS BY INDEX ROWID| TECH         |     1 |    12 |     4   (0)| 00:00:01 |
|  10 |       NESTED LOOPS              |              |     1 |   104 |   640   (1)| 00:00:08 |
|* 11 |        TABLE ACCESS FULL        | USERS        |     1 |    92 |   636   (1)| 00:00:08 |
|* 12 |        INDEX RANGE SCAN         | TECH_USER_CD |     3 |       |     1   (0)| 00:00:01 |
------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter("TECH_STS"='Y')
   4 - filter("USERS_USER_CD"='ABX65842')
   5 - access("USERS_USER_CD"="Tech_ETI_USER_CD")
       filter("TECH_ETI_USER_CD" IS NOT NULL)
   9 - filter("TECH_STS"='Y')
  11 - filter("USERS_USER_CD"='ABX65842')
  12 - access("USERS_USER_CD"="Tech_ETI_USER_CD")
       filter("TECH_ETI_USER_CD" IS NOT NULL)


Statistics
----------------------------------------------------------
          0  recursive calls
          0  db block gets
       4671  consistent gets
          0  physical reads
          0  redo size
        558  bytes sent via SQL*Net to client
        381  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
          1  rows processed

SQL&gt;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
Instead of writing two queries to fetch TECH_PAC_ID and COUNT(USERS_USER_CD) we can achieve the same result by using Analytic Functions. The new query not only returns the required result but is also less resource intensive and more database-friendly. 
&lt;p&gt;&lt;/p&gt;
Here goes the enhanced query using Analytic Function:
&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt;
SQL&gt; SELECT Tech_PAC_ID, Users_USER_CD, 
  2         count(1) over (partition by Users_USER_CD) TOT_PAC
  3          FROM Tech, Users 
  4         WHERE Tech_STS = 'Y' AND Users_USER_CD = UPPER('ABX65842' )
  5           AND Users_USER_CD = Tech_ETI_USER_CD
  6  GROUP BY Tech_PAC_ID, Users_USER_CD;

TECH_PAC_N USERS_USER_CD        TOT_PAC
---------- -------------------- ----------
236XXX123  ABX65842                     5

Elapsed: 00:00:00.04

Execution Plan
----------------------------------------------------------
Plan hash value: 1328229640

----------------------------------------------------------------------------------------------
| Id  | Operation                     | Name         | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |              |     1 |    41 |   641   (1)| 00:00:08 |
|   1 |  WINDOW BUFFER                |              |     1 |    41 |   641   (1)| 00:00:08 |
|   2 |   SORT GROUP BY               |              |     1 |    41 |   641   (1)| 00:00:08 |
|*  3 |    TABLE ACCESS BY INDEX ROWID| TECH         |     1 |    18 |     4   (0)| 00:00:01 |
|   4 |     NESTED LOOPS              |              |     1 |    41 |   640   (1)| 00:00:08 |
|*  5 |      TABLE ACCESS FULL        | USERS        |     1 |    23 |   636   (1)| 00:00:08 |
|*  6 |      INDEX RANGE SCAN         | TECH_USER_CD |     3 |       |     1   (0)| 00:00:01 |
----------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   3 - filter("TECH_STS"='Y')
   5 - filter("USERS_USER_CD"='ABX65842')
   6 - access("USERS_USER_CD"="Tech_ETI_USER_CD")
       filter("TECH_ETI_USER_CD" IS NOT NULL)


Statistics
----------------------------------------------------------
          0  recursive calls
          0  db block gets
       2335  consistent gets
          0  physical reads
          0  redo size
        558  bytes sent via SQL*Net to client
        381  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          2  sorts (memory)
          0  sorts (disk)
          1  rows processed

SQL&gt; 
SQL&gt;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
The new query seems to be doing a good job. The “consistent gets” have dropped from 4671 to 2335. That’s nearly half of the original, the reason being: instead of hitting the table twice, we are getting the work done in one hit. But still, the consistent gets seems to be reasonably high. 
&lt;p&gt;&lt;/p&gt;
Although ROWS=1 is being shown in the Explain Plan for USERS table, but optimizer is spending most of its time doing a Full Table Scan at this step. Adding an index on USERS_USER_CD column of USERS table should do the trick.
&lt;p&gt;&lt;/p&gt;
After adding the index on USERS (USERS_USER_CD) column, the query seems to be flying. 
&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt;
SQL&gt; SELECT Tech_PAC_ID, Users_USER_CD, 
  2         count(1) over (partition by Users_USER_CD) TOT_PAC
  3          FROM Tech, Users 
  4         WHERE Tech_STS = 'Y' AND Users_USER_CD = UPPER('ABX65842' )
  5           AND Users_USER_CD = Tech_ETI_USER_CD
  6  GROUP BY Tech_PAC_ID, Users_USER_CD;

TECH_PAC_N USERS_USER_CD        TOT_PAC
---------- -------------------- ----------
236XXX123  ABX65842                     5


Execution Plan
----------------------------------------------------------
Plan hash value: 1753916289

---------------------------------------------------------------------------------------------------------
| Id  | Operation                       | Name                  | Rows  | Bytes | Cost (%CPU)| Time  |
---------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                |                       |     1 |    33 |     7  (15)| 00:00:01 |
|   1 |  WINDOW BUFFER                  |                       |     1 |    33 |     7  (15)| 00:00:01 |
|   2 |   SORT GROUP BY                 |                       |     1 |    33 |     7  (15)| 00:00:01 |
|*  3 |    TABLE ACCESS BY INDEX ROWID  | TECH                  |     1 |    15 |     4   (0)| 00:00:01 |
|   4 |     NESTED LOOPS                |                       |     1 |    33 |     6   (0)| 00:00:01 |
|   5 |      TABLE ACCESS BY INDEX ROWID| USERS                 |     1 |    18 |     2   (0)| 00:00:01 |
|*  6 |       INDEX RANGE SCAN          | USERS_MUB_USER_CD_IDX |     1 |       |     1   (0)| 00:00:01 |
|*  7 |      INDEX RANGE SCAN           | TECH_USER_CD          |     3 |       |     1   (0)| 00:00:01 |
---------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   3 - filter("TECH_STS"='Y')
   6 - filter("USERS_USER_CD"='ABX65842')
   7 - access("USERS_USER_CD"="Tech_ETI_USER_CD")
       filter("TECH_ETI_USER_CD" IS NOT NULL)


Statistics
----------------------------------------------------------
          1  recursive calls
          0  db block gets
          6  consistent gets
          1  physical reads
          0  redo size
        558  bytes sent via SQL*Net to client
        399  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          2  sorts (memory)
          0  sorts (disk)
          1  rows processed

SQL&gt;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
This time merely 6 “consistent gets” were required. It’s a 99%+ reduction in overall consistent gets.
&lt;p&gt;&lt;/p&gt;
As I mentioned earlier in the post, I see SQL statements similar to this one over and over again. Analytic function is a nice alternative to the traditional way of writing these types of queries that works extremely well and provides the performance needed for high numbers of executions in a high data volume environment.
&lt;p&gt;&lt;/p&gt;
More information on Analytic Functions can (should) be obtained from here:
&lt;p&gt;&lt;/p&gt;
&lt;a href=” http://68.142.116.68/docs/cd/B19306_01/server.102/b14200/functions001.htm#i81407”&gt;Oracle Database SQL Reference 10g Release 2&lt;/a&gt;
&lt;p&gt;&lt;/p&gt;
&lt;a href=”http://www.oracle.com/technology/oramag/oracle/07-jan/o17asktom.html”&gt; On Top-n and Pagination Queries&lt;/a&gt; by Tom Kyte
&lt;p&gt;&lt;/p&gt;
&lt;a href=”http://asktom.oracle.com/pls/asktom/f?p=100:1:4461238362686089::NO:RP::”&gt; AskTom &lt;/a&gt;
&lt;p&gt;&lt;/p&gt;
&lt;a href=” http://www.quest-pipelines.com/newsletter-v7/0706_B.htm”&gt;One Analytic Function Can do More Than a 1000 Lines of Code&lt;/a&gt; by Alex Nuitjen

&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
Happy reading.&lt;blockquote&gt;&lt;/blockquote&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/34560741-2992557868801450032?l=momendba.blogspot.com'/&gt;&lt;/div&gt;</description><link>http://momendba.blogspot.com/2008/09/dear-readers-analytic-functions-most.html</link><author>noreply@blogger.com (Asif Momen)</author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">2</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-34560741.post-1995698843704977363</guid><pubDate>Sat, 20 Sep 2008 08:52:00 +0000</pubDate><atom:updated>2008-09-20T15:44:58.779+03:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">Syntax Highlighter</category><title>Syntax Highlighter</title><description>Dear Readers,
&lt;p&gt;&lt;/p&gt;
Most of you might have experienced difficulies in placing code snippets in a nice formatted way on Blogger.com. I have been previously using &lt;a href="http://formatmysourcecode.blogspot.com/"&gt;Greg Houston's&lt;/a&gt; Code formatter, but when you have pipes ("|") in the code like in Explain Plan, then Blogger fails to display the code correctly.
&lt;p&gt;&lt;/p&gt;

But, recently I stumbled on one of the &lt;a href="http://karenmorton.blogspot.com/search?updated-max=2008-08-15T10%3A00%3A00-07%3A00&amp;amp;max-results=7"&gt;Karen Morton's&lt;/a&gt; post where she discusses a way to format your code snippets on blogger. &lt;a href="http://code.google.com/p/syntaxhighlighter/wiki/Usage/"&gt;Here&lt;/a&gt; are more details on highlightling your code in blogger.com using &lt;a href="http://code.google.com/p/syntaxhighlighter/wiki/Usage/"&gt;Syntax Highlighter&lt;/a&gt;.

&lt;p&gt;&lt;/p&gt;
The code below has been formatted using SyntaxHighlighter:
&lt;p&gt;&lt;/p&gt;

&lt;pre class="sql" name="code"&gt;

SQL&gt; conn scott/tiger
Connected.
SQL&gt; select * from tab;

TNAME                          TABTYPE  CLUSTERID
------------------------------ ------- ----------
DEPT                           TABLE
EMP                            TABLE
BONUS                          TABLE
SALGRADE                       TABLE
TEST_RUN                       TABLE
PLAN_TABLE                     TABLE
EMP_VIEW                       VIEW
EMP2                           TABLE

8 rows selected.

SQL&gt;

&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
More interesting stuff can by found on &lt;a href="http://fahdshariff.blogspot.com/2008/07/syntax-highlighting-code-in-webpages.html"&gt;Fahd Shariff's&lt;/a&gt; blog and &lt;a href="http://morten.lyhr.dk/2007/12/how-to-get-syntax-highlighting-in.html"&gt;Morten Lyhr's&lt;/a&gt; blog.

&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;

Happy formating !!!
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/34560741-1995698843704977363?l=momendba.blogspot.com'/&gt;&lt;/div&gt;</description><link>http://momendba.blogspot.com/2008/09/syntax-highlighter.html</link><author>noreply@blogger.com (Asif Momen)</author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-34560741.post-6849917450425876676</guid><pubDate>Sat, 02 Aug 2008 11:54:00 +0000</pubDate><atom:updated>2008-10-08T20:38:45.453+03:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">Database</category><title>Dynamic Sampling and Table Partitions</title><description>Dynamic Sampling was first introduced in Oracle 9i Release 2 and the purpose is to improve query performance by determining more accurate estimates for predicate selectivity and statistics for tables and indexes.
&lt;p&gt;&lt;/p&gt;
Dynamic sampling is to be used predominantly with un-analyzed tables. Before hard parsing a query Oracle determines if dynamic sampling would improve query performance. If so, then the optimizer issues recursive SQL statements to estimate the necessary statistics dynamically. 
&lt;p&gt;&lt;/p&gt;
Here’s an example of dynamic sampling:
&lt;p&gt;&lt;/p&gt;
Operating System: Windows XP
Database:  Oracle 10g Release 2 (10.2.0.4)
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;  
SQL&gt; create table t (a number);

Table created.

SQL&gt; set autotrace traceonly exp
SQL&gt; select * from t;

Execution Plan
----------------------------------------------------------
Plan hash value: 1601196873

--------------------------------------------------------------------------
| Id  | Operation         | Name | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |      |     1 |    13 |     2   (0)| 00:00:01 |
|   1 |  TABLE ACCESS FULL| T    |     1 |    13 |     2   (0)| 00:00:01 |
--------------------------------------------------------------------------

Note
-----
   - dynamic sampling used for this statement

SQL&gt; set autotrace off
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
Its clear from the above Execution Plan that dynamic sampling was performed.
&lt;p&gt;&lt;/p&gt;

But it seems dynamic sampling is not partition friendly or rather dynamic sampling is partition unaware. What I mean is if you have a partitioned table and at least one partition is analyzed then dynamic sampling is NOT performed on any of the un-analyzed partitions. This is because Optimizer looks at columns like NUM_ROWS of DBA_TABLES view and considers that the table is analyzed. 
&lt;p&gt;&lt;/p&gt;
Let us create a partitioned table with a local index.
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;  
SQL&gt; create table part(
  2         sno     number,
  3         dt      date,
  4              remark     varchar2(30))
  5      partition by range(dt) (
  6      partition p1 values less than (to_date('01-02-2008', 'dd-mm-yyyy')),
  7      partition p2 values less than (to_date('01-03-2008', 'dd-mm-yyyy')),
  8      partition p3 values less than (to_date('01-04-2008', 'dd-mm-yyyy')),
  9      partition p4 values less than (to_date('01-05-2008', 'dd-mm-yyyy'))
 10     );

Table created.

SQL&gt;
SQL&gt; create index part_idx on part(dt, sno) local;

Index created.

SQL&gt;
&lt;/pre&gt;

&lt;p&gt;&lt;/p&gt;
Now, populate all but the last partition with test data.
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;
SQL&gt; insert into part
  2      select level,
  3             to_date('01-01-2008', 'dd-mm-yyyy') + level - 1 ,
  4             'test'
  5        from dual
  6      connect by level &lt;=90;

90 rows created.

SQL&gt;
SQL&gt; commit;

Commit complete.

SQL&gt;
&lt;/pre&gt;

&lt;p&gt;&lt;/p&gt;
Let us now collect  statistics for partitions P1, P2, and P3.
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;
SQL&gt; exec dbms_stats.gather_table_stats(user, tabname =&gt; 'part', -
&gt;       partname =&gt; 'p1', estimate_percent =&gt; null, -
&gt;       method_opt =&gt; 'for all columns size 1', cascade =&gt; true);

PL/SQL procedure successfully completed.

SQL&gt;
SQL&gt;
SQL&gt; exec dbms_stats.gather_table_stats(user, tabname =&gt; 'part', -
&gt;       partname =&gt; 'p2', estimate_percent =&gt; null, -
&gt;       method_opt =&gt; 'for all columns size 1', cascade =&gt; true);

PL/SQL procedure successfully completed.

SQL&gt;
SQL&gt;
SQL&gt; exec dbms_stats.gather_table_stats(user, tabname =&gt; 'part', -
&gt;       partname =&gt; 'p3', estimate_percent =&gt; null, -
&gt;       method_opt =&gt; 'for all columns size 1', cascade =&gt; true);

PL/SQL procedure successfully completed.

SQL&gt;
SQL&gt; select table_name, num_rows
  2    from user_tables
  3   where table_name ='PART';

TABLE_NAME                       NUM_ROWS
------------------------------ ----------
PART                                   90

SQL&gt;
SQL&gt;
SQL&gt; select partition_name, num_rows
  2    from user_tab_partitions
  3   where table_name = 'PART';

PARTITION_NAME                   NUM_ROWS
------------------------------ ----------
P1                                     31
P2                                     29
P3                                     30
P4

SQL&gt;
SQL&gt;
SQL&gt; select partition_name, num_rows
  2    from user_ind_partitions
  3   where index_name = 'PART_IDX';

PARTITION_NAME                   NUM_ROWS
------------------------------ ----------
P1                                     31
P2                                     29
P3                                     30
P4

SQL&gt;
&lt;/pre&gt;

&lt;p&gt;&lt;/p&gt;
It’s time to test whether optimizer considers dynamic sampling or not.
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;
SQL&gt; set autotrace traceonly exp
SQL&gt;
SQL&gt; select *
  2    from part
  3   where dt = to_date('01-04-2008', 'dd-mm-yyyy')
  4     and sno = 1;

Execution Plan
----------------------------------------------------------
Plan hash value: 2633488982

-----------------------------------------------------------------------------------------------
| Id  | Operation              | Name | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
-----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT       |      |     1 |    16 |     2   (0)| 00:00:01 |       |       |
|   1 |  PARTITION RANGE SINGLE|      |     1 |    16 |     2   (0)| 00:00:01 |     4 |     4 |
|*  2 |   TABLE ACCESS FULL    | PART |     1 |    16 |     2   (0)| 00:00:01 |     4 |     4 |
-----------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter("DT"=TO_DATE(' 2008-04-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss') AND
              "SNO"=1)

SQL&gt;
SQL&gt; set autotrace off
SQL&gt;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;

No! Optimizer thinks that it has enough information available at hand to with it rather than sampling dynamically.
&lt;p&gt;&lt;/p&gt;
Now, let us populate the unanalyzed partition with data and re-run the same query.
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;
SQL&gt; insert into part select 1, to_date('01-04-2008', 'dd-mm-yyyy'), 'test'
  2        from dual connect by level &lt;=1000;

1000 rows created.

SQL&gt;
SQL&gt; commit;

Commit complete.

SQL&gt;
SQL&gt; set autotrace traceonly exp
SQL&gt;
SQL&gt;
SQL&gt; select * from part
  2   where dt = to_date('01-04-2008', 'dd-mm-yyyy')
  3     and sno = 1;

Execution Plan
----------------------------------------------------------
Plan hash value: 2633488982

-----------------------------------------------------------------------------------------------
| Id  | Operation              | Name | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
-----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT       |      |     1 |    16 |     2   (0)| 00:00:01 |       |       |
|   1 |  PARTITION RANGE SINGLE|      |     1 |    16 |     2   (0)| 00:00:01 |     4 |     4 |
|*  2 |   TABLE ACCESS FULL    | PART |     1 |    16 |     2   (0)| 00:00:01 |     4 |     4 |
-----------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter("DT"=TO_DATE(' 2008-04-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss') AND
              "SNO"=1)

SQL&gt;
SQL&gt; set autotrace off
&lt;/pre&gt;

&lt;p&gt;&lt;/p&gt;
Well, the optimizer took the same plan because statistics were not up to date. Consider gathering statistics on the partition in concern and execute the same query again.
&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt; 
SQL&gt; exec dbms_stats.gather_table_stats(user, tabname =&gt; 'part', -
&gt;       partname =&gt; 'p4', estimate_percent =&gt; null, -
&gt;       method_opt =&gt; 'for all columns size 1', cascade =&gt; true);

PL/SQL procedure successfully completed.

SQL&gt;
SQL&gt; set autotrace traceonly exp
SQL&gt;
SQL&gt;
SQL&gt; select * from part
  2   where dt = to_date('01-04-2008', 'dd-mm-yyyy')
  3     and sno = 1;

Execution Plan
----------------------------------------------------------
Plan hash value: 2633488982

-----------------------------------------------------------------------------------------------
| Id  | Operation              | Name | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
-----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT       |      |  1000 | 16000 |     3   (0)| 00:00:01 |       |       |
|   1 |  PARTITION RANGE SINGLE|      |  1000 | 16000 |     3   (0)| 00:00:01 |     4 |     4 |
|*  2 |   TABLE ACCESS FULL    | PART |  1000 | 16000 |     3   (0)| 00:00:01 |     4 |     4 |
-----------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter("DT"=TO_DATE(' 2008-04-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss') AND
              "SNO"=1)

SQL&gt;
SQL&gt; set autotrace off
SQL&gt;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
Well this time optimizer had updated statistics which gives a better and clear picture to pick the most appropriate access path. So, optimizer thinks “TABLE ACCESS FULL” on partition P4 is appropriate and which makes sense too.
&lt;p&gt;&lt;/p&gt;
I have purposely populated partition P4 with duplicate data so that we see a difference in optimizer access path.
&lt;p&gt;&lt;/p&gt;
Finally, the conclusions are:
&lt;p&gt;&lt;/p&gt;
• Dynamic sampling ignores partition level statistical information.
• Always update statistics after data load process, or assign default statistics to unanalyzed partition.

Happy reading.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/34560741-6849917450425876676?l=momendba.blogspot.com'/&gt;&lt;/div&gt;</description><link>http://momendba.blogspot.com/2008/08/dynamic-sampling-and-table-partitions.html</link><author>noreply@blogger.com (Asif Momen)</author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-34560741.post-3497809747740780254</guid><pubDate>Fri, 01 Aug 2008 16:32:00 +0000</pubDate><atom:updated>2008-10-08T20:21:23.188+03:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">Database</category><title>Bugs in REMAINDER Function (Updated)</title><description>Dear all,
&lt;p&gt;&lt;/p&gt;
Regarding &lt;a href="http://momendba.blogspot.com/2008/07/bugs-in-remainder-function.html"&gt;Bugs in REMAINDER Function&lt;/a&gt;, I have opened a call with Oracle Support and they say "REMAINDER function works as intended and this is a documentation issue which requires minor clarification".
&lt;p&gt;&lt;/p&gt;
Oracle documentation has modified the text as follows:
&lt;p&gt;&lt;/p&gt;
"If n1 != 0, then the remainder is n2 - (n1*N) where N is the integer nearest n2/n1. If n2/n1 equals x.5, then N is the nearest even integer."
&lt;p&gt;&lt;/p&gt;

A bug (&lt;a href="https://metalink.oracle.com/metalink/plsql/f?p=130:15:2208519251026950010::::p15_database_id,p15_docid,p15_show_header,p15_show_help,p15_black_frame,p15_font:BUG,7236077,1,1,1,helvetica"&gt;7236077&lt;/a&gt;) has also been logged.
&lt;p&gt;&lt;/p&gt;

Regards&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/34560741-3497809747740780254?l=momendba.blogspot.com'/&gt;&lt;/div&gt;</description><link>http://momendba.blogspot.com/2008/08/bugs-in-remainder-function-updated.html</link><author>noreply@blogger.com (Asif Momen)</author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-34560741.post-8040090147979816480</guid><pubDate>Sun, 27 Jul 2008 10:33:00 +0000</pubDate><atom:updated>2008-10-08T20:24:37.439+03:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">Database</category><title>Funny “ORA-00942: table or view does not exist” Error Message</title><description>Dear all,
&lt;p&gt;&lt;/p&gt;
We all know when “ORA-00942” error message is returned. For instance, if I try to query a non-existent table then Oracle joyfully returns this errors message.
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;  
SQL&gt; select * from funny;
select * from funny
              *
ERROR at line 1:
ORA-00942: table or view does not exist
&lt;/pre&gt;
&lt;p&gt; &lt;/p&gt;
“ORA-00942” error message is thrown on our face whenever we try to perform any action like CREATE, ALTER, DROP, INSERT, UPDATE, DELETE ... against a non-existent table.

But this error is sometimes misleading, like the one below:
&lt;p&gt; &lt;/p&gt;

&lt;pre name="code" class="sql"&gt;  
SQL&gt; create table t(a number);

Table created.

SQL&gt; create view v as select * from t;

View created.

SQL&gt; drop table v;
drop table v
           *
ERROR at line 1:
ORA-00942: table or view does not exist
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
Error message reports “table or view does not exist”, but definitely a view named “V” exists in the same schema.
&lt;p&gt;&lt;/p&gt;
Wouldn’t it be more appropriate if Oracle reports “table does not exist”?&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/34560741-8040090147979816480?l=momendba.blogspot.com'/&gt;&lt;/div&gt;</description><link>http://momendba.blogspot.com/2008/07/funny-ora-00942-table-or-view-does-not.html</link><author>noreply@blogger.com (Asif Momen)</author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">2</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-34560741.post-1122358114911993749</guid><pubDate>Thu, 10 Jul 2008 11:01:00 +0000</pubDate><atom:updated>2008-10-08T23:23:11.661+03:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">Database</category><category domain="http://www.blogger.com/atom/ns#">SQL</category><category domain="http://www.blogger.com/atom/ns#">Tuning</category><title>MIN and MAX Functions in a Single Query are Disastrous</title><description>Dear Readers,
&lt;p&gt;&lt;/p&gt;

I would like to discuss a very interesting point about indexes in this post. When we are interested in finding out the minimum value of an indexed column, instead of reading entire table or the index, Oracle intelligently uses the index to navigate to the first index leaf block (leftmost index block) and quickly finds the minimum value of an indexed column.
&lt;p&gt;&lt;/p&gt;
A simple demo proves this: &lt;/span&gt;
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;
SQL&gt; select * from v$version;

BANNER
----------------------------------------------------------------
Oracle Database 10g Enterprise Edition Release 10.2.0.4.0 - Prod
PL/SQL Release 10.2.0.4.0 - Production
CORE    10.2.0.4.0      Production
TNS for 32-bit Windows: Version 10.2.0.4.0 - Production
NLSRTL Version 10.2.0.4.0 - Production

SQL&gt;
SQL&gt; drop table t purge;

Table dropped.

SQL&gt;
SQL&gt;
SQL&gt; create table t as select level sno, 'name ' || level name 
  2  from dual connect by level &lt;= 10000000;

Table created.

SQL&gt;

SQL&gt; create unique index t_idx on t(sno);

Index created.

SQL&gt; exec dbms_stats.gather_table_stats(user, 't');

PL/SQL procedure successfully completed.

SQL&gt; set autotrace on
SQL&gt;
SQL&gt; select min(sno) from t;

  MIN(SNO)
----------
         1


Execution Plan
----------------------------------------------------------
Plan hash value: 2683064407

------------------------------------------------------------------------------------
| Id  | Operation                  | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT           |       |     1 |     6 |  9034   (2)| 00:01:49 |
|   1 |  SORT AGGREGATE            |       |     1 |     6 |            |          |
|   2 |   INDEX FULL SCAN (MIN/MAX)| T_IDX |    10M|    57M|            |          |
------------------------------------------------------------------------------------


Statistics
----------------------------------------------------------
          1  recursive calls
          0  db block gets
          3  consistent gets
          1  physical reads
          0  redo size
        411  bytes sent via SQL*Net to client
        399  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          1  rows processed

SQL&gt;
SQL&gt; set autotrace off
SQL&gt;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
Although this table contains 10 Million rows, Oracle required only 3 consistent gets to fetch the minimum value. Superb !!!
&lt;p&gt;&lt;/p&gt;
Similarly, when finding out the maximum value, Oracle reads the last block on the right-hand side of the index structure. 
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;
SQL&gt; set autotrace on
SQL&gt; select max(sno) from t;

  MAX(SNO)
----------
  10000000


Execution Plan
----------------------------------------------------------
Plan hash value: 2683064407

------------------------------------------------------------------------------------
| Id  | Operation                  | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT           |       |     1 |     6 |  9034   (2)| 00:01:49 |
|   1 |  SORT AGGREGATE            |       |     1 |     6 |            |          |
|   2 |   INDEX FULL SCAN (MIN/MAX)| T_IDX |    10M|    57M|            |          |
------------------------------------------------------------------------------------


Statistics
----------------------------------------------------------
          1  recursive calls
          0  db block gets
          3  consistent gets
          2  physical reads
          0  redo size
        411  bytes sent via SQL*Net to client
        399  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          1  rows processed

SQL&gt; set autotrace off
SQL&gt;
&lt;/pre&gt;

&lt;p&gt;&lt;/p&gt;
Once again only 3 consistent gets were required to fetch the maximum value.
&lt;p&gt;&lt;/p&gt;

But things get messy when you use both MIN and MAX functions in the same query. Instead of using same "INDEX FULL SCAN (MIN/MAX)" path to read the left-most block and right-most block to arrive at the minimum and maximum values, Oracle goes with FULL TABLE SCAN. A Full Table Scan on 10 Million rows !!!
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;
SQL&gt; set autotrace on
SQL&gt; select min(sno), max(sno) from t;

  MIN(SNO)   MAX(SNO)
---------- ----------
         1   10000000


Execution Plan
----------------------------------------------------------
Plan hash value: 2966233522

---------------------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |     1 |     6 |  9034   (2)| 00:01:49 |
|   1 |  SORT AGGREGATE    |      |     1 |     6 |            |          |
|   2 |   TABLE ACCESS FULL| T    |    10M|    57M|  9034   (2)| 00:01:49 |
---------------------------------------------------------------------------


Statistics
----------------------------------------------------------
          1  recursive calls
          0  db block gets
      32928  consistent gets
      11391  physical reads
          0  redo size
        472  bytes sent via SQL*Net to client
        399  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          1  rows processed

SQL&gt; set autotrace off
SQL&gt;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;


From this terrible behavior of Oracle Optimizer, What I infer and suggest is:
&lt;p&gt;&lt;/p&gt;
 "Write separate queries to fetch MIN and MAX values instead of combining them into one query".

&lt;p&gt;&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/34560741-1122358114911993749?l=momendba.blogspot.com'/&gt;&lt;/div&gt;</description><link>http://momendba.blogspot.com/2008/07/min-and-max-functions-in-single-query.html</link><author>noreply@blogger.com (Asif Momen)</author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">8</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-34560741.post-5630964454483742139</guid><pubDate>Sat, 05 Jul 2008 15:09:00 +0000</pubDate><atom:updated>2008-10-08T23:31:17.606+03:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">Database</category><category domain="http://www.blogger.com/atom/ns#">SQL</category><title>Bugs in REMAINDER Function</title><description>While browsing through &lt;a href="http://download.oracle.com/docs/cd/B19306_01/server.102/b14200/toc.htm"&gt;SQL Reference&lt;/a&gt; documentation (Oracle 10g Release 2), I stopped at &lt;a href="http://download.oracle.com/docs/cd/B19306_01/server.102/b14200/functions133.htm#i1300767"&gt;REMAINDER&lt;/a&gt; function. The REMAINDER returns the remainder of n2 divided by n1 and internally uses ROUND function to arrive at the remainder value.
&lt;p&gt;&lt;/p&gt;

Oracle documentation also says, "If n1 != 0, then the remainder is n2 - (n1*N) where N is the integer nearest n2/n1."

&lt;p&gt; &lt;/p&gt;

&lt;pre name="code" class="sql"&gt;
SQL&gt; Select REMAINDER(501, 5) from dual;

REMAINDER(501,5)
----------------
               1

SQL&gt;
&lt;/pre&gt;
&lt;p&gt; &lt;/p&gt;

But, because of an internal bug, REMAINDER function returns incorrect results when "n2/n1" results in 0.5, 1.5, 2.5, .....

&lt;pre name="code" class="sql"&gt;
SQL&gt; Select REMAINDER(5, 2) from dual;

REMAINDER(5,2)
--------------
             1

SQL&gt;
&lt;/pre&gt;

&lt;p&gt;&lt;/p&gt;
According to the formula,

&lt;pre name="code" class="sql"&gt;

n2 - (n1 * N) = 5 - (2 * ROUND(5/2)) = 5 - (2 * 3) = 5 - 6 = -1
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;

the value returned should be "-1" where as the REMAINDER function returns "+1". 

&lt;p&gt;&lt;/p&gt;

My observation is that, for example "1.5" is getting rounded to "1" instead of "2" and is spoiling the outcome of REMAINDER.

&lt;p&gt;&lt;/p&gt;
I wrote a small PL/SQL block to screen my observation:


&lt;pre name="code" class="sql"&gt;
SQL&gt; set linesize 1000
SQL&gt; set pages     100
SQL&gt; set serveroutput on
SQL&gt;
SQL&gt;    declare
  2       n1  number;
  3       n2  number;
  4       N   number;
  5
  6       sys_rem number;
  7       my_rem number;
  8     begin
  9       for n1 in 1..5 loop
 10        --for n2 in 1..50 loop
 11        n2 := 1;
 12        while n2 &lt;= 20 loop
 13          Sys_rem := REMAINDER(n2, n1);
 14          N := Round(n2/n1);
 15          My_rem := n2 - (n1 * N);
 16          If sys_rem &lt;&gt; my_rem then
 17            Dbms_output.put_line('n2 : ' || rpad(to_char(n2), 4, ' ') ||
 18                                 '  n1 : ' || to_char(n1) ||
 19                                 '   ' ||
 20                                 ' n2/n1 = ' || rpad(to_char(n2/n1), 4, ' ') ||
 21                                 '     ' ||
 22                                 ' ORCL rem = ' || rpad(to_char(sys_rem), 4, ' ') ||
 23                                 '     ' ||
 24                                 ' Calc Rem [' || rpad(to_char(n2), 4, ' ') ||
 25                                 ' - (' || to_char(n1) || '*' ||
 26                                 rpad(to_char(N), 4, ' ') || ')] = '||
 27                                 to_char(my_rem));
 28          End If;
 29          n2 := n2 + 0.1;
 30        end loop;
 31      end loop;
 32    end;
 33    /
n2 : 2.5   n1 : 1    n2/n1 = 2.5       ORCL rem = .5        Calc Rem [2.5  - (1*3   )] = -.5
n2 : 4.5   n1 : 1    n2/n1 = 4.5       ORCL rem = .5        Calc Rem [4.5  - (1*5   )] = -.5
n2 : 6.5   n1 : 1    n2/n1 = 6.5       ORCL rem = .5        Calc Rem [6.5  - (1*7   )] = -.5
n2 : 8.5   n1 : 1    n2/n1 = 8.5       ORCL rem = .5        Calc Rem [8.5  - (1*9   )] = -.5
n2 : 10.5  n1 : 1    n2/n1 = 10.5      ORCL rem = .5        Calc Rem [10.5 - (1*11  )] = -.5
n2 : 12.5  n1 : 1    n2/n1 = 12.5      ORCL rem = .5        Calc Rem [12.5 - (1*13  )] = -.5
n2 : 14.5  n1 : 1    n2/n1 = 14.5      ORCL rem = .5        Calc Rem [14.5 - (1*15  )] = -.5
n2 : 16.5  n1 : 1    n2/n1 = 16.5      ORCL rem = .5        Calc Rem [16.5 - (1*17  )] = -.5
n2 : 18.5  n1 : 1    n2/n1 = 18.5      ORCL rem = .5        Calc Rem [18.5 - (1*19  )] = -.5
n2 : 1     n1 : 2    n2/n1 = .5        ORCL rem = 1         Calc Rem [1    - (2*1   )] = -1
n2 : 5     n1 : 2    n2/n1 = 2.5       ORCL rem = 1         Calc Rem [5    - (2*3   )] = -1
n2 : 9     n1 : 2    n2/n1 = 4.5       ORCL rem = 1         Calc Rem [9    - (2*5   )] = -1
n2 : 13    n1 : 2    n2/n1 = 6.5       ORCL rem = 1         Calc Rem [13   - (2*7   )] = -1
n2 : 17    n1 : 2    n2/n1 = 8.5       ORCL rem = 1         Calc Rem [17   - (2*9   )] = -1
n2 : 1.5   n1 : 3    n2/n1 = .5        ORCL rem = 1.5       Calc Rem [1.5  - (3*1   )] = -1.5
n2 : 7.5   n1 : 3    n2/n1 = 2.5       ORCL rem = 1.5       Calc Rem [7.5  - (3*3   )] = -1.5
n2 : 13.5  n1 : 3    n2/n1 = 4.5       ORCL rem = 1.5       Calc Rem [13.5 - (3*5   )] = -1.5
n2 : 19.5  n1 : 3    n2/n1 = 6.5       ORCL rem = 1.5       Calc Rem [19.5 - (3*7   )] = -1.5
n2 : 2     n1 : 4    n2/n1 = .5        ORCL rem = 2         Calc Rem [2    - (4*1   )] = -2
n2 : 10    n1 : 4    n2/n1 = 2.5       ORCL rem = 2         Calc Rem [10   - (4*3   )] = -2
n2 : 18    n1 : 4    n2/n1 = 4.5       ORCL rem = 2         Calc Rem [18   - (4*5   )] = -2
n2 : 2.5   n1 : 5    n2/n1 = .5        ORCL rem = 2.5       Calc Rem [2.5  - (5*1   )] = -2.5
n2 : 12.5  n1 : 5    n2/n1 = 2.5       ORCL rem = 2.5       Calc Rem [12.5 - (5*3   )] = -2.5

PL/SQL procedure successfully completed.

SQL&gt;
&lt;/pre&gt;


&lt;p&gt;&lt;/p&gt;
I have tested this code on Oracle 10g Release 2. Unfortunately, this bug also exists in Oracle 11g.
&lt;p&gt;&lt;/p&gt;

I have raised a Service Request and also filed a bug with Oracle Support.
&lt;p&gt;&lt;/p&gt;

Regards
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/34560741-5630964454483742139?l=momendba.blogspot.com'/&gt;&lt;/div&gt;</description><link>http://momendba.blogspot.com/2008/07/bugs-in-remainder-function.html</link><author>noreply@blogger.com (Asif Momen)</author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-34560741.post-3888402146247696342</guid><pubDate>Sat, 28 Jun 2008 14:27:00 +0000</pubDate><atom:updated>2008-10-08T23:34:19.429+03:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">OTN</category><title>OTN - Discussion Forums in a great new look</title><description>Dear Readers,
&lt;p&gt;&lt;/p&gt;
Morning (GMT+3) when I tried to login to &lt;a href="http://forums.oracle.com/forums/category.jspa?categoryID=18"&gt;OTN-Discussion forum&lt;/a&gt;, it was down. But later when I checked it again, it's with a great new look.
&lt;p&gt;&lt;/p&gt;
With the new look comes new features. Following are the new features I have noticed:
&lt;p&gt;&lt;/p&gt;
* Great new look&lt;p&gt;&lt;/p&gt;
* Add smiley's to messages&lt;p&gt;&lt;/p&gt;
* Spell Check&lt;p&gt;&lt;/p&gt;
* Formatting&lt;p&gt;&lt;/p&gt;
* Reward points&lt;p&gt;&lt;/p&gt;
* Save messages&lt;p&gt;&lt;/p&gt;
* Tag messages and&lt;p&gt;&lt;/p&gt;
* Abuse alerts
&lt;p&gt;&lt;/p&gt;
It should be easier for the OTN-newbie's to post their questions in a well formatted and readable format.
&lt;p&gt;&lt;/p&gt;

Enjoy discussions !!!
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/34560741-3888402146247696342?l=momendba.blogspot.com'/&gt;&lt;/div&gt;</description><link>http://momendba.blogspot.com/2008/06/otn-discussion-forums-in-great-new-look.html</link><author>noreply@blogger.com (Asif Momen)</author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">1</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-34560741.post-492104618506225627</guid><pubDate>Tue, 24 Jun 2008 10:54:00 +0000</pubDate><atom:updated>2008-10-08T23:38:00.439+03:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">Database</category><category domain="http://www.blogger.com/atom/ns#">Tuning</category><title>Why my index is not used?</title><description>Well, this question keeps popping up now and then. Yesterday, one of my colleagues also came up with this question: "Why is it that Oracle is not using index even though I am selecting less than 10% of data?".
&lt;p&gt;&lt;/p&gt;

We ran the query with autotrace enabled and the execution plan showed a Full Table Scan. This table contains over 29 Million records and by adding the predicate, result set is reduced to 2.3 Million records, which is 8% of total records.
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;
SQL&gt; set autotrace traceonly exp
SQL&gt; SELECT *
  2      FROM quint_sec_tbl
  3      WHERE quint_type = 'XX06FR';

Execution Plan
----------------------------------------------------------
Plan hash value: 3917650069

----------------------------------------------------------------------------------------
| Id  | Operation         | Name               | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |                    |  3440 |   618K|   454K  (1)| 01:30:55 |
|*  1 |  TABLE ACCESS FULL| QUINT_SEC_TBL      |  3440 |   618K|   454K  (1)| 01:30:55 |
----------------------------------------------------------------------------------------
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;

&lt;a href="http://carymillsap.blogspot.com/ "&gt;Cary Millsap's&lt;/a&gt; very old (but still valid) article "&lt;a href="http://www.hotsos.com/e-library/abstract.php?id=5/"&gt;When to Use an Index&lt;/a&gt;" greatly helped me in this regard.  This article unveils that Index consideration is based on block selectivity and not on row selectivity. 
&lt;p&gt;&lt;/p&gt;

He also defines what Row Selectivity and Block Selectivity are in the article.
&lt;p&gt;&lt;/p&gt;

&lt;strong&gt;Row Selectivity:&lt;/strong&gt;
&lt;p&gt;&lt;/p&gt;
You can define the row selectivity of a given where-clause predicate as the number of rows returned by the predicate (r) divided by the total number of rows in the table (R):
&lt;p&gt;&lt;/p&gt;
        &lt;pre style="font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; color: #000000; background-color: #eee;font-size: 12px;border: 1px dashed #999999;line-height: 14px;padding: 5px; overflow: auto; width: 100%"&gt;&lt;code&gt;
        P(r) =  r / R

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;strong&gt;Block Selectivity:&lt;/strong&gt;
&lt;p&gt;&lt;/p&gt;
You can define the block selectivity of a given where-clause predicate analogously, as the number of data blocks containing at least one row matching the predicate condition (b) divided by the total number of data blocks below the high-water mark (B):
&lt;p&gt;&lt;/p&gt;
&lt;pre style="font-family: Andale Mono, Lucida Console, Monaco, fixed, monospace; color: #000000; background-color: #eee;font-size: 12px;border: 1px dashed #999999;line-height: 14px;padding: 5px; overflow: auto; width: 100%"&gt;&lt;code&gt;
        P(b) = b / B

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;

We can calculate block selectivity and row selectivity using SQL provided in this article. I used this SQL against my query and following are the results:
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;
SQL&gt; @hds
TableOwner : MYPRODUSER
TableName : QUINT_SEC_TBL
ColumnList : QUINT_TYPE
WhereClause: 
PageSize : 100

Table blocks below hwm    Table rows
         (B)                 (R)
---------------------- ----------------
             1,672,704       29,270,757
More: 

       Block selectivity  Block count    Row selectivity     Row count
QUINT_    (pb = b/B)          (b)          (pr = r/R)           (r)
------ ----------------- -------------- ----------------- ----------------
TT34DV            45.03%        753,277            37.99%       11,120,869
FG76SC            44.47%        743,788            13.67%        4,000,205
LH23Q2            42.78%        715,558             9.44%        2,762,284
XX06FR            42.32%        707,894             8.02%        2,346,846
:
:
:
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;

Output of this SQL is sorted in descending order of block selectivity. 
&lt;p&gt;&lt;/p&gt;

Looking at this output, row selectivity is only 8% but to fetch these 8% of rows Oracle has to visit 42% of blocks (block selectivity). That means, nearly half of the table's blocks contain at least one row for which QUINT_TYPE='XX06FR'. Instead of going through the hard path of Index Access, it’s more efficient for the optimizer to do a Full Table Scan.
&lt;p&gt;&lt;/p&gt;

So, now we know why the index was ignored and a Full Table Scan was preferred. 
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;

P.S.: Due to security reasons Username, Table name, column name and column values are modified.
&lt;p&gt;&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/34560741-492104618506225627?l=momendba.blogspot.com'/&gt;&lt;/div&gt;</description><link>http://momendba.blogspot.com/2008/06/why-my-index-is-not-used.html</link><author>noreply@blogger.com (Asif Momen)</author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">3</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-34560741.post-2767834284873228585</guid><pubDate>Wed, 18 Jun 2008 11:36:00 +0000</pubDate><atom:updated>2008-12-09T21:45:15.859+03:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">SQL</category><title>SQL for Loan Payment</title><description>Dear all,
&lt;p&gt;&lt;/p&gt;
Inspired by "&lt;a href="http://www.oraclenerd.com/2008/06/sql-for-buying-new-car.html"&gt;SQL for Buying a New Car&lt;/a&gt;", I thought of converting a very old excel sheet of mine into an SQL query. This excel sheet lists equated monthly installment (EMI), principal amount and the interest amount for duration of  loan payment. I feed Rate of Interest, Loan Amount and Period of Loan (in years) to the excel sheet. This was very convenient for me to analyze what part of my EMI is going towards interest payment and what portion goes as principal repayment. It is extremely important to understand what goes for interest and what goes for principal repayment when you are planning for a loan.
&lt;p&gt;&lt;/p&gt; 

Output from Excel sheet:
&lt;p&gt;&lt;/p&gt; 
&lt;p&gt;&lt;/p&gt; 

&lt;a href="http://4.bp.blogspot.com/_gZsupYKwzXg/SFj0LTsIzbI/AAAAAAAAACk/A4idtgmPqPA/s1600-h/LoanPayment_Excel.JPG"&gt;&lt;img style="cursor:pointer; cursor:hand;" src="http://4.bp.blogspot.com/_gZsupYKwzXg/SFj0LTsIzbI/AAAAAAAAACk/A4idtgmPqPA/s320/LoanPayment_Excel.JPG" border="0" alt=""id="BLOGGER_PHOTO_ID_5213185043782487474" /&gt;&lt;/a&gt;

&lt;p&gt;&lt;/p&gt; 
This can easily be converted into an SQL query using MODEL clause introduced in Oracle 10g.
&lt;p&gt;&lt;/p&gt; 
The output of this query includes a summary of total interest to be paid, principal to be paid and the net payment to be made. Ofcourse, we need to ignore the Summary on "Remaining" column as this is meaningless.
&lt;p&gt;&lt;/p&gt; 



&lt;pre name="code" class="sql"&gt;
SQL&gt; set line 10000
SQL&gt; set pages 500
SQL&gt; set verify off
SQL&gt;
SQL&gt; column interest  format 999,999,999.99
SQL&gt; column principal format 999,999,999.99
SQL&gt; column remaining format 999,999,999.99
SQL&gt;
SQL&gt; define roi = 10          -- Rate of Interest
SQL&gt; define years = 2         -- Payment Period
SQL&gt; define amt = 100000      -- Loan Amount
SQL&gt; define period = 12       -- Mode of payment.
SQL&gt;                          --     Monthly=12, Bi-monthly=48, Quarterly=4, Half-yearly=2, Yearly=1, Adhoc=n
SQL&gt;
SQL&gt;
SQL&gt; Select inst_no,
  2         sum(new_emi) emi,
  3         sum(new_interest) interest,
  4         sum(principal) principal,
  5         sum(remaining) remaining
  6    From ( Select inst_no + 1 inst_no,
  7                  new_emi,
  8                  interest,
  9                  decode( inst_no + 1, &amp;period*&amp;years, interest - remaining, interest)  new_interest,
 10                  decode( inst_no + 1, &amp;period*&amp;years, principal + remaining, principal)  principal,
 11                  decode( inst_no + 1, &amp;period*&amp;years, 0, remaining) remaining
 12             From  (select  rownum inst_no, roi, yrs, amt, period,
 13                            round( (roi * amt/(period * 100)) /
 14                                   (1 - power((1 + roi/(period * 100)), -yrs * period)), 2) emi
 15                      From (select &amp;roi roi, &amp;years yrs, &amp;amt amt, &amp;period period
 16                              From Dual) )
 17    MODEL
 18    DIMENSION BY ( inst_no )
 19    MEASURES     ( roi, yrs, amt, emi , period, 0 as new_emi, 0 as interest, 0 as principal, 0 as remaining)
 20    RULES
 21      UPSERT
 22      ITERATE (1000)
 23      (
 24        new_emi[ITERATION_NUMBER] = emi[1],
 25        interest[ITERATION_NUMBER]  = round(&amp;roi/(period[1] * 100) *
 26                                      nvl(remaining[ITERATION_NUMBER-1], amt[1]), 2),
 27        principal[ITERATION_NUMBER] = emi[1] - interest[cv()],
 28        remaining[ITERATION_NUMBER] = nvl(remaining[ITERATION_NUMBER-1], amt[1]) - principal[cv()]))
 29  Where interest &gt; 0
 30  Group by rollup( inst_no)
 31  order by  inst_no;

   INST_NO        EMI        INTEREST       PRINCIPAL       REMAINING
---------- ---------- --------------- --------------- ---------------
         1    4614.49          833.33        3,781.16       96,218.84
         2    4614.49          801.82        3,812.67       92,406.17
         3    4614.49          770.05        3,844.44       88,561.73
         4    4614.49          738.01        3,876.48       84,685.25
         5    4614.49          705.71        3,908.78       80,776.47
         6    4614.49          673.14        3,941.35       76,835.12
         7    4614.49          640.29        3,974.20       72,860.92
         8    4614.49          607.17        4,007.32       68,853.60
         9    4614.49          573.78        4,040.71       64,812.89
        10    4614.49          540.11        4,074.38       60,738.51
        11    4614.49          506.15        4,108.34       56,630.17
        12    4614.49          471.92        4,142.57       52,487.60
        13    4614.49          437.40        4,177.09       48,310.51
        14    4614.49          402.59        4,211.90       44,098.61
        15    4614.49          367.49        4,247.00       39,851.61
        16    4614.49          332.10        4,282.39       35,569.22
        17    4614.49          296.41        4,318.08       31,251.14
        18    4614.49          260.43        4,354.06       26,897.08
        19    4614.49          224.14        4,390.35       22,506.73
        20    4614.49          187.56        4,426.93       18,079.80
        21    4614.49          150.67        4,463.82       13,615.98
        22    4614.49          113.47        4,501.02        9,114.96
        23    4614.49           75.96        4,538.53        4,576.43
        24    4614.49           38.06        4,576.43             .00
            110747.76       10,747.76      100,000.00    1,189,739.34

25 rows selected.

SQL&gt;
&lt;/pre&gt;

&lt;p&gt;&lt;/p&gt;
Results of this query can be verified &lt;a href="http://www.fandktitle.com/calcs/allcalcs/loan_calculator.htm"&gt;here&lt;/a&gt;. 
&lt;p&gt;&lt;/p&gt;
References:
&lt;p&gt;&lt;/p&gt;
1. &lt;a href="http://www.fandktitle.com/calcs/allcalcs/loan_calculator.htm"&gt;Loan Calculator.&lt;/a&gt;&lt;p&gt;&lt;/p&gt;
2. &lt;a href="http://www.banking.hsbc.co.in/in/personal/loans/loan.htm"&gt;How to calculate EMI.&lt;/a&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;

Happy reading.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/34560741-2767834284873228585?l=momendba.blogspot.com'/&gt;&lt;/div&gt;</description><link>http://momendba.blogspot.com/2008/06/sql-for-loan-payment.html</link><author>noreply@blogger.com (Asif Momen)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://4.bp.blogspot.com/_gZsupYKwzXg/SFj0LTsIzbI/AAAAAAAAACk/A4idtgmPqPA/s72-c/LoanPayment_Excel.JPG" height="72" width="72" /><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">3</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-34560741.post-480350193216959260</guid><pubDate>Fri, 06 Jun 2008 00:59:00 +0000</pubDate><atom:updated>2008-10-08T23:44:21.282+03:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">Database</category><category domain="http://www.blogger.com/atom/ns#">SQL</category><title>Create a Unique Constraint Ignoring Existing Duplicate Data</title><description>Hi,
&lt;p&gt;&lt;/p&gt;
There was an interesting post on &lt;a href="http://forums.oracle.com/forums/thread.jspa?messageID=2570452&amp;amp;#2570452"&gt;OTN forums&lt;/a&gt;, where the OP asks:
&lt;p&gt;&lt;/p&gt;

&lt;/span&gt;&lt;span style="font-style: italic; font-family: verdana;font-size:85%;" &gt;Can we create a unique index on a column having duplicate values which can enforce the uniqueness for future values.?&lt;/span&gt;&lt;span style="font-size:85%;"&gt;

&lt;p&gt;&lt;/p&gt;

Well, this is possible as Oracle is rich in features. It can be achieved by creating a unique constraint with DEFERRABLE and NOVALIDATE keywords.  By default, constraints are created as NON DEFERRABLE and VALIDATE. When we create a constraint with DEFERRABLE and NOVALIDATE, we instruct Oracle to defer checking existing data for uniqueness but always check the new or modified rows.
&lt;p&gt;&lt;/p&gt;

Here's how we can do this:

&lt;pre name="code" class="sql"&gt;
SQL&gt; drop table t purge;

Table dropped.

SQL&gt;
SQL&gt; create table t
  2  ( x int );

Table created.

SQL&gt;
SQL&gt;
SQL&gt; insert into t values ( 1 );

1 row created.

SQL&gt; insert into t values ( 1);

1 row created.

SQL&gt;
SQL&gt; alter table t add constraint t_uk unique (x)
  2    deferrable initially immediate novalidate;

Table altered.

SQL&gt; select * from t;

         X
----------
         1
         1

SQL&gt;
SQL&gt; alter table t modify constraint t_uk enable novalidate;

Table altered.

SQL&gt;
SQL&gt; insert into t values ( 1);
insert into t values ( 1)
*
ERROR at line 1:
ORA-00001: unique constraint (TEST.T_UK) violated


SQL&gt; select * from t;

         X
----------
         1
         1

SQL&gt; insert into t values (2);

1 row created.

SQL&gt;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
Happy reading !!!
&lt;p&gt;&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/34560741-480350193216959260?l=momendba.blogspot.com'/&gt;&lt;/div&gt;</description><link>http://momendba.blogspot.com/2008/06/hi-there-was-interesting-post-on-otn.html</link><author>noreply@blogger.com (Asif Momen)</author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-34560741.post-2855955552590148406</guid><pubDate>Mon, 26 May 2008 11:40:00 +0000</pubDate><atom:updated>2008-10-08T23:54:29.771+03:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">Database</category><category domain="http://www.blogger.com/atom/ns#">SQL</category><category domain="http://www.blogger.com/atom/ns#">Tuning</category><title>Database is very slow!!!</title><description>Dear Readers,
&lt;p&gt;&lt;/p&gt;

I received a call from one of our developers and following was the conversation that took place between us:
&lt;p&gt;&lt;/p&gt;
He: “Database is very slow” &lt;br&gt;&lt;/br&gt;
Me: What is very slow? Can you tell me what actually you are doing?
&lt;br&gt;&lt;/br&gt;
He: I am running a simple report which is supposed to return less than 100 records. But it’s been more than 4 hours and the query is still running.
&lt;br&gt;&lt;/br&gt;
Me: How long it use to take before?
&lt;br&gt;&lt;/br&gt;
He: This is the first time we are running this query.
&lt;br&gt;&lt;/br&gt;
Me: Ok, let me log in to the database.
&lt;br&gt;&lt;/br&gt;
&lt;br&gt;&lt;/br&gt;
I logged into the database and ran my set of commands to trace the culprit SQL. I was able to identify the query and here it is:

&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;
SELECT
      TO_CHAR(TRN_DATE, 'YYYY-MM-DD HH24:MI:SS') TRN_DATE,
      TRAN_TYPE,
      TRAN_REF,
      KEY_NO,
      IN_AMT A1,
      0 A2
 FROM TRANSACTION_DAILY
WHERE TRN_DATE = TO_DATE ( '#BUSDATE#', 'DDMMYYYY' )
      AND SRC_FLAG ='L'
      AND (SUBSTR(OUT_KEY_NO,1,10) = 'DXIY0-19XV' or SUBSTR(IN_KEY_NO,1,10) = 'DXIY0-19XV')
      AND TRAN_TYPE  NVL(KEY_NO,'A')  NVL(IN_AMT,0) NOT IN (
                    SELECT  TD_TRAN_TYPE  TD_KEY_NO_REF  NVL(OUT_AMT,0)
                      FROM TRANSACTION_DETAILS
                     WHERE TRN_DATE = TO_DATE ( '#BUSDATE#', 'DDMMYYYY' ) 
                       AND RM_CODE = 'XXX123'  )
UNION
SELECT
      TO_CHAR(TRN_DATE, 'YYYY-MM-DD HH24:MI:SS') TRN_DATE,
      TD_TRAN_TYPE,
      TRAN_REF, 
      TD_KEY_NO_REF, 
      0 A1,
      OUT_AMT A2
 FROM TRANSACTION_DETAILS
WHERE TRN_DATE = TO_DATE ( '#BUSDATE#', 'DDMMYYYY' )
  AND RM_CODE = 'XXX123' 
  AND TD_TRAN_TYPE  TD_KEY_NO_REF  NVL(OUT_AMT,0)   NOT IN (
                    SELECT TRAN_TYPE  NVL(KEY_NO,'A')  NVL(IN_AMT,0)
                      FROM TRANSACTION_DAILY
                     WHERE TRN_DATE = TO_DATE ( '#BUSDATE#', 'DDMMYYYY' )
                       AND SRC_FLAG ='L'
                       AND (SUBSTR(OUT_KEY_NO,1,10) = 'DXIY0-19XV' 
                            or SUBSTR(IN_KEY_NO,1,10) = 'DXIY0-19XV') )
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;

and this is the execution plan:

&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt;
-------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                             | Name                  | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
-------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                      |                       |       |       |    51M(100)|          |       |       |
|   1 |  SORT UNIQUE                          |                       |  2842 |   159K|    51M (70)|198:47:09 |       |       |
|   2 |   UNION-ALL                           |                       |       |       |            |          |       |       |
|*  3 |    FILTER                             |                       |       |       |            |          |       |       |
|   4 |     PARTITION RANGE SINGLE            |                       |  2042 |   135K|  2854   (2)| 00:00:40 |   KEY |   KEY |
|*  5 |      TABLE ACCESS FULL                | TRANSACTION_DAILY     |  2042 |   135K|  2854   (2)| 00:00:40 |   KEY |   KEY |
|   6 |     PARTITION RANGE SINGLE            |                       |   760 | 19000 | 15269   (1)| 00:03:34 |   KEY |   KEY |
|*  7 |      TABLE ACCESS BY LOCAL INDEX ROWID| TRANSACTION_DETAILS   |   760 | 19000 | 15269   (1)| 00:03:34 |   KEY |   KEY |
|*  8 |       INDEX RANGE SCAN                | TRAN_DET_IDX          |   434K|       |   980   (1)| 00:00:14 |   KEY |   KEY |
|*  9 |    FILTER                             |                       |       |       |            |          |       |       |
|  10 |     PARTITION RANGE SINGLE            |                       |   800 | 24800 | 15269   (1)| 00:03:34 |   KEY |   KEY |
|* 11 |      TABLE ACCESS BY LOCAL INDEX ROWID| TRANSACTION_DETAILS   |   800 | 24800 | 15269   (1)| 00:03:34 |   KEY |   KEY |
|* 12 |       INDEX RANGE SCAN                | TRAN_DET_IDX          |   434K|       |   980   (1)| 00:00:14 |   KEY |   KEY |
|* 13 |     TABLE ACCESS BY GLOBAL INDEX ROWID| TRANSACTION_DAILY     |  1940 |   111K| 88735   (1)| 00:20:43 | ROW L | ROW L |
|* 14 |      INDEX RANGE SCAN                 | PK_TRANSACTION_DAILY  |   102K|       |   413   (1)| 00:00:06 |       |       |
-------------------------------------------------------------------------------------------------------------------------------


&lt;/pre&gt;

&lt;p&gt;&lt;/p&gt;
From the above execution plan, Optimizer thinks it needs “198:47:09” hours to “Sort Unique” the result set. But the actual time the query took to complete was 6 hours and 49 minute (24540 seconds).
&lt;p&gt;&lt;/p&gt;

I then simplified the query in order to understand it clearly by removing some of the predicates. The simplified query is:

&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt; 
SELECT *
 FROM TRANSACTION_DAILY
WHERE TRN_DATE = TO_DATE ( '#BUSDATE#', 'DDMMYYYY' ) 
      AND TRAN_TYPE || NVL(KEY_NO,'A') || NVL(IN_AMT,0) NOT IN (
                    SELECT  TD_TRAN_TYPE || 
                            TD_KEY_NO_REF || 
                            NVL(OUT_AMT,0) 
                      FROM TRANSACTION_DETAILS
                     WHERE TRN_DATE = 
                           TO_DATE ( '#BUSDATE#', 'DDMMYYYY' )  )
UNION
SELECT *
 FROM TRANSACTION_DETAILS
WHERE TRN_DATE = TO_DATE ( '#BUSDATE#', 'DDMMYYYY' ) 
  AND TD_TRAN_TYPE || TD_KEY_NO_REF || NVL(OUT_AMT,0)   NOT IN (
                    SELECT TRAN_TYPE || 
                           NVL(KEY_NO,'A') || 
                           NVL(IN_AMT,0)
                      FROM TRANSACTION_DAILY
                     WHERE TRN_DATE = 
                            TO_DATE ( '#BUSDATE#', 'DDMMYYYY' ) )

&lt;/pre&gt;

&lt;p&gt;&lt;/p&gt;
Hmmmm, this query seems to be doing a full outer join on “TRANSACTION_DAILY” and “TRANSACTION_DETAILS” tables. I called him back:
&lt;p&gt;&lt;/p&gt;

Me: What is the objective of this report?&lt;br&gt;&lt;/br&gt;
He: We need to find all the information from “TRANSACTION_DAILY” table which does not exist in “TRANSACTION_DETAILS” table and vice versa.&lt;p&gt;&lt;/p&gt;

Yes, I was right. It’s doing a “Full Outer Join”. I modified his original query and proposed him a new query which is executing at lighting speed and more importantly yielding the same result:
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt; 
SELECT  nvl(a.TRN_DATE, b.TRN_DATE) TRN_DATE,
        a.TRN_TYPE, b.OTH_TRN_TYPE,
        a.KEY_NO, b.KEY_NO_REF,
        nvl(a.IN_AMT, 0) A1, 
        nvl(b.OUT_AMT, 0) A2
FROM  (select TRN_DATE, TRN_TYPE, KEY_NO, IN_AMT, 
         from TRANSACTION_DAILY 
        where TRN_DATE = to_date('16-05-2008', 'DD-MM-YYYY')
          and (substr(IN_KEY_NO, 1, 10) = 'DXIY0-19XV' OR
               substr(OUT_KEY_NO, 1, 10) = 'DXIY0-19XV')) a 
  FULL OUTER JOIN 
      (select TRN_DATE, OTH_TRN_TYPE, KEY_NO_REF, OUT_AMT, 
         from TRANSACTION_DETAILS 
        where RM_CODE = 'XXX123'
          and TRN_DATE = to_date('16-05-2008', 'DD-MM-YYYY')) b
  ON (a.TRN_DATE = b.TRN_DATE and 
      a.TRN_TYPE = b.OTH_TRN_TYPE and 
      a.KEY_NO = b.KEY_NO_REF and 
      a.IN_AMT = b.OUT_AMT and 
      a.rno = b.rno)
WHERE (a.TRN_TYPE is null or 
       b.OTH_TRN_TYPE is null or 
       a.KEY_NO is null or 
       b.KEY_NO_REF is null or 
       a.IN_AMT is null or 
       b.OUT_AMT is null);

&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;

I called him back with over-excitement announcing him that the query is ready and let us test it:
&lt;p&gt;&lt;/p&gt;

Me: I have re-written the query, kindly come over to my office so that we can test it together.&lt;br&gt;&lt;/br&gt;
He: How long does it take to execute?&lt;br&gt;&lt;/br&gt;
Me: It completes in less than 4 seconds.&lt;br&gt;&lt;/br&gt;
He: What...? … less than 4 seconds … ? … Are you sure, have you included all the condition that I have mentioned?&lt;br&gt;&lt;/br&gt;
Me: Yes, let us test it.&lt;p&gt;&lt;/p&gt;

He made couple of tests and the query was perfect to all the test cases but except one. Requirements started getting little messy so, I decided to create two tables and test the query against them. I created two tables, A and B, of same table structure with sample data:
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt; 
create table a(id number, amt number);
insert into a values (1, 10);
insert into a values (2, 20);
insert into a values (3, 30);
insert into a values (4, 40);

create table b(id number, amt number);
insert into b values (2, 20);
insert into b values (3, 30);
insert into b values (5, 50);
insert into b values (6, 60);

commit;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
Basically, we need to find out data that exists in A and is not in B and also records that exist in B which are not A. Here’s the query:
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt; 
SQL&gt; select *
  2    from a FULL OUTER JOIN b
  3      on a.id = b.id
  4   where a.id is null or b.id is null;

        ID        AMT         ID        AMT
---------- ---------- ---------- ----------
         4         40
                               6         60
                               5         50

SQL&gt;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;

He said: “Yes, the query is fetching right data, but what happens when you have two records with same id and amt in table A and only a single record in B? In this case, the query should display one record from table A.”
&lt;p&gt;&lt;/p&gt;
Adding more to the complexity he said: “There is no third column in either table to distinguish this occurrence.”
&lt;p&gt;&lt;/p&gt;

So, we inserted a record in each table and ran the same query:
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt; 
insert into a values (1, 10);
insert into b values (1, 10);


SQL&gt; select *
  2    from a FULL OUTER JOIN b
  3      on a.id = b.id
  4   where a.id is null or b.id is null;

        ID        AMT         ID        AMT
---------- ---------- ---------- ----------
         4         40
                               6         60
                               5         50

SQL&gt;

&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
Oops!! The query fails at this point. Ok, I then decided to re-write this query and this time use analytical functions to rescue me from the current problematic situation.
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt; 
SQL&gt; select x.id, x.amt, y.id, y.amt
  2    from (select id, amt, 
  3                 row_number() over (partition by id, amt 
  4                                    order by id, amt) rno 
  5            from a) x
  6         FULL OUTER JOIN
  7         (select id, amt, 
  8                 row_number() over (partition by id, amt 
  9                                     order by id, amt) rno 
 10                 from b) y
 11      on x.id = y.id and x.rno = y.rno
 12   where x.id is null 
 13      or y.id is null 
 14      or x.rno is null 
 15      or y.rno is null;

        ID        AMT         ID        AMT
---------- ---------- ---------- ----------
         1         10
         4         40
                               5         50
                               6         60

SQL&gt;

&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
Yippy!!! This query rocks. I then quickly transformed the original query into this form and ran it again:

&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt; 
SELECT  nvl(a.TRN_DATE, b.TRN_DATE) TRN_DATE,
        a.TRN_TYPE, b.OTH_TRN_TYPE,
        a.KEY_NO, b.KEY_NO_REF,
        nvl(a.IN_AMT, 0) A1, 
        nvl(b.OUT_AMT, 0) A2
FROM  (select TRN_DATE, TRN_TYPE, KEY_NO, IN_AMT, 
              row_number() over (partition by KEY_NO, 
                                              TRN_TYPE, 
                                              IN_AMT 
                                     order by KEY_NO, 
                                              TRN_TYPE, 
                                              IN_AMT) rno 
         from TRANSACTION_DAILY 
        where TRN_DATE = to_date('16-05-2008', 'DD-MM-YYYY')
          and (substr(IN_KEY_NO, 1, 10) = 'DXIY0-19XV' OR
               substr(OUT_KEY_NO, 1, 10) = 'DXIY0-19XV')) a 
  FULL OUTER JOIN 
      (select TRN_DATE, OTH_TRN_TYPE, KEY_NO_REF, OUT_AMT, 
              row_number() over (partition by KEY_NO_REF, 
                                              OTH_TRN_TYPE, 
                                              OUT_AMT 
                                     order by KEY_NO_REF, 
                                              OTH_TRN_TYPE, 
                                              OUT_AMT) rno    
         from TRANSACTION_DETAILS 
        where RM_CODE = 'XXX123'
          and TRN_DATE = to_date('16-05-2008', 'DD-MM-YYYY')) b
  ON (a.TRN_DATE = b.TRN_DATE and 
      a.TRN_TYPE = b.OTH_TRN_TYPE and 
      a.KEY_NO = b.KEY_NO_REF and
      a.IN_AMT = b.OUT_AMT and 
      a.rno = b.rno)
WHERE (a.TRN_TYPE is null or 
       b.OTH_TRN_TYPE is null or 
       a.KEY_NO is null or 
       b.KEY_NO_REF is null or 
       a.IN_AMT is null or 
       b.OUT_AMT is null or
       a.rno is null or
       b.rno is null);
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
Execution plan of this query is:
&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt; 
----------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                                | Name                  | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
----------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                         |                       |  2230 |   409K| 48722   (1)| 00:11:23 |       |       |
|   1 |  VIEW                                    |                       |  2230 |   409K| 48722   (1)| 00:11:23 |       |       |
|   2 |   UNION-ALL                              |                       |       |       |            |          |       |       |
|*  3 |    FILTER                                |                       |       |       |            |          |       |       |
|*  4 |     HASH JOIN RIGHT OUTER                |                       |  1552 |   175K| 24361   (1)| 00:05:42 |       |       |
|   5 |      VIEW                                |                       |   678 | 40002 | 14852   (1)| 00:03:28 |       |       |
|   6 |       WINDOW SORT                        |                       |   678 | 12882 | 14852   (1)| 00:03:28 |       |       |
|   7 |        PARTITION RANGE SINGLE            |                       |   678 | 12882 | 14851   (1)| 00:03:28 |    14 |    14 |
|*  8 |         TABLE ACCESS BY LOCAL INDEX ROWID| TRANSACTION_DETAILS   |   678 | 12882 | 14851   (1)| 00:03:28 |    14 |    14 |
|*  9 |          INDEX RANGE SCAN                | TRAN_DET_IDX          |   422K|       |   897   (1)| 00:00:13 |    14 |    14 |
|  10 |      VIEW                                |                       |  1552 | 88464 |  9508   (1)| 00:02:14 |       |       |
|  11 |       WINDOW SORT                        |                       |  1552 | 69840 |  9508   (1)| 00:02:14 |       |       |
|  12 |        PARTITION RANGE SINGLE            |                       |  1552 | 69840 |  9507   (1)| 00:02:14 |     7 |     7 |
|* 13 |         TABLE ACCESS FULL                | TRANSACTION_DAILY     |  1552 | 69840 |  9507   (1)| 00:02:14 |     7 |     7 |
|* 14 |    HASH JOIN ANTI                        |                       |   678 | 78648 | 24361   (1)| 00:05:42 |       |       |
|  15 |     VIEW                                 |                       |   678 | 40002 | 14852   (1)| 00:03:28 |       |       |
|  16 |      WINDOW SORT                         |                       |   678 | 12882 | 14852   (1)| 00:03:28 |       |       |
|  17 |       PARTITION RANGE SINGLE             |                       |   678 | 12882 | 14851   (1)| 00:03:28 |    14 |    14 |
|* 18 |        TABLE ACCESS BY LOCAL INDEX ROWID | TRANSACTION_DETAILS   |   678 | 12882 | 14851   (1)| 00:03:28 |    14 |    14 |
|* 19 |         INDEX RANGE SCAN                 | TRAN_DET_IDX          |   422K|       |   897   (1)| 00:00:13 |    14 |    14 |
|  20 |     VIEW                                 |                       |  1552 | 88464 |  9508   (1)| 00:02:14 |       |       |
|  21 |      WINDOW SORT                         |                       |  1552 | 69840 |  9508   (1)| 00:02:14 |       |       |
|  22 |       PARTITION RANGE SINGLE             |                       |  1552 | 69840 |  9507   (1)| 00:02:14 |     7 |     7 |
|* 23 |        TABLE ACCESS FULL                 | TRANSACTION_DAILY     |  1552 | 69840 |  9507   (1)| 00:02:14 |     7 |     7 |
----------------------------------------------------------------------------------------------------------------------------------
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
The results were convincing and it still takes 4 seconds to fetch the data compared to 6 hours and 49 minutes. It is 6135 times faster than the original one.
&lt;p&gt;&lt;/p&gt;

This query left End-Users rejoicing and he left my desk smiling but without answering  my question, “Is the database very slow?”
&lt;p&gt;&lt;/p&gt;

P.S.: I have renamed all the table names and columns names so as not to reveal official and sensitive information on my personal blog, yet preserving the reality.
&lt;p&gt;&lt;/p&gt;

Regards

&lt;/span&gt;&lt;/span&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/34560741-2855955552590148406?l=momendba.blogspot.com'/&gt;&lt;/div&gt;</description><link>http://momendba.blogspot.com/2008/05/database-is-very-slow.html</link><author>noreply@blogger.com (Asif Momen)</author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-34560741.post-4485139245569518912</guid><pubDate>Fri, 23 May 2008 06:55:00 +0000</pubDate><atom:updated>2008-10-09T00:02:03.353+03:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">Database</category><category domain="http://www.blogger.com/atom/ns#">Tuning</category><title>Automatic statistics gathering during Index Creation and Rebuilds</title><description>Dear all,
&lt;p&gt;&lt;/p&gt;
While testing dynamic sampling in Oracle 10g, I came to learn a 10g new feature (very late though). 
&lt;p&gt;&lt;/p&gt;
My test case for dynamic sampling goes like this:
&lt;p&gt;&lt;/p&gt;
(a) Create table with data 
(b) Create an index, and
(c) Execute the query and note the execution plan 

&lt;pre name="code" class="sql"&gt;
SQL&gt; Create table t as select * from scott.emp;

Table created.

SQL&gt; Create index t_idx on t(empno);

Index created.

SQL&gt; set autotrace traceonly explain
SQL&gt; Select * from t where empno = 7788;

Execution Plan
----------------------------------------------------------
Plan hash value: 1020776977

-------------------------------------------------------------------------------------
| Id  | Operation                   | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |       |     1 |    87 |     2   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID| T     |     1 |    87 |     2   (0)| 00:00:01 |
|*  2 |   INDEX RANGE SCAN          | T_IDX |     1 |       |     1   (0)| 00:00:01 |
-------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - access("EMPNO"=7788)

Note
-----
   - dynamic sampling used for this statement

SQL&gt; Set autotrace off
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
Yes, the optimizer did dynamic sampling and picked up the index plan as the optimal execution plan. Now, let me delete the statistics and re-run the same SQL.
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;
SQL&gt; Exec dbms_stats.delete_table_stats(user, 't');

PL/SQL procedure successfully completed.

SQL&gt; set autotrace traceonly explain
SQL&gt; Select * from t where empno = 7788;

Execution Plan
----------------------------------------------------------
Plan hash value: 2153619298

--------------------------------------------------------------------------
| Id  | Operation         | Name | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |      |     1 |    87 |     3   (0)| 00:00:01 |
|*  1 |  TABLE ACCESS FULL| T    |     1 |    87 |     3   (0)| 00:00:01 |
--------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - filter("EMPNO"=7788)

Note
-----
   - dynamic sampling used for this statement

SQL&gt; Set autotrace off
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
Oops, optimizer did consider dynamic sampling but this time chose a Full Table Scan. How’ come the same optimizer picked index access plan on the first run? 
&lt;p&gt;&lt;/p&gt;
Ok, let me do it again and this time being very careful.
&lt;p&gt;&lt;/p&gt;
&lt;span style="font-weight:bold;"&gt;(a) Create table with data &lt;/span&gt;

&lt;pre name="code" class="sql"&gt;
SQL&gt; Drop table t purge;

Table dropped.

SQL&gt; Create table t as select * from scott.emp;

Table created.

SQL&gt; Select num_rows, last_analyzed from user_tables where table_name = 'T';

  NUM_ROWS LAST_ANALYZED
---------- --------------


SQL&gt;
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;span style="font-weight:bold;"&gt;(b) Create an index, and&lt;/span&gt;
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;
SQL&gt; Create index t_idx on t(empno);

Index created.

SQL&gt; column index_name format a10
SQL&gt; set line 10000

SQL&gt; select blevel, leaf_blocks, distinct_keys, clustering_factor, 
  2  num_rows, last_analyzed, user_stats, global_stats
  3  from user_indexes
  4  where index_name = 'T_IDX';

    BLEVEL LEAF_BLOCKS DISTINCT_KEYS CLUSTERING_FACTOR   NUM_ROWS LAST_ANALYZED        USE GLO
---------- ----------- ------------- ----------------- ---------- -------------------- --- ---
         0           1            14                 1         14 05-May-2008 10:14:48 NO  NO

SQL&gt; 
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
Oh! I did not gather statistics.
&lt;p&gt;&lt;/p&gt;
&lt;span style="font-weight:bold;"&gt;(c) Execute the query and note the execution plan &lt;/span&gt;
&lt;p&gt;&lt;/p&gt;

&lt;pre name="code" class="sql"&gt;
SQL&gt; set autotrace traceonly explain
SQL&gt; Select * from t where empno = 7788;

Execution Plan
----------------------------------------------------------
Plan hash value: 1020776977

-------------------------------------------------------------------------------------
| Id  | Operation                   | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |       |     1 |    87 |     2   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID| T     |     1 |    87 |     2   (0)| 00:00:01 |
|*  2 |   INDEX RANGE SCAN          | T_IDX |     1 |       |     1   (0)| 00:00:01 |
-------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - access("EMPNO"=7788)

Note
-----
   - dynamic sampling used for this statement

SQL&gt; Set autotrace off
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
Now it’s clear why optimizer finds Index Access Path to be best plan. Let’s continue and delete the statistics and then execute the query again:
&lt;p&gt;&lt;/p&gt;
&lt;pre name="code" class="sql"&gt;
SQL&gt; Exec dbms_stats.delete_table_stats(user, 't');

PL/SQL procedure successfully completed.

SQL&gt; select blevel, leaf_blocks, distinct_keys, clustering_factor, 
  2  num_rows, last_analyzed, user_stats, global_stats
  3  from user_indexes
  4  where index_name = 'T_IDX';

    BLEVEL LEAF_BLOCKS DISTINCT_KEYS CLUSTERING_FACTOR   NUM_ROWS LAST_ANALYZED        USE GLO
---------- ----------- ------------- ----------------- ---------- -------------------- --- ---
                                                                                       NO  NO

SQL&gt; set autotrace traceonly explain
SQL&gt; Select * from t where empno = 7788;

Execution Plan
----------------------------------------------------------
Plan hash value: 2153619298

--------------------------------------------------------------------------
| Id  | Operation         | Name | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |      |     1 |    87 |     3   (0)| 00:00:01 |
|*  1 |  TABLE ACCESS FULL| T    |     1 |    87 |     3   (0)| 00:00:01 |
--------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - filter("EMPNO"=7788)

Note
-----
   - dynamic sampling used for this statement

SQL&gt;  Set autotrace off
&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;

This time the optimizer did dynamic sampling and FTS seems to be less expensive.
&lt;p&gt;&lt;/p&gt;
In Oracle 10g, when an index is created, Oracle automatically gathers statistics on the newly created index. This is also true in case of index rebuild. Because of this reason, optimizer picked Index Path to be the best plan.
&lt;p&gt;&lt;/p&gt;

References:
&lt;br&gt;&lt;/br&gt;
Oracle® Database SQL Reference 10g Release 2 (10.2)
&lt;p&gt;&lt;/p&gt;
Regards&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/34560741-4485139245569518912?l=momendba.blogspot.com'/&gt;&lt;/div&gt;</description><link>http://momendba.blogspot.com/2008/05/automatic-statistics-gathering-during_23.html</link><author>noreply@blogger.com (Asif Momen)</author><thr:total xmlns:thr="http://purl.org/syndication/thread/1.0">0</thr:total></item></channel></rss>
