<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Striving for Optimal Performance &#187; Partitioning</title>
	<atom:link href="http://antognini.ch/category/oracledatabase/partitioning/feed/" rel="self" type="application/rss+xml" />
	<link>http://antognini.ch</link>
	<description></description>
	<lastBuildDate>Sat, 17 Dec 2011 23:42:45 +0000</lastBuildDate>
	<generator>http://wordpress.org/?v=2.9.2</generator>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
			<item>
		<title>Deferred Segment Creation as of 11.2.0.2</title>
		<link>http://antognini.ch/2010/10/deferred-segment-creation-as-of-11-2-0-2/</link>
		<comments>http://antognini.ch/2010/10/deferred-segment-creation-as-of-11-2-0-2/#comments</comments>
		<pubDate>Tue, 05 Oct 2010 08:29:04 +0000</pubDate>
		<dc:creator>Christian Antognini</dc:creator>
				<category><![CDATA[11gR2]]></category>
		<category><![CDATA[Partitioning]]></category>

		<guid isPermaLink="false">http://antognini.ch/?p=1314</guid>
		<description><![CDATA[One year ago I wrote a post entitled Deferred Segment Creation. If you read the comments related to it you can see that several people were concerned by the fact that it was not possible to easily get rid of segments associated to empty tables. That was with version 11.2.0.1. Now we have version 11.2.0.2 [...]]]></description>
			<content:encoded><![CDATA[<p>One year ago I wrote a post entitled <a href="/2009/09/deferred-segment-creation/">Deferred Segment Creation</a>. If you read the <a href="/2009/09/deferred-segment-creation/#comments">comments</a> related to it you can see that several people were concerned by the fact that it was not possible to easily get rid of segments associated to empty tables. That was with version 11.2.0.1. Now we have version 11.2.0.2 and this specific problem has been fixed.</p>
<p>As of 11.2.0.2 DBMS_SPACE_ADMIN, and not DBMS_SPACE as written in the <a href="http://download.oracle.com/docs/cd/E11882_01/server.112/e17128/chapter1_2.htm#FEATURENO09129">New Feature Guide</a>, provides the following procedures:</p>
<ul>
<li>MATERIALIZE_DEFERRED_SEGMENTS</li>
<li>DROP_EMPTY_SEGMENTS</li>
</ul>
<p>With them it is possible to materialize/drop the segments of the empty tables and their associated objects. Depending on the specified parameters, they can process all database segments, all segments owned by a specific schema, all segments associated to a specific table, or all segments associated to a specific partition.</p>
<p>Let’s have a look to an example:</p>
<ul>
<li>Create a partitioned table with a primary key and a LOB column</li>
</ul>
<p><pre>SQL&gt; CREATE TABLE t (
  2    id NUMBER,
  3    c CLOB,
  4    CONSTRAINT t_pk PRIMARY KEY (id) USING INDEX LOCAL
  5  )
  6  SEGMENT CREATION DEFERRED
  7  PARTITION BY HASH(id) PARTITIONS 4;</pre></p>
<ul>
<li>Show that no segment is available</li>
</ul>
<p><pre>SQL&gt; SELECT segment_name, segment_type, bytes, extents
  2  FROM user_segments
  3  WHERE segment_name IN (&#039;T&#039;,&#039;T_PK&#039;)
  4  OR segment_name IN (SELECT segment_name
  5                      FROM user_lobs
  6                      WHERE table_name = &#039;T&#039;)
  7  ORDER BY 1,2;

no rows selected</pre></p>
<ul>
<li>Materialize the segments</li>
</ul>
<p><pre>SQL&gt; BEGIN
  2    dbms_space_admin.materialize_deferred_segments(
  3      schema_name =&gt; &#039;CHA&#039;,
  4      table_name  =&gt; &#039;T&#039;
  5    );
  6  END;
  7  /</pre></p>
<ul>
<li>Show that the segments are now available</li>
</ul>
<p><pre>SQL&gt; SELECT segment_name, segment_type, bytes, extents
  2  FROM user_segments
  3  WHERE segment_name IN (&#039;T&#039;,&#039;T_PK&#039;)
  4  OR segment_name IN (SELECT segment_name
  5                      FROM user_lobs
  6                      WHERE table_name = &#039;T&#039;)
  7  ORDER BY 1,2;

SEGMENT_NAME                   SEGMENT_TYPE              BYTES    EXTENTS
------------------------------ -------------------- ---------- ----------
SYS_LOB0000103611C00002$$      LOB PARTITION           8388608          1
SYS_LOB0000103611C00002$$      LOB PARTITION           8388608          1
SYS_LOB0000103611C00002$$      LOB PARTITION           8388608          1
SYS_LOB0000103611C00002$$      LOB PARTITION           8388608          1
T                              TABLE PARTITION         8388608          1
T                              TABLE PARTITION         8388608          1
T                              TABLE PARTITION         8388608          1
T                              TABLE PARTITION         8388608          1
T_PK                           INDEX PARTITION           65536          1
T_PK                           INDEX PARTITION           65536          1
T_PK                           INDEX PARTITION           65536          1
T_PK                           INDEX PARTITION           65536          1</pre></p>
<ul>
<li>Get rid of the segments (this is possible because the table is empty)</li>
</ul>
<p><pre>SQL&gt; BEGIN
  2    dbms_space_admin.drop_empty_segments(
  3      schema_name =&gt; &#039;CHA&#039;,
  4      table_name  =&gt; &#039;T&#039;
  5    );
  6  END;
  7  /</pre></p>
<ul>
<li>Show that no segment is available</li>
</ul>
<p><pre>SQL&gt; SELECT segment_name, segment_type, bytes, extents
  2  FROM user_segments
  3  WHERE segment_name IN (&#039;T&#039;,&#039;T_PK&#039;)
  4  OR segment_name IN (SELECT segment_name
  5                      FROM user_lobs
  6                      WHERE table_name = &#039;T&#039;)
  7  ORDER BY 1,2;

no rows selected</pre></p>
<p>The attentive reader might have noticed two additional new features available since 11.2.0.2. The first one is that deferred segment creation is also supported for partitioned table. The second one is that the initial extents associated to partitioned tables and partitioned LOBs (but not to partitioned indexes) have a new default size of 8MB. Note that this new default is only used for segments created in an EXTENT MANAGEMENT LOCAL AUTOALLOCATE tablespace.</p>
]]></content:encoded>
			<wfw:commentRss>http://antognini.ch/2010/10/deferred-segment-creation-as-of-11-2-0-2/feed/</wfw:commentRss>
		<slash:comments>8</slash:comments>
		</item>
		<item>
		<title>Partially Index a Table</title>
		<link>http://antognini.ch/2010/08/partially-index-a-table/</link>
		<comments>http://antognini.ch/2010/08/partially-index-a-table/#comments</comments>
		<pubDate>Wed, 04 Aug 2010 07:31:40 +0000</pubDate>
		<dc:creator>Christian Antognini</dc:creator>
				<category><![CDATA[11gR2]]></category>
		<category><![CDATA[Indexes]]></category>
		<category><![CDATA[Partitioning]]></category>

		<guid isPermaLink="false">http://antognini.ch/?p=1214</guid>
		<description><![CDATA[Recenty the following question was posted on oracle-l (I paraphrase&#8230;): 
With Oracle Database it is possible to create something similar to Teradata&#8217;s sparse indexes?
Since the question is an interesting one, I decided to write this short post.
First of all, I have to say that such a feature is not supported by the CREATE INDEX statement [...]]]></description>
			<content:encoded><![CDATA[<p>Recenty the following question was posted on <a href="http://www.freelists.org/post/oracle-l/oracle-equivalent-of-teradatas-sparse-index">oracle-l</a> (I paraphrase&#8230;): </p>
<blockquote><p>With Oracle Database it is possible to create something similar to Teradata&#8217;s sparse indexes?</p></blockquote>
<p>Since the question is an interesting one, I decided to write this short post.</p>
<p>First of all, I have to say that such a feature is not supported by the CREATE INDEX statement with Oracle Database. What a pity! I would really like to see this feature in Oracle Database 12x (I bet that &#8220;x&#8221; will be &#8220;f&#8221;; post you opinion as a comment if you want). Several database engines do so. Teradata was mentioned by the OP, two additional examples are <a href="http://www.postgresql.org/docs/current/static/sql-createindex.html">PostgreSQL</a> and <a href="http://msdn.microsoft.com/en-us/library/ms188783.aspx">SQL Server</a>.</p>
<p>How to do something similar with Oracle Database?</p>
<p>As of Oracle Database 11g Release 2 it is possible to partially index a table by taking advantage of <a href="/2009/11/zero-size-unusable-indexes-and-the-query-optimizer/">Zero-Size Unusable Indexes</a>. The following SQL statements show an example where only the data of August 2010 is indexed.</p>
<p><pre>SQL&gt; CREATE TABLE t AS
  2  SELECT rownum AS id, sysdate-mod(rownum,100) AS tim, rpad(&#039;*&#039;,50,&#039;*&#039;) AS pad
  3  FROM dual
  4  CONNECT BY level &lt;= 1000;

SQL&gt; CREATE INDEX i ON t (tim)
  2  GLOBAL PARTITION BY RANGE (tim) (
  3    PARTITION i_201001 VALUES LESS THAN (to_date(&#039;2010-02-01&#039;,&#039;YYYY-MM-DD&#039;)),
  4    PARTITION i_201002 VALUES LESS THAN (to_date(&#039;2010-03-01&#039;,&#039;YYYY-MM-DD&#039;)),
  5    PARTITION i_201003 VALUES LESS THAN (to_date(&#039;2010-04-01&#039;,&#039;YYYY-MM-DD&#039;)),
  6    PARTITION i_201004 VALUES LESS THAN (to_date(&#039;2010-05-01&#039;,&#039;YYYY-MM-DD&#039;)),
  7    PARTITION i_201005 VALUES LESS THAN (to_date(&#039;2010-06-01&#039;,&#039;YYYY-MM-DD&#039;)),
  8    PARTITION i_201006 VALUES LESS THAN (to_date(&#039;2010-07-01&#039;,&#039;YYYY-MM-DD&#039;)),
  9    PARTITION i_201007 VALUES LESS THAN (to_date(&#039;2010-08-01&#039;,&#039;YYYY-MM-DD&#039;)),
 10    PARTITION i_201008 VALUES LESS THAN (to_date(&#039;2010-09-01&#039;,&#039;YYYY-MM-DD&#039;)),
 11    PARTITION i_201009 VALUES LESS THAN (to_date(&#039;2010-10-01&#039;,&#039;YYYY-MM-DD&#039;)),
 12    PARTITION i_201010 VALUES LESS THAN (to_date(&#039;2010-11-01&#039;,&#039;YYYY-MM-DD&#039;)),
 13    PARTITION i_201011 VALUES LESS THAN (to_date(&#039;2010-12-01&#039;,&#039;YYYY-MM-DD&#039;)),
 14    PARTITION i_201012 VALUES LESS THAN (to_date(&#039;2011-01-01&#039;,&#039;YYYY-MM-DD&#039;)),
 15    PARTITION i_maxvalue VALUES LESS THAN (MAXVALUE)
 16  )
 17  UNUSABLE;

SQL&gt; ALTER INDEX i REBUILD PARTITION i_201008;</pre></p>
<p>It goes without saying that you are not forced to have so many partitions in place. In fact, to index the data of August, the following CREATE INDEX is more appropriate.</p>
<p><pre>SQL&gt; CREATE INDEX i ON t (tim)
  2  GLOBAL PARTITION BY RANGE (tim) (
  3    PARTITION i_201007 VALUES LESS THAN (to_date(&#039;2010-08-01&#039;,&#039;YYYY-MM-DD&#039;)),
  4    PARTITION i_201008 VALUES LESS THAN (to_date(&#039;2010-09-01&#039;,&#039;YYYY-MM-DD&#039;)),
  5    PARTITION i_maxvalue VALUES LESS THAN (MAXVALUE)
  6  )
  7  UNUSABLE;</pre></p>
<p>Then, to index the data of September, you have to execute some SQL statements like the following ones.</p>
<ul>
<li>Create a partition for the data of September and rebuild it:</li>
</ul>
<p><pre>SQL&gt; ALTER INDEX i SPLIT PARTITION i_maxvalue AT (to_date(&#039;2010-10-01&#039;,&#039;YYYY-MM-DD&#039;)) INTO (
  2    PARTITION i_201009,
  3    PARTITION i_maxvalue
  4  );

SQL&gt; ALTER INDEX i REBUILD PARTITION i_201009;</pre></p>
<ul>
<li>Drop the oldest partition:</li>
</ul>
<p><pre>SQL&gt; ALTER INDEX i DROP PARTITION i_201007;</pre></p>
<ul>
<li>Make unusable the partition for the data of August:</li>
</ul>
<p><pre>SQL&gt; ALTER INDEX i MODIFY PARTITION i_201008 UNUSABLE;</pre></p>
<p>In this way you have at most three partitions available. And, for most of the time, only one of them is usable and, therefore, occupying space.</p>
<p>Even though in the example I provide in this post I use a global index, you can use the same technique with local indexes as well. That said, I see no problem in using a global index as the one shown in this post.</p>
]]></content:encoded>
			<wfw:commentRss>http://antognini.ch/2010/08/partially-index-a-table/feed/</wfw:commentRss>
		<slash:comments>9</slash:comments>
		</item>
		<item>
		<title>Partition-Wise Join of List-Partitioned Tables</title>
		<link>http://antognini.ch/2010/08/partition-wise-join-of-list-partitioned-tables/</link>
		<comments>http://antognini.ch/2010/08/partition-wise-join-of-list-partitioned-tables/#comments</comments>
		<pubDate>Mon, 02 Aug 2010 09:59:08 +0000</pubDate>
		<dc:creator>Christian Antognini</dc:creator>
				<category><![CDATA[10gR1]]></category>
		<category><![CDATA[10gR2]]></category>
		<category><![CDATA[11gR1]]></category>
		<category><![CDATA[11gR2]]></category>
		<category><![CDATA[Partitioning]]></category>
		<category><![CDATA[Query Optimizer]]></category>

		<guid isPermaLink="false">http://antognini.ch/?p=1202</guid>
		<description><![CDATA[When two tables are equi-partitioned on their join keys, the query optimizer is able to take advantage of partition-wise joins. To make sure that the tables are equi-partitioned, as of Oracle Database 11g reference partitioning can be used. In fact, per definition, with reference partitioning all “related” tables have exactly the same partitioning schema. If [...]]]></description>
			<content:encoded><![CDATA[<p>When two tables are equi-partitioned on their join keys, the query optimizer is able to take advantage of partition-wise joins. To make sure that the tables are equi-partitioned, as of Oracle Database 11g reference partitioning can be used. In fact, per definition, with reference partitioning all “related” tables have exactly the same partitioning schema. If you are not using reference partitioning, you must be very careful that the tables are effectively partitioned in very same way. For range and hash partitioned tables this is usually not a problem. However, when using list partitioning, it is quite easy to make a mistake. The reason is that the partitions can be defined in any order. Let’s have a look to an example based on the following two tables.</p>
<p><pre>SQL&gt; CREATE TABLE t1p
  2  PARTITION BY LIST (pkey) (
  3    PARTITION p_0 VALUES (0),
  4    PARTITION p_1 VALUES (1),
  5    PARTITION p_2 VALUES (2),
  6    PARTITION p_3 VALUES (3),
  7    PARTITION p_4 VALUES (4),
  8    PARTITION p_5 VALUES (5),
  9    PARTITION p_6 VALUES (6),
 10    PARTITION p_7 VALUES (7),
 11    PARTITION p_8 VALUES (8),
 12    PARTITION p_9 VALUES (9)
 13  )
 14  AS
 15  SELECT rownum AS num, mod(rownum,10) AS pkey, dbms_random.string(&#039;p&#039;,50) AS pad
 16  FROM dual
 17  CONNECT BY level &lt;= 10000;

SQL&gt; CREATE TABLE t2p
  2  PARTITION BY LIST (pkey) (
  3    PARTITION p_0 VALUES (0),
  4    PARTITION p_1 VALUES (1),
  5    PARTITION p_2 VALUES (2),
  6    PARTITION p_3 VALUES (3),
  7    PARTITION p_5 VALUES (5),
  8    PARTITION p_4 VALUES (4),
  9    PARTITION p_6 VALUES (6),
 10    PARTITION p_7 VALUES (7),
 11    PARTITION p_8 VALUES (8),
 12    PARTITION p_9 VALUES (9)
 13  )
 14  AS
 15  SELECT rownum AS num, mod(rownum,10) AS pkey, dbms_random.string(&#039;p&#039;,50) AS pad
 16  FROM dual
 17  CONNECT BY level &lt;= 10000;

SQL&gt; BEGIN
  2    dbms_stats.gather_table_stats(user,&#039;t1p&#039;);
  3    dbms_stats.gather_table_stats(user,&#039;t2p&#039;);
  4  END;
  5  /</pre></p>
<p>Even though they are logically equivalent, as shown in the following execution plan, with them partition-wise joins cannot be used.</p>
<p><pre>SQL&gt; EXPLAIN PLAN FOR SELECT * FROM t1p JOIN t2p USING (num, pkey);

SQL&gt; SELECT * FROM table(dbms_xplan.display(format=&gt;&#039;basic&#039;));

PLAN_TABLE_OUTPUT
------------------------------------

Plan hash value: 3059592055

------------------------------------
| Id  | Operation           | Name |
------------------------------------
|   0 | SELECT STATEMENT    |      |
|   1 |  HASH JOIN          |      |
|   2 |   PARTITION LIST ALL|      |
|   3 |    TABLE ACCESS FULL| T1P  |
|   4 |   PARTITION LIST ALL|      |
|   5 |    TABLE ACCESS FULL| T2P  |
------------------------------------</pre></p>
<p>The difference in the order of the partitions can also be confirmed by a query like the following one.</p>
<p><pre>SQL&gt; SELECT t1p.high_value,
  2         t1p.partition_position AS pos_t1p,
  3         t2p.partition_position AS pos_t2p,
  4         decode(t1p.partition_position, t2p.partition_position, &#039;Y&#039;, &#039;N&#039;) AS equal
  5  FROM user_tab_partitions t1p JOIN user_tab_partitions t2p ON t1p.partition_name = t2p.partition_name
  6  WHERE t1p.table_name = &#039;T1P&#039;
  7  AND t2p.table_name = &#039;T2P&#039;;

HIGH_VALUE   POS_T1P  POS_T2P EQUAL
----------- -------- -------- ------
0                  1        1 Y
1                  2        2 Y
2                  3        3 Y
3                  4        4 Y
5                  6        5 N
4                  5        6 N
6                  7        7 Y
7                  8        8 Y
8                  9        9 Y
9                 10       10 Y</pre></p>
<p>It goes without saying that to solve the problem it is necessary to reorder the partitions. To do so it is enough to move the out-of-order partitions. To avoid a double storage of the data a series of ALTER TABLE EXCHANGE/DROP/ADD/EXCHANGE statements can be used.</p>
<ul>
<li>Move the P5 partition of the T1P table</li>
</ul>
<p><pre>SQL&gt; CREATE TABLE t1p_5 AS
  2  SELECT *
  3  FROM t1p PARTITION (p_5)
  4  WHERE 1 = 0;

SQL&gt; ALTER TABLE t1p EXCHANGE PARTITION p_5 WITH TABLE t1p_5;

SQL&gt; ALTER TABLE t1p DROP PARTITION p_5;

SQL&gt; ALTER TABLE t1p ADD PARTITION p_5 VALUES (5);

SQL&gt; ALTER TABLE t1p EXCHANGE PARTITION p_5 WITH TABLE t1p_5;

SQL&gt; DROP TABLE t1p_5 PURGE;</pre></p>
<ul>
<li>Move the P5 partition of the T2P table</li>
</ul>
<p><pre>SQL&gt; CREATE TABLE t2p_5 AS
  2  SELECT *
  3  FROM t2p PARTITION (p_5)
  4  WHERE 1 = 0;

SQL&gt; ALTER TABLE t2p EXCHANGE PARTITION p_5 WITH TABLE t2p_5;

SQL&gt; ALTER TABLE t2p DROP PARTITION p_5;

SQL&gt; ALTER TABLE t2p ADD PARTITION p_5 VALUES (5);

SQL&gt; ALTER TABLE t2p EXCHANGE PARTITION p_5 WITH TABLE t2p_5;

SQL&gt; DROP TABLE t2p_5 PURGE;</pre></p>
<ul>
<li>Check whether the order is ok</li>
</ul>
<p><pre>SQL&gt; SELECT t1p.high_value,
  2         t1p.partition_position AS pos_t1p,
  3         t2p.partition_position AS pos_t2p,
  4         decode(t1p.partition_position, t2p.partition_position, &#039;Y&#039;, &#039;N&#039;) AS equal
  5  FROM user_tab_partitions t1p JOIN user_tab_partitions t2p ON t1p.partition_name = t2p.partition_name
  6  WHERE t1p.table_name = &#039;T1P&#039;
  7  AND t2p.table_name = &#039;T2P&#039;;

HIGH_VALUE   POS_T1P  POS_T2P EQUAL
----------- -------- -------- ------
0                  1        1 Y
1                  2        2 Y
2                  3        3 Y
3                  4        4 Y
4                  5        5 Y
6                  6        6 Y
7                  7        7 Y
8                  8        8 Y
9                  9        9 Y
5                 10       10 Y</pre></p>
<p>After these operations partition-wise joins are allowed. The following execution plan confirms this. </p>
<p><pre>SQL&gt; SELECT * FROM table(dbms_xplan.display(format=&gt;&#039;basic&#039;));

PLAN_TABLE_OUTPUT
------------------------------------

Plan hash value: 1324269388

------------------------------------
| Id  | Operation           | Name |
------------------------------------
|   0 | SELECT STATEMENT    |      |
|   1 |  PARTITION LIST ALL |      |
|   2 |   HASH JOIN         |      |
|   3 |    TABLE ACCESS FULL| T1P  |
|   4 |    TABLE ACCESS FULL| T2P  |
------------------------------------</pre></p>
]]></content:encoded>
			<wfw:commentRss>http://antognini.ch/2010/08/partition-wise-join-of-list-partitioned-tables/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>Does CREATE INDEX Gather Global Statistics?</title>
		<link>http://antognini.ch/2009/12/does-create-index-gather-global-statistics/</link>
		<comments>http://antognini.ch/2009/12/does-create-index-gather-global-statistics/#comments</comments>
		<pubDate>Thu, 17 Dec 2009 08:54:04 +0000</pubDate>
		<dc:creator>Christian Antognini</dc:creator>
				<category><![CDATA[10gR1]]></category>
		<category><![CDATA[10gR2]]></category>
		<category><![CDATA[11gR1]]></category>
		<category><![CDATA[11gR2]]></category>
		<category><![CDATA[9iR2]]></category>
		<category><![CDATA[Indexes]]></category>
		<category><![CDATA[Object Statistics]]></category>
		<category><![CDATA[Partitioning]]></category>

		<guid isPermaLink="false">http://antognini.ch/?p=792</guid>
		<description><![CDATA[You can add the COMPUTE STATISTICS clause to the CREATE INDEX statement. It instructs the SQL statement to gather and store index statistics in the data dictionary, while creating the index. This is useful because the overhead associated with the gathering of statistics while executing this SQL statement is negligible. In Oracle9i, the gathering of [...]]]></description>
			<content:encoded><![CDATA[<p>You can add the COMPUTE STATISTICS clause to the CREATE INDEX statement. It instructs the SQL statement to gather and store index statistics in the data dictionary, while creating the index. This is useful because the overhead associated with the gathering of statistics while executing this SQL statement is negligible. In Oracle9i, the gathering of statistics is performed only when this clause is specified. As of Oracle Database 10g, whenever statistics are not locked, their gathering is done by default, which means the COMPUTE STATISTICS clause is deprecated and available for backward compatibility only.</p>
<p>Unfortunately, CREATE INDEX does not gather global statistics. As a result, whenever you are creating partitioned indexes, the global statistics might be inaccurate. Let me show you an example:</p>
<ul>
<li>Create partitioned table, insert data (notice that the number of distinct values is equal to the number of rows) and create a local index</li>
</ul>
<p><pre>SQL&gt; CREATE TABLE t (n1 number, n2 number)
  2  PARTITION BY RANGE (n1) (
  3    PARTITION p1 VALUES LESS THAN (11),
  4    PARTITION p2 VALUES LESS THAN (21)
  5  );

Table created.

SQL&gt; INSERT INTO t
  2  SELECT rownum, rownum
  3  FROM dual
  4  CONNECT BY level &lt;= 20;

20 rows created.

SQL&gt; CREATE INDEX i ON t (n2) LOCAL;

Index created.</pre></p>
<ul>
<li>The CREATE INDEX statement gathered the statistics for the index; let&#8217;s check them&#8230;</li>
</ul>
<p><pre>SQL&gt; SELECT partition_name, global_stats, distinct_keys
  2  FROM user_ind_statistics
  3  WHERE index_name = &#039;I&#039;;

PARTITION_NAME GLOBAL_STATS DISTINCT_KEYS
-------------- ------------ -------------
               NO                      10
P1             NO                      10
P2             NO                      10</pre></p>
<p>As you can see 1) the number of distinct keys at the global level is wrong; it should be 20! 2) the GLOBAL_STATS column at the index level is set to NO. As a result, when you create a partitioned index, you should manually gather the global index statistics straight after. In other words, you should do the following:</p>
<ul>
<li>Manually gather global level index statistics</li>
</ul>
<p><pre>SQL&gt; execute dbms_stats.gather_index_stats(ownname=&gt;user, indname=&gt;&#039;i&#039;, granularity=&gt;&#039;global&#039;)

PL/SQL procedure successfully completed.</pre></p>
<ul>
<li>Check whether the index statistics are accurate</li>
</ul>
<p><pre>SQL&gt; SELECT partition_name, global_stats, distinct_keys
  2  FROM user_ind_statistics
  3  WHERE index_name = &#039;I&#039;;

PARTITION_NAME GLOBAL_STATS DISTINCT_KEYS
-------------- ------------ -------------
               YES                     20
P1             NO                      10
P2             NO                      10</pre></p>
<p>There are situations, however, where it is not necessary to manually gather the global index statistics. For example, when the index is prefixed. But, as a general rule, I would not rely on the automatically gathered statistics for partitioned indexes.</p>
]]></content:encoded>
			<wfw:commentRss>http://antognini.ch/2009/12/does-create-index-gather-global-statistics/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Zero-Size Unusable Indexes and the Query Optimizer</title>
		<link>http://antognini.ch/2009/11/zero-size-unusable-indexes-and-the-query-optimizer/</link>
		<comments>http://antognini.ch/2009/11/zero-size-unusable-indexes-and-the-query-optimizer/#comments</comments>
		<pubDate>Wed, 25 Nov 2009 19:25:03 +0000</pubDate>
		<dc:creator>Christian Antognini</dc:creator>
				<category><![CDATA[11gR2]]></category>
		<category><![CDATA[Indexes]]></category>
		<category><![CDATA[Partitioning]]></category>
		<category><![CDATA[Query Optimizer]]></category>

		<guid isPermaLink="false">http://antognini.ch/?p=760</guid>
		<description><![CDATA[Zero-size unusable indexes and index partions is a small but useful feature of Oracle Database 11g Release 2. Simply put, its aim is to save space in the database by immediately releasing the segment associated to unusable indexes or index partitions. To illustrate this, let&#8217;s have a look to an example…

Create a partitioned table, insert [...]]]></description>
			<content:encoded><![CDATA[<p><em>Zero-size unusable indexes and index partions</em> is a small but useful feature of Oracle Database 11g Release 2. Simply put, its aim is to save space in the database by immediately releasing the segment associated to unusable indexes or index partitions. To illustrate this, let&#8217;s have a look to an example…</p>
<ul>
<li>Create a partitioned table, insert data, create a local index and gather object statistics:</li>
</ul>
<p><pre>SQL&gt; CREATE TABLE t (
  2    id NUMBER NOT NULL,
  3    d DATE NOT NULL,
  4    n NUMBER NOT NULL,
  5    pad VARCHAR2(4000) NOT NULL
  6  )
  7  PARTITION BY RANGE (d) (
  8    PARTITION t_jan_2009 VALUES LESS THAN (to_date(&#039;2009-02-01&#039;,&#039;yyyy-mm-dd&#039;)),
  9    PARTITION t_feb_2009 VALUES LESS THAN (to_date(&#039;2009-03-01&#039;,&#039;yyyy-mm-dd&#039;)),
 10    PARTITION t_mar_2009 VALUES LESS THAN (to_date(&#039;2009-04-01&#039;,&#039;yyyy-mm-dd&#039;)),
 11    PARTITION t_apr_2009 VALUES LESS THAN (to_date(&#039;2009-05-01&#039;,&#039;yyyy-mm-dd&#039;)),
 12    PARTITION t_may_2009 VALUES LESS THAN (to_date(&#039;2009-06-01&#039;,&#039;yyyy-mm-dd&#039;)),
 13    PARTITION t_jun_2009 VALUES LESS THAN (to_date(&#039;2009-07-01&#039;,&#039;yyyy-mm-dd&#039;)),
 14    PARTITION t_jul_2009 VALUES LESS THAN (to_date(&#039;2009-08-01&#039;,&#039;yyyy-mm-dd&#039;)),
 15    PARTITION t_aug_2009 VALUES LESS THAN (to_date(&#039;2009-09-01&#039;,&#039;yyyy-mm-dd&#039;)),
 16    PARTITION t_sep_2009 VALUES LESS THAN (to_date(&#039;2009-10-01&#039;,&#039;yyyy-mm-dd&#039;)),
 17    PARTITION t_oct_2009 VALUES LESS THAN (to_date(&#039;2009-11-01&#039;,&#039;yyyy-mm-dd&#039;)),
 18    PARTITION t_nov_2009 VALUES LESS THAN (to_date(&#039;2009-12-01&#039;,&#039;yyyy-mm-dd&#039;)),
 19    PARTITION t_dec_2009 VALUES LESS THAN (to_date(&#039;2010-01-01&#039;,&#039;yyyy-mm-dd&#039;))
 20  );

SQL&gt; INSERT INTO t
  2  SELECT rownum, to_date(&#039;2009-01-01&#039;,&#039;yyyy-mm-dd&#039;)+rownum/274, mod(rownum,11), rpad(&#039;*&#039;,100,&#039;*&#039;)
  3  FROM dual
  4  CONNECT BY level &lt;= 100000;

SQL&gt; CREATE INDEX i ON t (d) LOCAL;

SQL&gt; execute dbms_stats.gather_table_stats(user,&#039;T&#039;)</pre></p>
<ul>
<li>Make all partitions but the last one unusable:</li>
</ul>
<p><pre>SQL&gt; ALTER INDEX i MODIFY PARTITION t_jan_2009 UNUSABLE;

SQL&gt; ALTER INDEX i MODIFY PARTITION t_feb_2009 UNUSABLE;

SQL&gt; ALTER INDEX i MODIFY PARTITION t_mar_2009 UNUSABLE;

SQL&gt; ALTER INDEX i MODIFY PARTITION t_apr_2009 UNUSABLE;

SQL&gt; ALTER INDEX i MODIFY PARTITION t_may_2009 UNUSABLE;

SQL&gt; ALTER INDEX i MODIFY PARTITION t_jun_2009 UNUSABLE;

SQL&gt; ALTER INDEX i MODIFY PARTITION t_jul_2009 UNUSABLE;

SQL&gt; ALTER INDEX i MODIFY PARTITION t_aug_2009 UNUSABLE;

SQL&gt; ALTER INDEX i MODIFY PARTITION t_sep_2009 UNUSABLE;

SQL&gt; ALTER INDEX i MODIFY PARTITION t_oct_2009 UNUSABLE;

SQL&gt; ALTER INDEX i MODIFY PARTITION t_nov_2009 UNUSABLE;</pre></p>
<ul>
<li>Check whether the segments associated to the unusable partitions still exist:</li>
</ul>
<p><pre>SQL&gt; SELECT partition_name, bytes
  2  FROM user_segments
  3  WHERE segment_name = &#039;I&#039;
  4  AND segment_type = &#039;INDEX PARTITION&#039;
  5  ORDER BY partition_name;

PARTITION_NAME       BYTES
--------------- ----------
T_DEC_2009          262144</pre></p>
<p>As you can see from the output of the last query, only the segment associated to the partition T_DEC_2009 exists. All other segments have been freed. (Note that up to Oracle Database 11g Release 1 all segments would still exist.)</p>
<p>This is nice but, in my opinion, there is a more important thing to consider&#8230;<br />
What does the query optimizer do when it has to generate the execution plan for a query that reads data stored into a table having unusable index partitions? For example, does the query optimizer take advantage of the usable partitions to apply a restriction? If yes, what happens when both partitions having usable and unusable index partitions have to be accessed? Let&#8217;s have a look to some examples…</p>
<ul>
<li>First, let’s check whether the usable index partition can be used to apply a restriction:</li>
</ul>
<p><pre>SQL&gt; SELECT count(d)
  2  FROM t
  3  WHERE d BETWEEN to_date(&#039;2009-12-01 23:00:00&#039;,&#039;yyyy-mm-dd hh24:mi:ss&#039;)
  4              AND to_date(&#039;2009-12-02 01:00:00&#039;,&#039;yyyy-mm-dd hh24:mi:ss&#039;);

SQL&gt; SELECT * FROM table(dbms_xplan.display_cursor(format=&gt;&#039;basic +partition&#039;));

--------------------------------------------------------
| Id  | Operation               | Name | Pstart| Pstop |
--------------------------------------------------------
|   0 | SELECT STATEMENT        |      |       |       |
|   1 |  SORT AGGREGATE         |      |       |       |
|   2 |   PARTITION RANGE SINGLE|      |    12 |    12 |
|   3 |    INDEX RANGE SCAN     | I    |    12 |    12 |
--------------------------------------------------------</pre></p>
<p>Nice, an index range scan can be performed.</p>
<p><br/></p>
<ul>
<li>Second, let’s check what happen when an unusable index partition would be accessed:</li>
</ul>
<p><pre>SQL&gt; SELECT count(d)
  2  FROM t
  3  WHERE d BETWEEN to_date(&#039;2009-11-01 23:00:00&#039;,&#039;yyyy-mm-dd hh24:mi:ss&#039;)
  4              AND to_date(&#039;2009-11-02 01:00:00&#039;,&#039;yyyy-mm-dd hh24:mi:ss&#039;);

SQL&gt; SELECT * FROM table(dbms_xplan.display_cursor(format=&gt;&#039;basic +partition&#039;));

--------------------------------------------------------
| Id  | Operation               | Name | Pstart| Pstop |
--------------------------------------------------------
|   0 | SELECT STATEMENT        |      |       |       |
|   1 |  SORT AGGREGATE         |      |       |       |
|   2 |   PARTITION RANGE SINGLE|      |    11 |    11 |
|   3 |    TABLE ACCESS FULL    | T    |    11 |    11 |
--------------------------------------------------------</pre></p>
<p>Obviously, a partition scan is performed. Note that this is only true if the initialization parameter SKIP_UNUSABLE_INDEXES is set to TRUE (this is the default). Otherwise an ORA-01502 would be generated.</p>
<p><br/></p>
<ul>
<li>Third, let’s check what happens when both usable and unusable index partitions would be accessed:</li>
</ul>
<p><pre>SQL&gt; SELECT count(d)
  2  FROM t
  3  WHERE d BETWEEN to_date(&#039;2009-11-30 23:00:00&#039;,&#039;yyyy-mm-dd hh24:mi:ss&#039;)
  4              AND to_date(&#039;2009-12-01 01:00:00&#039;,&#039;yyyy-mm-dd hh24:mi:ss&#039;);

SQL&gt; SELECT * FROM table(dbms_xplan.display_cursor(format=&gt;&#039;basic +partition&#039;));

-------------------------------------------------------------
| Id  | Operation                 | Name    | Pstart| Pstop |
-------------------------------------------------------------
|   0 | SELECT STATEMENT          |         |       |       |
|   1 |  SORT AGGREGATE           |         |       |       |
|   2 |   VIEW                    | VW_TE_2 |       |       |
|   3 |    UNION-ALL              |         |       |       |
|   4 |     PARTITION RANGE SINGLE|         |    12 |    12 |
|   5 |      INDEX RANGE SCAN     | I       |    12 |    12 |
|   6 |     PARTITION RANGE SINGLE|         |    11 |    11 |
|   7 |      TABLE ACCESS FULL    | T       |    11 |    11 |
-------------------------------------------------------------</pre></p>
<p>Good stuff! The query optimizer generates an execution plan containing a UNION ALL to takes advantage of the usable index partition. This is really interesting because it allows us to selectively remove unnecessary index partitions. For example, in case some indexes are only used for the “current” partition(s), the index partitions of the older ones could be set unusable. As a result, lot of space might be released.</p>
<p>Be careful, however, that the flexibility of the query optimizer has (still?) some limits. For example, in the following query the unusable partition is the one in the “middle”. In such a case, I was not able to let the query optimizer apply the optimization described above.</p>
<p><pre>SQL&gt; ALTER INDEX i REBUILD PARTITION t_oct_2009;

SQL&gt; SELECT count(d)
  2  FROM t
  3  WHERE d BETWEEN to_date(&#039;2009-10-30 23:00:00&#039;,&#039;yyyy-mm-dd hh24:mi:ss&#039;)
  4              AND to_date(&#039;2009-12-01 01:00:00&#039;,&#039;yyyy-mm-dd hh24:mi:ss&#039;);

SQL&gt; SELECT * FROM table(dbms_xplan.display_cursor(format=&gt;&#039;basic +partition&#039;));

----------------------------------------------------------
| Id  | Operation                 | Name | Pstart| Pstop |
----------------------------------------------------------
|   0 | SELECT STATEMENT          |      |       |       |
|   1 |  SORT AGGREGATE           |      |       |       |
|   2 |   PARTITION RANGE ITERATOR|      |    10 |    12 |
|   3 |    TABLE ACCESS FULL      | T    |    10 |    12 |
----------------------------------------------------------</pre></p>
]]></content:encoded>
			<wfw:commentRss>http://antognini.ch/2009/11/zero-size-unusable-indexes-and-the-query-optimizer/feed/</wfw:commentRss>
		<slash:comments>10</slash:comments>
		</item>
		<item>
		<title>Virtual Column-Based Partitioning Might Lead to Wrong Results</title>
		<link>http://antognini.ch/2009/02/virtual-column-based-partitioning-might-lead-to-wrong-results/</link>
		<comments>http://antognini.ch/2009/02/virtual-column-based-partitioning-might-lead-to-wrong-results/#comments</comments>
		<pubDate>Tue, 17 Feb 2009 06:53:31 +0000</pubDate>
		<dc:creator>Christian Antognini</dc:creator>
				<category><![CDATA[11gR1]]></category>
		<category><![CDATA[Bug]]></category>
		<category><![CDATA[Partitioning]]></category>

		<guid isPermaLink="false">http://antognini.ch/?p=77</guid>
		<description><![CDATA[As of Oracle Database 11g it is possible to use a virtual column as partition key. In this post I do not want to discuss how it works and whether this is good or not&#8230; Instead, I would like to show you that the feature might lead to wrong results. 
First of all, I would [...]]]></description>
			<content:encoded><![CDATA[<p>As of Oracle Database 11g it is possible to use a virtual column as partition key. In this post I do not want to discuss how it works and whether this is good or not&#8230; Instead, I would like to show you that the feature might lead to wrong results. </p>
<p>First of all, I would like to show you a test where everything works fine. For that purpose, let&#8217;s create a table (notice the virtual column n2), insert one row into it, and gather the object statistics:</p>
<p><pre>SQL&gt; CREATE TABLE t (
  2    n1 NUMBER,
  3    n2 AS (CASE n1 WHEN 1 THEN 1 WHEN 2 THEN 2 ELSE 0 END) VIRTUAL
  4  )
  5  PARTITION BY LIST (n2) (
  6    PARTITION zero VALUES (0),
  7    PARTITION one VALUES (1),
  8    PARTITION two VALUES (2)
  9  )
 10  ENABLE ROW MOVEMENT;

SQL&gt; INSERT INTO t (n1) VALUES (1);

SQL&gt; COMMIT;

SQL&gt; execute dbms_stats.gather_table_stats(user,&#039;t&#039;)</pre></p>
<p>The aim of the following test is to check whether row movement works correctly. Hence, I update the column n1 to cause such a movement. To check whether row movement is performed or not, I display the content of the two involved partitions before and after the update statement. In addition, I also display the rowids (because of the movement the row should get a new rowid).</p>
<p><pre>SQL&gt; SELECT rowid, n1, n2 FROM t PARTITION (zero);

no rows selected

SQL&gt; SELECT rowid, n1, n2 FROM t PARTITION (one);

ROWID                      N1         N2
------------------ ---------- ----------
AAAE89AAEAAAAGNAAA          1          1

SQL&gt; UPDATE t SET n1 = 3;

SQL&gt; COMMIT;

SQL&gt; SELECT rowid, n1, n2 FROM t PARTITION (zero);

ROWID                      N1         N2
------------------ ---------- ----------
AAAE88AAEAAAAF9AAA          3          0

SQL&gt; SELECT rowid, n1, n2 FROM t PARTITION (one);

no rows selected</pre></p>
<p>The previous test was successful. Now, let me show you a situation that leads to wrong results :-(</p>
<p>To reproduce the bug I basically execute the same operations as before. The only difference is that seven columns are added before the columns n1 and n2 in the table. Hence, the test table is recreated with the following statements:</p>
<p><pre>SQL&gt; DROP TABLE t PURGE;

SQL&gt; CREATE TABLE t (
  2    d1 NUMBER,
  3    d2 NUMBER,
  4    d3 NUMBER,
  5    d4 NUMBER,
  6    d5 NUMBER,
  7    d6 NUMBER,
  8    d7 NUMBER,
  9    n1 NUMBER,
 10    n2 AS (CASE n1 WHEN 1 THEN 1 WHEN 2 THEN 2 ELSE 0 END) VIRTUAL
 11  )
 12  PARTITION BY LIST (n2) (
 13    PARTITION zero VALUES (0),
 14    PARTITION one VALUES (1),
 15    PARTITION two VALUES (2)
 16  )
 17  ENABLE ROW MOVEMENT;

SQL&gt; INSERT INTO t (n1) VALUES (1);

SQL&gt; COMMIT;

SQL&gt; execute dbms_stats.gather_table_stats(user,&#039;t&#039;)</pre></p>
<p>As before, I update the row to cause the movement and display the content of the two involved partitions before and after doing it.</p>
<p><pre>SQL&gt; SELECT rowid, n1, n2 FROM t PARTITION (zero);

no rows selected

SQL&gt; SELECT rowid, n1, n2 FROM t PARTITION (one);

ROWID                      N1         N2
------------------ ---------- ----------
AAAE9BAAEAAAAGNAAA          1          1

SQL&gt; UPDATE t SET n1 = 3;

SQL&gt; COMMIT;

SQL&gt; SELECT rowid, n1, n2 FROM t PARTITION (zero);

no rows selected

SQL&gt; SELECT rowid, n1, n2 FROM t PARTITION (one);

ROWID                      N1         N2
------------------ ---------- ----------
AAAE9BAAEAAAAGNAAA          3          0</pre></p>
<p>As you can see, the two queries after the update statement return wrong results. Also the rowid is the same. Hence, row movement was not performed. It goes without saying that also other queries might return wrong results. An example is the following:</p>
<p><pre>SQL&gt; SELECT rowid, n1, n2 FROM t WHERE n2 = 1;

ROWID                      N1         N2
------------------ ---------- ----------
AAAE9BAAEAAAAGNAAA          3          0</pre></p>
<p>By playing around with the number of columns and position of the columns n1 and n2, I found out that depending on the situation you might have correct results or wrong results.</p>
<p>Since I was able to reproduce the problem with several databases (both 11.1.0.6 and 11.1.0.7), last Friday I opened a service request. Now the issue is tracked as bug# 8258501.</p>
]]></content:encoded>
			<wfw:commentRss>http://antognini.ch/2009/02/virtual-column-based-partitioning-might-lead-to-wrong-results/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>Granularity &#8216;APPROX_GLOBAL AND PARTITION&#8217;</title>
		<link>http://antognini.ch/2008/10/granularity-approx_global-and-partition/</link>
		<comments>http://antognini.ch/2008/10/granularity-approx_global-and-partition/#comments</comments>
		<pubDate>Fri, 03 Oct 2008 08:27:19 +0000</pubDate>
		<dc:creator>Christian Antognini</dc:creator>
				<category><![CDATA[11gR1]]></category>
		<category><![CDATA[Object Statistics]]></category>
		<category><![CDATA[Partitioning]]></category>

		<guid isPermaLink="false">http://antognini.ch/?p=47</guid>
		<description><![CDATA[The patchset 11.1.0.7 introduces in the package DBMS_STATS a new value for the parameter GRANULARITY. 
The description provided by the development team in $ORACLE_HOME/rdbms/admin/dbmsstat.sql is the following:
&#039;APPROX_GLOBAL AND PARTITION&#039; - This option is similar to
   &#039;GLOBAL AND PARTITION&#039;. But the global statistics are aggregated
   from partition level statistics. It will aggregate [...]]]></description>
			<content:encoded><![CDATA[<p>The patchset 11.1.0.7 introduces in the package DBMS_STATS a new value for the parameter GRANULARITY. </p>
<p>The description provided by the development team in $ORACLE_HOME/rdbms/admin/dbmsstat.sql is the following:</p>
<p><pre>&#039;APPROX_GLOBAL AND PARTITION&#039; - This option is similar to
   &#039;GLOBAL AND PARTITION&#039;. But the global statistics are aggregated
   from partition level statistics. It will aggregate all statistics except number of
   distinct values for columns and number of distinct keys of indexes.
   The existing histograms of the columns at the table level
   are also aggregated.The global statistics are  gathered
   (i.e., going back to GLOBAL AND PARTITION behaviour)
   if partname argument is null or if the aggregation cannot be done
   e.g., statistics of one of the partitions is missing.</pre><pre></pre></p>
<p>To illustrate how it works, let&#8217;s have a look to an example&#8230;</p>
<ul>
<li>Create a range-partitioned table with a local index, load some data and gather object statistics.</li>
</ul>
<p><pre>SQL&gt; CREATE TABLE t (
  2    id NUMBER,
  3    n NUMBER,
  4    d DATE,
  5    pad VARCHAR2(4000)
  6  )
  7  PARTITION BY RANGE (d) (
  8    PARTITION t_jan_2008 VALUES LESS THAN (to_date(&#039;2008-02-01&#039;,&#039;yyyy-mm-dd&#039;))
  9  )
 10  NOLOGGING;

SQL&gt; CREATE INDEX i ON t(n) LOCAL;

SQL&gt; INSERT /*+ append */ INTO t
  2  WITH
  3    t1000 AS (SELECT rownum AS dummy
  4              FROM dual
  5              CONNECT BY level &lt;=1000)
  6  SELECT 1000000+rownum,
  7         CASE WHEN rownum &lt;= 500 THEN mod(rownum,11) ELSE nullif(mod(rownum,2),1) END,
  8         to_date(&#039;2008-01-01&#039;,&#039;yyyy-mm-dd&#039;)+mod(rownum,31),
  9         rpad(&#039;*&#039;,100,&#039;*&#039;)
 10  FROM t1000, t1000
 11  WHERE rownum &lt;= 500000;

500000 rows created.

SQL&gt; COMMIT;

SQL&gt; BEGIN
  2    dbms_stats.gather_table_stats(
  3      ownname =&gt; user,
  4      tabname =&gt; &#039;t&#039;,
  5      estimate_percent =&gt; 100,
  6      granularity =&gt; &#039;global and partition&#039;,
  7      method_opt =&gt; &#039;for olumns size 1 id, d, pad, n size 254&#039;,
  8      cascade =&gt; TRUE
  9    );
 10  END;
 11  /</pre></p>
<ul>
<li>Add a new partition and load data into it.</li>
</ul>
<p><pre>SQL&gt; ALTER TABLE t
  2  ADD PARTITION t_feb_2008 VALUES LESS THAN (to_date(&#039;2008-03-01&#039;,&#039;yyyy-mm-dd&#039;));

SQL&gt; INSERT /*+ append */ INTO t
  2  WITH
  3    t1000 AS (SELECT rownum AS dummy
  4              FROM dual
  5              CONNECT BY level &lt;=1000)
  6  SELECT 2000000+rownum,
  7         CASE WHEN rownum &lt;= 500 THEN mod(rownum,12) ELSE nullif(mod(rownum,2),1) END,
  8         to_date(&#039;2008-02-01&#039;,&#039;yyyy-mm-dd&#039;)+mod(rownum,29),
  9         rpad(&#039;*&#039;,200,&#039;*&#039;)
 10  FROM t1000, t1000;

1000000 rows created.

SQL&gt; COMMIT;</pre></p>
<ul>
<li>Gather object statistics (notice that the new value for the parameter GRANULARITY is used).</li>
</ul>
<p><pre>SQL&gt; BEGIN
  2    dbms_stats.gather_table_stats(
  3      ownname =&gt; user,
  4      tabname =&gt; &#039;t&#039;,
  5      partname =&gt; &#039;t_feb_2008&#039;,
  6      estimate_percent =&gt; 100,
  7      granularity =&gt; &#039;approx_global and partition&#039;,
  8      method_opt =&gt; &#039;for columns size 1 id, d, pad, n size 254&#039;,
  9      cascade =&gt; TRUE
 10    );
 11  END;
 12  /</pre></p>
<ul>
<li>Compare the current object statistics with the previous ones.</li>
</ul>
<p><pre>SQL&gt; SELECT *
  2  FROM table(dbms_stats.diff_table_stats_in_history(
  3               ownname =&gt; user,
  4               tabname =&gt; &#039;t&#039;,
  5               time1 =&gt; localtimestamp,
  6               time2 =&gt; localtimestamp-to_dsinterval(&#039;0 00:00:15&#039;),
  7               pctthreshold =&gt; 0
  8             ));

STATISTICS DIFFERENCE REPORT FOR:
.................................

TABLE         : T
OWNER         : OPS$CHA
SOURCE A      : Statistics as of 21-SEP-08 06.39.37.597595 PM +02:00
SOURCE B      : Statistics as of 21-SEP-08 06.39.22.597595 PM +02:00
PCTTHRESHOLD  : 0
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

TABLE / (SUB)PARTITION STATISTICS DIFFERENCE:
.............................................

OBJECTNAME                  TYP SRC ROWS       BLOCKS     ROWLEN     SAMPSIZE
...............................................................................

T                           T   A   1500000    40091      183        500000
                                B   500000     8614       116        500000
T_FEB_2008                  P   A   1000000    31477      216        1000000
                                B   NO_STATS
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

COLUMN STATISTICS DIFFERENCE:
.............................

COLUMN_NAME     SRC NDV     DENSITY    HIST NULLS   LEN  MIN   MAX   SAMPSIZ
...............................................................................

D               A   31      .032258064 NO   0       8    786C0 786C0 1500000
                B   31      .032258064 NO   0       8    786C0 786C0 500000
ID              A   500000  .000002    NO   0       6    C4020 C404  1500000
                B   500000  .000002    NO   0       6    C4020 C4023 500000
N               A   11      .000001998 YES  749500  2    80    C10C  750500
                B   11      .000001998 YES  249750  2    80    C10B  250250
PAD             A   1       1          NO   0       168  2A2A2 2A2A2 1500000
                B   1       1          NO   0       101  2A2A2 2A2A2 500000

                              PARTITION: T_FEB_2008
                              .....................

D               A   29      .034482758 NO   0       8    786C0 786C0 1000000
                B   NO_STATS
ID              A   1000000 .000001    NO   0       6    C4030 C404  1000000
                B   NO_STATS
N               A   12      .000000999 YES  499750  2    80    C10C  500250
                B   NO_STATS
PAD             A   1       1          NO   0       201  2A2A2 2A2A2 1000000
                B   NO_STATS
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

INDEX / (SUB)PARTITION STATISTICS DIFFERENCE:
.............................................

OBJECTNAME      TYP SRC ROWS    LEAFBLK DISTKEY LF/KY DB/KY CLF     LVL SAMPSIZ
...............................................................................

                                    INDEX: I
                                    ........

I               I   A   750500  1221    12      101   3332  39991   2   750500
                    B   250250  407     11      37    778   8565    1   250250
T_FEB_2008      P   A   500250  814     12      67    2618  31426   2   500250
                    B   NO_STATS

</pre></p>
<p>A few remarks about the number of distinct keys&#8230;</p>
<p>As documented, the number of distinct values at table level has not been updated. I do understand that aggregating them is not possible. However, IMHO, it should be quite easy to do a kind of sanity check and to set the number of distinct values at the table level according to the highest number found at partition level. For example, in the previous case, the new partition contains 1M distinct values for the column ID. Hence, the number of distinct values for that column at the table level should be at least 1M.</p>
<p>Interestingly, the number of distinct values at the index level is correct. The reason is quite simple&#8230; they do gather them as usual (I checked that with SQL trace). In other words, the new value for the parameter GRANULARITY is only valid for tables. This is also the case when the procedure GATHER_INDEX_STATS is used. In other words, the new value for the parameter granularity is accepted but ignored!</p>
<p>What really puzzle me is why such a feature has been implemented in first place. In fact, as of Oracle Database 11g, with an incremental gathering is possible to have better statistics than with this feature.</p>
<p>Any suggestion/thought/opinion is welcome&#8230;</p>
]]></content:encoded>
			<wfw:commentRss>http://antognini.ch/2008/10/granularity-approx_global-and-partition/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Bloom Filters</title>
		<link>http://antognini.ch/2008/09/bloom-filters/</link>
		<comments>http://antognini.ch/2008/09/bloom-filters/#comments</comments>
		<pubDate>Fri, 26 Sep 2008 16:24:13 +0000</pubDate>
		<dc:creator>Christian Antognini</dc:creator>
				<category><![CDATA[10gR2]]></category>
		<category><![CDATA[11gR1]]></category>
		<category><![CDATA[Parallel Processing]]></category>
		<category><![CDATA[Partitioning]]></category>
		<category><![CDATA[Query Optimizer]]></category>

		<guid isPermaLink="false">http://antognini.ch/?p=50</guid>
		<description><![CDATA[Oracle Database, as of 10g Release 2, uses bloom filters in various situations. Unfortunately, no information about their usage is available in Oracle documentation. For this reason, I decided to write a paper to explain not only what bloom filters are, but also, and foremost, to describe how Oracle Database makes use of them. Specifically, [...]]]></description>
			<content:encoded><![CDATA[<p>Oracle Database, as of 10g Release 2, uses bloom filters in various situations. Unfortunately, no information about their usage is available in Oracle documentation. For this reason, I decided to write a paper to explain not only what bloom filters are, but also, and foremost, to describe how Oracle Database makes use of them. Specifically, to explain how the database engine uses bloom filters to reduce data communication between slave processes in parallel joins and to implement join-filter pruning. </p>
<p>Originally, I wrote this paper for the <a href="http://www.ioug.org/">IOUG</a> <a href="http://www.ioug.org/selectjournal/index.cfm">Select Journal</a>. Even if I wrote it last June, I wanted to receive the printed copies before putting it online. Today a packet with five copies of the Q4/2008 issue and a polo shirt arrived&#8230; Hence, it is now available online as well.</p>
<p>If you don&#8217;t have access to Select Journal, you can download it from <a href="/publications/">this page</a>.</p>
]]></content:encoded>
			<wfw:commentRss>http://antognini.ch/2008/09/bloom-filters/feed/</wfw:commentRss>
		<slash:comments>11</slash:comments>
		</item>
	</channel>
</rss>

