月別アーカイブ: 2015年2月

「オプティマイザ統計の保留」の検証(その4)

前回のおさらい

前々回は「オプティマイザ統計の保留」の基本的な動作を紹介しました。
前回は保留中の統計情報がどのように格納されるのかを探るために、必要な情報を整理しました。

簡単にまとめると

  • 保留中の統計上はSYSAUX表領域にあるSYS.WRI$_OPTSTAT_TAB_HISTORY表に格納される。(プライベート・エリア)
  • 実は統計情報の履歴もこのSYS.WRI$_OPTSTAT_TAB_HISTORY表に格納される。
  • オプティマイザで使用されるテーブルの統計情報はSYTEM表領域SYS.TAB$表に格納される。(ディクショナリ)

今回はこれらの領域に格納されている情報を確認するためのSQL文showstat.sqlを使用して、統計情報がどのように管理されているのかを可視化していきます。

1. 検証用テーブルの削除

まず最初に今までの検証で使用したテーブルを削除(DROP)し確認用SQLを実行してみます。

SQL> drop table test purge;

Table dropped.

SQL> @showstat

no rows selected

no rows selected

当然のことながら、テーブル自体がありませんので結果が返ってきません。

2. テーブル、インデックスの再作成と初期データインサート後のディクショナリの状況

検証用テーブルを作成し、テストデータを1,000件インサートし、インデックスを作成します。
その状態でディクショナリの状況を確認します。

SQL> create table test(col1 number,col2 varchar2(20));

Table created.

SQL> begin
2 for i in 1..1000 loop
3 insert into test values (i,'TEST DATA');
4 commit;
5 end loop;
6 end;
7 /

PL/SQL procedure successfully completed.

SQL> select count(*) from test;

  COUNT(*)
----------
      1000

SQL> create index test_idx on test (col1);

Index created.

SQL> @showstat

no rows selected

STAT    OWNER  TABLE_NAME    NUM_ROWS     BLOCKS AVG_ROW_LEN SAMPLE_SIZE LAST_ANALYZED
------- ------ ----------- ---------- ---------- ----------- ----------- -------------------
CURRENT TEST   TEST

TEST表はディクショナリに存在していますが、統計情報はまだ取得されていないのでLAST_ANALYZED列およびその他の列はNullとなっています(31行目)。

3. 統計情報の取得(1回目)

レコード件数1000件の状態で統計情報を取得します。この段階でのテーブル統計情報公開属性はデフォルトの「TRUE=公開」です。

SQL> begin
  2   DBMS_STATS.GATHER_TABLE_STATS('TEST','TEST');
  3  end;
  4  /

PL/SQL procedure successfully completed.

SQL> @showstat

STAT    OWNER  TABLE_NAME    NUM_ROWS     BLOCKS AVG_ROW_LEN SAMPLE_SIZE LAST_ANALYZED       SAVE_TIME
------- ------ ----------- ---------- ---------- ----------- ----------- ------------------- -------------------
PUBLISH TEST   TEST                                                                          2015-02-xx 12:30:29

STAT    OWNER  TABLE_NAME    NUM_ROWS     BLOCKS AVG_ROW_LEN SAMPLE_SIZE LAST_ANALYZED
------- ------ ----------- ---------- ---------- ----------- ----------- -------------------
CURRENT TEST   TEST              1000          5          14        1000 2015-02-xx 12:30:29

統計情報が取得され、即時(12:30:29)に公開されていることがわかります(16行目)。
SYS.WRI$_OPTSTAT_TAB_HISTORY表(プライベート・エリア)にはSAVE_TIME列のみに公開された日時が記録されています(12行目)。

4. テーブル統計情報公開属性を「FALSE=保留」に変更

SQL> begin
  2   DBMS_STATS.SET_TABLE_PREFS('TEST','TEST','PUBLISH','FALSE');
  3  end;
  4  /

PL/SQL procedure successfully completed.

SQL> col prefs for a10
SQL> select DBMS_STATS.GET_PREFS('PUBLISH','TEST','TEST') prefs from dual;

PREFS
----------
FALSE

SSQL> @showstat

STAT    OWNER  TABLE_NAME    NUM_ROWS     BLOCKS AVG_ROW_LEN SAMPLE_SIZE LAST_ANALYZED       SAVE_TIME
------- ------ ----------- ---------- ---------- ----------- ----------- ------------------- -------------------
PUBLISH TEST   TEST                                                                          2015-02-xx 12:30:29

STAT    OWNER  TABLE_NAME    NUM_ROWS     BLOCKS AVG_ROW_LEN SAMPLE_SIZE LAST_ANALYZED
------- ------ ----------- ---------- ---------- ----------- ----------- -------------------
CURRENT TEST   TEST              1000          5          14        1000 2015-02-xx 12:30:29

属性を変更するだけでは、プライベート・エリアおよびディクショナリに変化はありません。

5. テーブルのTRUNCATE

SQL> truncate table test;

Table truncated.

SQL> @showstat

STAT    OWNER  TABLE_NAME    NUM_ROWS     BLOCKS AVG_ROW_LEN SAMPLE_SIZE LAST_ANALYZED       SAVE_TIME
------- ------ ----------- ---------- ---------- ----------- ----------- ------------------- -------------------
PUBLISH TEST   TEST                                                                          2015-02-xx 12:30:29

STAT    OWNER  TABLE_NAME    NUM_ROWS     BLOCKS AVG_ROW_LEN SAMPLE_SIZE LAST_ANALYZED
------- ------ ----------- ---------- ---------- ----------- ----------- -------------------
CURRENT TEST   TEST              1000          5          14        1000 2015-02-xx 12:30:29

テーブルをTRUNCATEしても、プライベート・エリアおよびディクショナリの状態は変わりません。
これが統計情報公開属性によるものなのか、そもそものデフォルトの動作なのか、ここでは結論を出さないでおきます。

6. データ50,000件インサート

SQL> begin
  2  for i in 1..50000 loop
  3   insert into test values (i,'TEST DATA');
  4   commit;
  5  end loop;
  6  end;
  7  /

PL/SQL procedure successfully completed.

SQL> select count(*) from test;

COUNT(*)
----------
     50000

SQL> @showstat

STAT    OWNER  TABLE_NAME    NUM_ROWS     BLOCKS AVG_ROW_LEN SAMPLE_SIZE LAST_ANALYZED       SAVE_TIME
------- ------ ----------- ---------- ---------- ----------- ----------- ------------------- -------------------
PUBLISH TEST   TEST                                                                          2015-02-xx 12:30:29

STAT    OWNER  TABLE_NAME    NUM_ROWS     BLOCKS AVG_ROW_LEN SAMPLE_SIZE LAST_ANALYZED
------- ------ ----------- ---------- ---------- ----------- ----------- -------------------
CURRENT TEST   TEST              1000          5          14        1000 2015-02-xx 12:30:29

テーブルにレコードを50,000件インサートして確かに件数は確認できていますが、統計情報を取得していないのであくまでもこのテーブルの持つ統計情報は1,000件です。
従って、バッチ処理で大量のデータが作成されたとしても統計情報を取得しなければ、処理前の小量データ状態での統計情報がオプティマイザによって使用されます。これは非常に重要なポイントです。

7. 統計情報の取得(2回目)

レコード件数50,000件の状態で統計情報を取得します。そしてテーブル統計情報公開属性は「FALSE=保留」です。

SQL> begin
  2   DBMS_STATS.GATHER_TABLE_STATS('TEST','TEST');
  3  end;
  4  /

PL/SQL procedure successfully completed.

SQL> @showstat

STAT    OWNER  TABLE_NAME    NUM_ROWS     BLOCKS AVG_ROW_LEN SAMPLE_SIZE LAST_ANALYZED       SAVE_TIME
------- ------ ----------- ---------- ---------- ----------- ----------- ------------------- -------------------
PUBLISH TEST   TEST                                                                          2015-02-xx 12:30:29
PENDING TEST   TEST             50000        244          15       50000 2015-02-xx 12:33:39 3000-12-01 01:00:00

STAT    OWNER  TABLE_NAME    NUM_ROWS     BLOCKS AVG_ROW_LEN SAMPLE_SIZE LAST_ANALYZED
------- ------ ----------- ---------- ---------- ----------- ----------- -------------------
CURRENT TEST   TEST              1000          5          14        1000 2015-02-xx 12:30:29

ここがこの検証における最大のポイントです。
ハイライト表示させた13行目に注目すると、SAVE_TIME列が「3000-12-01 01:00:00」となった新しい行が作成されました。
この日時は現在日付よりも確実に未来日付です。Oracle11g以降はこの状態を「保留状態」の統計情報として管理しているようです。
そして、ディクショナリの方は全く変化がありません。つまり、実行計画に変化を及ぼすことはありません。

8. 現セッションのみ保留統計情報を使用できるようにする

SQL> alter session set OPTIMIZER_USE_PENDING_STATISTICS = TRUE;

Session altered.

SQL> @showstat

STAT    OWNER  TABLE_NAME    NUM_ROWS     BLOCKS AVG_ROW_LEN SAMPLE_SIZE LAST_ANALYZED       SAVE_TIME
------- ------ ----------- ---------- ---------- ----------- ----------- ------------------- -------------------
PUBLISH TEST   TEST                                                                          2015-02-xx 12:30:29
PENDING TEST   TEST             50000        244          15       50000 2015-02-xx 12:33:39 3000-12-01 01:00:00

STAT    OWNER  TABLE_NAME    NUM_ROWS     BLOCKS AVG_ROW_LEN SAMPLE_SIZE LAST_ANALYZED
------- ------ ----------- ---------- ---------- ----------- ----------- -------------------
CURRENT TEST   TEST              1000          5          14        1000 2015-02-xx 12:30:29

OPTIMIZER_USE_PENDING_STATISTICS初期化パラメータをセッション単位で変更しましたが、ディクショナリには変化はありません。
つまり、このパラメータをTRUEにしてもオプティマイザは一時的に保留統計情報を使うだけです。

9. OPTIMIZER_USE_PENDING_STATISTICS初期化パラメータをデフォルトに戻す

保留統計情報を公開する前に、OPTIMIZER_USE_PENDING_STATISTICS初期化パラメータをデフォルトに戻しておきます。

SQL> conn test/test
Connected.
SQL> show parameter OPTIMIZER_USE_PENDING_STATISTICS

NAME                                 TYPE        VALUE
------------------------------------ ----------- ------------------------------
optimizer_use_pending_statistics     boolean     FALSE
SQL> @showstat

STAT    OWNER  TABLE_NAME    NUM_ROWS     BLOCKS AVG_ROW_LEN SAMPLE_SIZE LAST_ANALYZED       SAVE_TIME
------- ------ ----------- ---------- ---------- ----------- ----------- ------------------- -------------------
PUBLISH TEST   TEST                                                                          2015-02-xx 12:30:29
PENDING TEST   TEST             50000        244          15       50000 2015-02-xx 12:33:39 3000-12-01 01:00:00

STAT    OWNER  TABLE_NAME    NUM_ROWS     BLOCKS AVG_ROW_LEN SAMPLE_SIZE LAST_ANALYZED
------- ------ ----------- ---------- ---------- ----------- ----------- -------------------
CURRENT TEST   TEST              1000          5          14        1000 2015-02-xx 12:30:29

念のためプライベート・エリア、ディクショナリ共に変化がないことを確認しておきます。

10. 保留統計情報を公開する

ここが次のポイントです。
保留統計情報を手動で公開し、状況を確認します。

SQL> exec DBMS_STATS.PUBLISH_PENDING_STATS('TEST','TEST');

PL/SQL procedure successfully completed.

SQL> @showstat

STAT    OWNER  TABLE_NAME    NUM_ROWS     BLOCKS AVG_ROW_LEN SAMPLE_SIZE LAST_ANALYZED       SAVE_TIME
------- ------ ----------- ---------- ---------- ----------- ----------- ------------------- -------------------
PUBLISH TEST   TEST                                                                          2015-02-xx 12:30:29
PUBLISH TEST   TEST              1000          5          14        1000 2015-02-xx 12:30:29 2015-02-xx 12:38:27

STAT    OWNER  TABLE_NAME    NUM_ROWS     BLOCKS AVG_ROW_LEN SAMPLE_SIZE LAST_ANALYZED
------- ------ ----------- ---------- ---------- ----------- ----------- -------------------
CURRENT TEST   TEST             50000        244          15       50000 2015-02-xx 12:33:39

いろいろなところが同時に更新されているので整理します。

  1. ディクショナリには50,000件の統計情報が公開されました(14行目)。統計情報が取得された時刻「12:33:39」(LAST_ANALYZED列)はDBMS_STATS.GATHER_TABLE_STATSが実行された時刻です。
  2. プライベート・エリアの最終行(10行目)はSAVE_TIME列が「12:38:27」に更新されており、これはDBMS_STATS.PUBLISHが実行された時刻です。そして過去時刻となったことで保留統計情報ではなく公開統計情報の履歴となっていることがわかります。
  3. さらに、この行は1,000行データの統計情報となっていることがわかります。(LAST_ANALYZED=12:30:27)
  4.  そして、統計情報を取得する前の状態も保存されています。(9行目、SAVE_TIME=12:30:29)

11. レコードを削除し50,000→1,000件とした後に統計情報を取得する(3回目)。

50,000件の状態から49,000件を削除します。その後統計情報を取得します。

SQL> delete from test where col1 > 1000;

49000 rows deleted.

SQL> select count(*) from test;

  COUNT(*)
----------
      1000

SQL> @showstat

STAT    OWNER  TABLE_NAME    NUM_ROWS     BLOCKS AVG_ROW_LEN SAMPLE_SIZE LAST_ANALYZED       SAVE_TIME
------- ------ ----------- ---------- ---------- ----------- ----------- ------------------- -------------------
PUBLISH TEST   TEST                                                                          2015-02-xx 12:30:29
PUBLISH TEST   TEST              1000          5          14        1000 2015-02-xx 12:30:29 2015-02-xx 12:38:27

STAT    OWNER  TABLE_NAME    NUM_ROWS     BLOCKS AVG_ROW_LEN SAMPLE_SIZE LAST_ANALYZED
------- ------ ----------- ---------- ---------- ----------- ----------- -------------------
CURRENT TEST   TEST             50000        244          15       50000 2015-02-xx 12:33:39

SQL> begin
  2   DBMS_STATS.GATHER_TABLE_STATS('TEST','TEST');
  3  end;
  4  /

PL/SQL procedure successfully completed.

SQL> @showstat

STAT    OWNER  TABLE_NAME    NUM_ROWS     BLOCKS AVG_ROW_LEN SAMPLE_SIZE LAST_ANALYZED       SAVE_TIME
------- ------ ----------- ---------- ---------- ----------- ----------- ------------------- -------------------
PUBLISH TEST   TEST                                                                          2015-02-xx 12:30:29
PUBLISH TEST   TEST              1000          5          14        1000 2015-02-xx 12:30:29 2015-02-xx 12:38:27
PENDING TEST   TEST              1000        244          14        1000 2015-02-xx 12:41:37 3000-12-01 01:00:00

STAT    OWNER  TABLE_NAME    NUM_ROWS     BLOCKS AVG_ROW_LEN SAMPLE_SIZE LAST_ANALYZED
------- ------ ----------- ---------- ---------- ----------- ----------- -------------------
CURRENT TEST   TEST             50000        244          15       50000 2015-02-xx 12:33:39

DELETEしただけでは統計情報に変化はありません。DBMS_STATS.GATHER_TABLE_STATSを実行して統計情報を新たに取得しますが(23行目-)、統計情報公開属性が「FALSE」のままなのでディクショナリには変化はありません(41行目)。
プライベート・エリアの方は「SAVE_TIME=”3000-12-01 01:00:00″」となる1,000件データの保留統計情報が追加されていることがわかります(36行目)。

12. 統計情報を公開する

SQL> exec DBMS_STATS.PUBLISH_PENDING_STATS('TEST','TEST');

PL/SQL procedure successfully completed.

SQL> @showstat 

STAT    OWNER  TABLE_NAME    NUM_ROWS     BLOCKS AVG_ROW_LEN SAMPLE_SIZE LAST_ANALYZED       SAVE_TIME
------- ------ ----------- ---------- ---------- ----------- ----------- ------------------- -------------------
PUBLISH TEST   TEST                                                                          2015-02-xx 12:30:29
PUBLISH TEST   TEST              1000          5          14        1000 2015-02-xx 12:30:29 2015-02-xx 12:38:27
PUBLISH TEST   TEST             50000        244          15       50000 2015-02-xx 12:33:39 2015-02-xx 12:42:22

STAT    OWNER  TABLE_NAME    NUM_ROWS     BLOCKS AVG_ROW_LEN SAMPLE_SIZE LAST_ANALYZED
------- ------ ----------- ---------- ---------- ----------- ----------- -------------------
CURRENT TEST   TEST              1000        244          14        1000 2015-02-xx 12:41:37

1,000件データの保留統計情報はディクショナリに反映されましたが、公開された統計情報の履歴にも1件追加されました(11行目)。
統計情報を取得し公開する度に統計情報の履歴は増えていきます。

13. 統計情報公開属性を「TRUE」に戻し、テーブルをTRUNCATEする。

SQL> begin
  2   DBMS_STATS.SET_TABLE_PREFS('TEST','TEST','PUBLISH','TRUE');
  3  end;
  4  /

PL/SQL procedure successfully completed.

SQL> col prefs for a10
SQL> select DBMS_STATS.GET_PREFS('PUBLISH','TEST','TEST') prefs from dual;

PREFS
----------
TRUE

SQL> @showstat

STAT    OWNER  TABLE_NAME    NUM_ROWS     BLOCKS AVG_ROW_LEN SAMPLE_SIZE LAST_ANALYZED       SAVE_TIME
------- ------ ----------- ---------- ---------- ----------- ----------- ------------------- -------------------
PUBLISH TEST   TEST                                                                          2015-02-xx 12:30:29
PUBLISH TEST   TEST              1000          5          14        1000 2015-02-xx 12:30:29 2015-02-xx 12:38:27
PUBLISH TEST   TEST             50000        244          15       50000 2015-02-xx 12:33:39 2015-02-xx 12:42:22

STAT    OWNER  TABLE_NAME    NUM_ROWS     BLOCKS AVG_ROW_LEN SAMPLE_SIZE LAST_ANALYZED
------- ------ ----------- ---------- ---------- ----------- ----------- -------------------
CURRENT TEST   TEST              1000        244          14        1000 2015-02-xx 12:41:37

SQL> truncate table test;

Table truncated.

SQL> select count(*) from test;

  COUNT(*)
----------
         0

SQL> @showstat

STAT    OWNER  TABLE_NAME    NUM_ROWS     BLOCKS AVG_ROW_LEN SAMPLE_SIZE LAST_ANALYZED       SAVE_TIME
------- ------ ----------- ---------- ---------- ----------- ----------- ------------------- -------------------
PUBLISH TEST   TEST                                                                          2015-02-xx 12:30:29
PUBLISH TEST   TEST              1000          5          14        1000 2015-02-xx 12:30:29 2015-02-xx 12:38:27
PUBLISH TEST   TEST             50000        244          15       50000 2015-02-xx 12:33:39 2015-02-xx 12:42:22

STAT    OWNER  TABLE_NAME    NUM_ROWS     BLOCKS AVG_ROW_LEN SAMPLE_SIZE LAST_ANALYZED
------- ------ ----------- ---------- ---------- ----------- ----------- -------------------
CURRENT TEST   TEST              1000        244          14        1000 2015-02-xx 12:41:37

テーブルのTRUNCATEでレコード件数は0件になったはずですが、ディクショナリの統計情報は1,000件データのままです。
即ち、統計情報公開属性に関わらず(デフォルトのままでも)TRUNCATE後もディクショナリ統計情報はそのままです。

14. 統計情報を取得する(4回目)。

SQL> begin
  2   DBMS_STATS.GATHER_TABLE_STATS('TEST','TEST');
  3  end;
  4  /

PL/SQL procedure successfully completed.

SQL> @showstat

STAT    OWNER  TABLE_NAME    NUM_ROWS     BLOCKS AVG_ROW_LEN SAMPLE_SIZE LAST_ANALYZED       SAVE_TIME
------- ------ ----------- ---------- ---------- ----------- ----------- ------------------- -------------------
PUBLISH TEST   TEST                                                                          2015-02-xx 12:30:29
PUBLISH TEST   TEST              1000          5          14        1000 2015-02-xx 12:30:29 2015-02-xx 12:38:27
PUBLISH TEST   TEST             50000        244          15       50000 2015-02-xx 12:33:39 2015-02-xx 12:42:22
PUBLISH TEST   TEST              1000        244          14        1000 2015-02-xx 12:41:37 2015-02-xx 12:56:07

STAT    OWNER  TABLE_NAME    NUM_ROWS     BLOCKS AVG_ROW_LEN SAMPLE_SIZE LAST_ANALYZED
------- ------ ----------- ---------- ---------- ----------- ----------- -------------------
CURRENT TEST   TEST                 0          0           0           0 2015-02-xx 12:56:07

TRUNCATEした0件データの状態で最後の統計情報を取得します。
統計情報習得と同時に即時に公開されます。

以上のように、SAVE_TIME列の日付を巧みに使い分けることによって、統計情報の保留と履歴が上手く管理されていることがわかります。
データ・ディクショナリ・ビューの定義を解析することで統計情報管理の秘密を解明することができました。

ところで公開された統計情報は履歴がどんどん溜まっていきますが、保留中の統計情報は履歴が記録されるのでしょうか?
それから、「統計情報の保留」と「統計情報のロック」は違うものなのでしょうか?

検証して新たな疑問点がいくつか出てきましたので次回さらに検証を続けたいと思います。

続く

「オプティマイザ統計の保留」の検証(その3)

前回のおさらい

前回は「オプティマイザ統計の保留」をシナリオに沿って動作を検証しました。

テーブルの統計情報公開属性を「FALSE」(=保留)に設定すれば、統計情報を取得したとしても現在の実行計画に影響を与えず、保留状態に置かれます。

そして、保留状態の統計情報を手動で公開すれば、次回実行計画を算出されるタイミングでその公開された統計情報が使用されて新しい実行計画が作成されることを確認しました。

この機能の概要は前々回の記事「オプティマイザ統計の保留」の検証(その1)で紹介しましたが、別のマニュアルにある記事も紹介しておきます。

Oracle® Database PL/SQLパッケージおよびタイプ・リファレンス
11g リリース2(11.2)
B56262-06
保留中の統計情報

「保留中の統計情報」のポイント

  • デフォルトでは収集された統計情報はディクショナリに格納される。
  • テーブルのPUBLISHオプション(統計情報公開属性)を「FALSE」に設定すると、統計情報はディクショナリではなくシステムのプライベート・エリアに格納される。
  • プライベート・エリアに格納された統計情報は原則的にはコストベース・オプティマイザ(CBO)で使用されない。
  • パラメータOPTIMIZER_USE_PENDING_STATISTICSをセッション(またはシステム)レベルで「TRUE」に設定変更すると、保留中の統計情報が新しい実行計画に影響を与えるか否かの確認を行うことができる。

プライベート・エリアはどこにあるのか?

上のポイントに出てきた保留中の統計情報が格納されるプライベート・エリアとは一体どこなのでしょうか?
前回の検証で、保留中の統計情報はデータ・ディクショナリ・ビュー「XXX_TAB_PENDING_STATS」で参照することを紹介しました。
まずは、このビュー定義からどのような実表に情報が格納されるのかを見ていきましょう。

ビュー定義を確認する

ビュー定義はDBA_VIEWSのTEXT列を確認することで得られます。以下はDBA_TAB_PENDING_STATSのビュー定義です。

SQL> set long 2000000000
SQL> set pages 200
SQL> select TEXT from dba_views where VIEW_NAME = 'DBA_TAB_PENDING_STATS';

TEXT
--------------------------------------------------------------------------------
select u.name, o.name, null, null, h.rowcnt, h.blkcnt, h.avgrln,
         h.samplesize, h.analyzetime
  from   sys.user$ u, sys.obj$ o, sys.wri$_optstat_tab_history h
  where  h.obj# = o.obj# and o.type# = 2 and o.owner# = u.user#
    and  h.savtime > systimestamp
  union all
  -- partitions
  select u.name, o.name, o.subname, null, h.rowcnt, h.blkcnt,
         h.avgrln, h.samplesize, h.analyzetime
  from   sys.user$ u, sys.obj$ o, sys.wri$_optstat_tab_history h
  where  h.obj# = o.obj# and o.type# = 19 and o.owner# = u.user#
    and  h.savtime > systimestamp
  union all
  -- sub partitions
  select u.name, osp.name, ocp.subname, osp.subname, h.rowcnt,
         h.blkcnt, h.avgrln, h.samplesize, h.analyzetime
  from  sys.user$ u,  sys.obj$ osp, obj$ ocp,  sys.tabsubpart$ tsp,
        sys.wri$_optstat_tab_history h
  where h.obj# = osp.obj# and osp.type# = 34 and osp.obj# = tsp.obj# and
        tsp.pobj# = ocp.obj# and osp.owner# = u.user#
    and h.savtime > systimestamp

このビューは3つの部分(テーブル、パーティション、サブパーティション)の問い合わせから成っていることがわかります。
今回の検証ではテーブルのみを対象とするので、1番最初のパートに着目します。
それから、ハイライト表示した11行目の「h.savtime > systimestamp」というWHERE条件はこの先重要になりますので覚えておいてください。

さて、保留中の統計情報はWRI$_OPTSTAT_TAB_HISTORY(所有者はSYSユーザ)という実表に格納されるようです。
このテーブルの構成を確認しておきましょう。

SQL> desc sys.wri$_optstat_tab_history
 名前               NULL?    型
 ----------------- -------- ----------------------------
 OBJ#              NOT NULL NUMBER
 SAVTIME                    TIMESTAMP(6) WITH TIME ZONE
 FLAGS                      NUMBER
 ROWCNT                     NUMBER
 BLKCNT                     NUMBER
 AVGRLN                     NUMBER
 SAMPLESIZE                 NUMBER
 ANALYZETIME                DATE
 CACHEDBLK                  NUMBER
 CACHEHIT                   NUMBER
 LOGICALREAD                NUMBER
 SPARE1                     NUMBER
 SPARE2                     NUMBER
 SPARE3                     NUMBER
 SPARE4                     VARCHAR2(1000)
 SPARE5                     VARCHAR2(1000)
 SPARE6                     TIMESTAMP(6) WITH TIME ZONE

またこのテーブルが格納されている表領域を確認しておきます。

SQL> select OWNER,TABLE_NAME,TABLESPACE_NAME
  2  from dba_tables
  3  where OWNER = 'SYS' and TABLE_NAME = 'WRI$_OPTSTAT_TAB_HISTORY';

OWNER  TABLE_NAME                     TABLESPACE_NAME
------ ------------------------------ ----------------
SYS    WRI$_OPTSTAT_TAB_HISTORY       SYSAUX

SYSAUX表領域であることがわかります。つまり「プライベート・エリア」というのはSYSAUX表領域にあるSYS.WRI$_OPTSTAT_TAB_HISTORY表のようです。

実は、このSYS.WRI$_OPTSTAT_TAB_HISTORYを元表とするビューは他にも存在します。
以下は、DBA_TAB_STATS_HISTORYビューの定義を確認したところです。

SQL> select TEXT from dba_views where VIEW_NAME = 'DBA_TAB_STATS_HISTORY';

TEXT
--------------------------------------------------------------------------------
select u.name, o.name, null, null, h.savtime
  from   sys.user$ u, sys.obj$ o, sys.wri$_optstat_tab_history h
  where  h.obj# = o.obj# and o.type# = 2 and o.owner# = u.user#
    and  h.savtime <= systimestamp  -- exclude pending statistics
  union all
  -- partitions
  select u.name, o.name, o.subname, null, h.savtime
  from   sys.user$ u, sys.obj$ o, sys.wri$_optstat_tab_history h
  where  h.obj# = o.obj# and o.type# = 19 and o.owner# = u.user#
    and  h.savtime <= systimestamp  -- exclude pending statistics
  union all
  -- sub partitions
  select u.name, osp.name, ocp.subname, osp.subname, h.savtime
  from  sys.user$ u,  sys.obj$ osp, obj$ ocp,  sys.tabsubpart$ tsp,
        sys.wri$_optstat_tab_history h
  where h.obj# = osp.obj# and osp.type# = 34 and osp.obj# = tsp.obj#
    and tsp.pobj# = ocp.obj# and osp.owner# = u.user#
    and h.savtime <= systimestamp  -- exclude pending statistics
  union all
  -- fixed tables
  select 'SYS', t.kqftanam, null, null, h.savtime
  from  sys.x$kqfta t, sys.wri$_optstat_tab_history h
  where t.kqftaobj = h.obj#
    and h.savtime <= systimestamp  -- exclude pending statistics

驚いたことに、DBA_TAB_PENDING_STATSビューと定義がほぼ同じで、8行目のWHERE条件が正反対になっています。

実は
13.5.3 前のバージョンの統計のリストア
Oracle® Databaseパフォーマンス・チューニング・ガイド
11gリリース2 (11.2)
B56312-06

に出てくる統計情報の履歴管理と、統計情報の保留はほぼ同じ仕組みで実装されているようです。

そこで、以下の「保留中の統計情報」と「統計情報の履歴」を同時に確認できるSQL文を使って、統計情報取得に伴う情報の管理のされ方を追いかけて行きたいと思います。

showstat.sql

COLUMN TABLE_NAME FORMAT a11
COLUMN OWNER      FORMAT a6
select
  case when h.savtime >  systimestamp
        then 'PENDING'
       when h.savtime <= systimestamp
        then 'PUBLISH'
  end as        STAT
, u.name        OWNER
, o.name        TABLE_NAME
, h.rowcnt      NUM_ROWS
, h.blkcnt      BLOCKS
, h.avgrln      AVG_ROW_LEN
, h.samplesize  SAMPLE_SIZE
, to_char(h.analyzetime,'yyyy-mm-dd hh24:mi:ss') LAST_ANALYZED
, to_char(h.savtime,'yyyy-mm-dd hh24:mi:ss')     SAVE_TIME
From
  sys.user$                     u
, sys.obj$                      o
, sys.wri$_optstat_tab_history  h
where h.obj#    = o.obj#
and   o.type#   = 2
and   o.owner#  = u.user#
and   u.name    = 'TEST'
and   o.name    like 'TEST%'
;
select
  'CURRENT'     STAT
, u.name        OWNER
, o.name        TABLE_NAME
, t.rowcnt      NUM_ROWS
, t.blkcnt      BLOCKS
, t.avgrln      AVG_ROW_LEN
, t.samplesize  SAMPLE_SIZE
, to_char(t.analyzetime,'yyyy-mm-dd hh24:mi:ss') LAST_ANALYZED
from
  sys.user$ u
, sys.tab$  t
, sys.obj$  o
where o.owner#  = u.user#
and   o.obj#    = t.obj#
and   u.name    = 'TEST'
and   o.name    like 'TEST%'
/

解説:
4-8行目のCASE式により、h.savtime列の値が現在日時に対して大小関係がどうなっているかで表示を変えています。
また27行目以降は、現在オプティマイザで使用されているディクショナリ(SYS.TAB$)に格納された統計情報の状況を確認するための2番目のクエリーです。

ちなみにSYS.TAB$は、以下のとおりSYSTEM表領域に格納されるディクショナリ表です。

SQL> select OWNER,TABLE_NAME,TABLESPACE_NAME
  2  from dba_tables
  3  where OWNER = 'SYS' and TABLE_NAME = 'TAB$';

OWNER  TABLE_NAME  TABLESPACE_NAME
------ ----------- ----------------
SYS    TAB$        SYSTEM

長くなったので、検証結果は次回に紹介します。

続く

「オプティマイザ統計の保留」の検証(その2)

検証開始!

前回示した検証シナリオに沿って、「オプティマイザ統計の保留」の基本的な機能を確認していきます。

1. 検証準備(スキーマの作成)

ここでは検証用スキーマとして「TEST」を作成し、必要な権限を与えます。特にデータ・ディクショナリを確認できるように「SELECT ANY DICTIONARY」権限を与えておきます。

SQL> create user test identified by "test" default tablespace users temporary tablespace temp;

User created.

SQL> grant connect,resource to test;

Grant succeeded.

SQL> grant select any dictionary to test;

Grant succeeded.

SQL> grant plustrace to test;

Grant succeeded.

2. テーブル作成、データ作成、インデックス作成および統計情報の取得

簡単な構造の検証用テーブル「TEST」を作成、データを1,000件インサートし、さらにインデックスを作成します。
その後、統計情報を取得します。

SQL> conn test/test
Connected.

SQL> create table test(col1 number,col2 varchar2(20));

Table created.

SQL> desc test
 Name                  Null?    Type
 --------------------- -------- ------------------
 COL1                           NUMBER
 COL2                           VARCHAR2(20)

SQL> begin
  2 for i in 1..1000 loop
  3   insert into test values (i,'TEST DATA');
  4   commit;
  5 end loop;
  6 end;
  7 /

PL/SQL procedure successfully completed.

SQL> select count(*) from test;

  COUNT(*)
----------
      1000

SQL> create index test_idx on test (col1);

Index created.

SQL> begin
  2  DBMS_STATS.GATHER_TABLE_STATS('TEST','TEST');
  3 end;
  4 /

PL/SQL procedure successfully completed.

SQL> select to_char(last_analyzed,'hh24:mi:ss') last_analyzed, num_rows from user_tables
  2  where table_name = 'TEST';

LAST_ANA   NUM_ROWS
-------- ----------
20:44:35       1000

3. 実行計画の確認

それではこの状態で、以下の問い合わせを実行してみましょう。(この後同じ問い合わせを実行し、実行計画がどのようになるかを確認します。)

SQL> set autot traceonly
SQL> set lines 200
SQL> select * from test where col1 > 999;

Execution Plan
----------------------------------------------------------
Plan hash value: 1389866015

----------------------------------------------------------------------------------------
| Id  | Operation                   | Name     | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |          |     1 |    14 |     3   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID| TEST     |     1 |    14 |     3   (0)| 00:00:01 |
|*  2 |   INDEX RANGE SCAN          | TEST_IDX |     1 |       |     2   (0)| 00:00:01 |
----------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("COL1">999)

Statistics
----------------------------------------------------------
          8  recursive calls
          0  db block gets
         15  consistent gets
          0  physical reads
          0  redo size
        625  bytes sent via SQL*Net to client
        520  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          3  sorts (memory)
          0  sorts (disk)
          1  rows processed

SQL> set autot off

INDEX RANGE SCAN」が選択され、1件のデータを取得していることがわかります。

4. テーブルの統計情報公開属性を「FALSE=保留」に設定変更する。

デフォルトではテーブルの統計情報公開属性は「TRUE=公開」になっています。
以下のプロシージャにより属性を「FALSE=保留」に設定変更します。
(テーブルの属性を変更しますが「ALTER TABLE」文を使用しないことに注意してください。)

SQL> begin
  2   DBMS_STATS.SET_TABLE_PREFS('TEST','TEST','PUBLISH','FALSE');
  3  end;
  4  /

PL/SQL procedure successfully completed.

SQL> col prefs for a10
SQL> select DBMS_STATS.GET_PREFS('PUBLISH','TEST','TEST') prefs from dual;

PREFS
----------
FALSE

5. テーブルをTRUNCATE後、データを50,000件インサートする。

SQL> truncate table test;

Table truncated.

SQL> begin
  2  for i in 1..50000 loop
  3   insert into test values (i,'TEST DATA');
  4   commit;
  5  end loop;
  6  end;
  7  /

PL/SQL procedure successfully completed.

SQL> select count(*) from test;

  COUNT(*)
----------
     50000

6. 統計情報を取得し、内容を確認する。

デフォルトではテーブルをTRUNCATEした時点で、統計情報も削除されます。その後データを50,000件インサートしても統計情報を再取得しない限り統計情報は「なし」のままです。
「オプティマイザ統計の保留」が有効になっている状態で、統計情報がどうなるかを確認しましょう。

SQL> begin
  2   DBMS_STATS.GATHER_TABLE_STATS('TEST','TEST');
  3  end;
  4  /

PL/SQL procedure successfully completed.

SQL> select to_char(last_analyzed,'hh24:mi:ss') last_analyzed, num_rows from user_tables
  2  where table_name = 'TEST';

LAST_ANA   NUM_ROWS
-------- ----------
20:44:35       1000

非常に興味深い結果となりましたが、TRUNCATEする前の統計情報がそのまま「保留」されていることがわかります。
TRUNCATEではデータを削除するのではなく、同じ構造で空のテーブルを再作成するような内部動作が行われます(オブジェクトIDが変更される)が、テーブルに紐付く(公開されている)統計情報はテーブル定義とは別の場所に保持されていることがわかります。
しかも、統計情報を再取得してもこの保留されている統計情報を上書きすることはありません

7. 保留されている統計情報および公開情報との違いを確認する。

「オプティマイザ統計の保留」が有効になっているテーブルは「USER_TAB_PENDHING_STATS」ビューによって保留中の統計情報を確認することができます。
さらに、「DBMS_STATS.DIFF_TABLE_STATS_IN_PENDING」ファンクションで、公開中と保留中の統計情報を比較することができます。

SQL> col table_name for a12
SQL> select table_name, to_char(last_analyzed,'hh24:mi:ss') last_analyzed, num_rows
  2  from user_tab_pending_stats where table_name = 'TEST';

TABLE_NAME   LAST_ANA   NUM_ROWS
------------ -------- ----------
TEST         20:49:44      50000

SQL> set long 10000
SQL> set pages 9999
SQL> set head off
SQL> select report from table (DBMS_STATS.DIFF_TABLE_STATS_IN_PENDING('TEST','TEST'));

###############################################################################

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

TABLE         : TEST
OWNER         : TEST
SOURCE A      : Current Statistics in dictionary (注:公開中の統計情報)
SOURCE B      : Pending Statistics               (注:保留中の統計情報)
PCTTHRESHOLD  : 10
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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

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

TEST                        T   A   1000       5          14         1000
                                B   50000      244        15         50000
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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

COLUMN_NAME     SRC NDV     DENSITY    HIST NULLS   LEN  MIN   MAX   SAMPSIZ
                    (=Number of Distinct Value)
...............................................................................

COL1            A   1000    .001       NO   0       4    C102  C20B  1000
                B   50000   .00002     NO   0       5    C102  C306  50000
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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

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

                                 INDEX:TEST_IDX
                                 ...............

TEST_IDX        I   A   1000    3       1000    1     1     3       1   1000
                    B   50000   99      50000   1     1     137     NUL 50000
###############################################################################

SQL> set head on

7. 同じ問い合わせを実行し、実行計画を確認する。

データ件数が50倍になったところで、同じ問い合わせを実行し実行計画を確認します。

SQL> set autot traceonly
SQL> set timing on
SQL> select * from test where col1 > 999;

49001 rows selected.

Execution Plan
----------------------------------------------------------
Plan hash value: 1389866015

----------------------------------------------------------------------------------------
| Id  | Operation                   | Name     | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |          |     1 |    14 |     3   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID| TEST     |     1 |    14 |     3   (0)| 00:00:01 |
|*  2 |   INDEX RANGE SCAN          | TEST_IDX |     1 |       |     2   (0)| 00:00:01 |
----------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - access("COL1">999)

Statistics
----------------------------------------------------------
         26  recursive calls
          0  db block gets
       6791  consistent gets
          0  physical reads
          0  redo size
    1578657  bytes sent via SQL*Net to client
      36446  bytes received via SQL*Net from client
       3268  SQL*Net roundtrips to/from client
          6  sorts (memory)
          0  sorts (disk)
      49001  rows processed

50,000件のうち49,001件を取得する問い合わせなので、理想的には「TABLE ACCESS FULL」が選択されるべきですが、統計情報上は1,000件のテーブルなので最初の問い合わせ時と同じ「INDEX RANGE SCAN」が選択されています。
取得された49,001個のrowidに対してそれぞれ「TABLE ACCESS BY INDEX ROWID」が実行されるため、あまり効率的ではないI/Oが発生してしまいます。

8. 保留中の統計情報を使って同じ問い合わせを実行し、実行計画を確認する。

次に、今接続中のこのセッションのみ保留中の統計情報を使うように設定(「OPTIMIZER_USE_PENDING_STATISTICS」初期化パラメータ)を変更し、再度同じ問い合わせを実行してみます。

SQL> alter session set OPTIMIZER_USE_PENDING_STATISTICS = TRUE;

Session altered.

SQL> select * from test where col1 > 999;

49001 rows selected.

Execution Plan
----------------------------------------------------------
Plan hash value: 1357081020

--------------------------------------------------------------------------
| Id  | Operation         | Name | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |      | 49002 |   717K|    68   (0)| 00:00:01 |
|*  1 |  TABLE ACCESS FULL| TEST | 49002 |   717K|    68   (0)| 00:00:01 |
--------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter("COL1">999)

Statistics
----------------------------------------------------------
        592  recursive calls
          0  db block gets
       4413  consistent gets
          0  physical reads
        124  redo size
    1078861  bytes sent via SQL*Net to client
      36446  bytes received via SQL*Net from client
       3268  SQL*Net roundtrips to/from client
        160  sorts (memory)
          0  sorts (disk)
      49001  rows processed

今度は「TABLE ACCESS FULL」が選択されていることがわかります。

9. 再接続しセッション情報をクリアした後に同じ問い合わせを実行する。

SQL> conn test/test
Connected.
SQL> show parameter OPTIMIZER_USE_PENDING_STATISTICS

NAME                                 TYPE        VALUE
------------------------------------ ----------- ------------------------------
optimizer_use_pending_statistics     boolean     FALSE
SQL> set autot traceonly
SQL> set timing on
SQL> select * from test where col1 > 999;

49001 rows selected.

Execution Plan
----------------------------------------------------------
Plan hash value: 1389866015

----------------------------------------------------------------------------------------
| Id  | Operation                   | Name     | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |          |     1 |    14 |     3   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID| TEST     |     1 |    14 |     3   (0)| 00:00:01 |
|*  2 |   INDEX RANGE SCAN          | TEST_IDX |     1 |       |     2   (0)| 00:00:01 |
----------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - access("COL1">999)

Statistics
----------------------------------------------------------
          0  recursive calls
          0  db block gets
       6743  consistent gets
          0  physical reads
          0  redo size
    1578657  bytes sent via SQL*Net to client
      36446  bytes received via SQL*Net from client
       3268  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
      49001  rows processed

CONNECTコマンドで再接続すると、「ALTER SESSION」文で変更した「OPTIMIZER_USE_PENDING_STATISTICS」初期化パラメータの値がデフォルトの「FALSEに戻ります。
その状態で同じ問い合わせを実行すると、非効率な「INDEX RANGE SCAN」を使用した実行計画に戻ってしまうことがわかります。

10. 保留中の統計情報を公開した後に同じ問い合わせを実行する。

今度は保留中の統計情報をDBMS_STATS.PUBLISH_PENDING_STATSプロシージャによって永続的に公開し、再度同じ問い合わせを実行して実行計画を確認します。

SQL> exec DBMS_STATS.PUBLISH_PENDING_STATS('TEST','TEST');

PL/SQL procedure successfully completed.

SQL> select * from test where col1 > 999;

49001 rows selected.

Execution Plan
----------------------------------------------------------
Plan hash value: 1357081020

--------------------------------------------------------------------------
| Id  | Operation         | Name | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |      | 49002 |   717K|    68   (0)| 00:00:01 |
|*  1 |  TABLE ACCESS FULL| TEST | 49002 |   717K|    68   (0)| 00:00:01 |
--------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter("COL1">999)

Statistics
----------------------------------------------------------
          0  recursive calls
          0  db block gets
       6743  consistent gets
          0  physical reads
          0  redo size
    1578657  bytes sent via SQL*Net to client
      36446  bytes received via SQL*Net from client
       3268  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
      49001  rows processed

SQL> set autot off

永続的に公開された統計情報を使った実行計画なので、他のセッションから同じ問い合わせが実行されてもこの実行計画が選択されます。

11. レコードを削除し件数を1,000件とした後に同じ問い合わせを実行する。

次に50,000件のレコードから49,000件を削除(DELETE)し、同じ問い合わせを実行して実行計画を確認します。

SQL> delete from test where col1 > 1000;

49000 rows deleted.

SQL> commit;

Commit complete.

SQL> select count(*) from test;

  COUNT(*)
----------
      1000

SQL> set autot traceonly
SQL> select * from test where col1 > 999;

Execution Plan
----------------------------------------------------------
Plan hash value: 1357081020

--------------------------------------------------------------------------
| Id  | Operation         | Name | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |      | 49002 |   717K|    68   (0)| 00:00:01 |
|*  1 |  TABLE ACCESS FULL| TEST | 49002 |   717K|    68   (0)| 00:00:01 |
--------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - filter("COL1">999)

Statistics
----------------------------------------------------------
          0  recursive calls
          0  db block gets
        101  consistent gets
          0  physical reads
          0  redo size
        625  bytes sent via SQL*Net to client
        520  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          1  rows processed

SQL> set autot off

統計情報上はこのテーブルはまだ50,000件のデータがあるとみなされているため、同じように「TABLE ACCESS FULL」が選択されています。
(蛇足ですが、実行計画ツリーでRowsが「49002」となっていることが興味深いです。)

12. 統計情報を再取得し、公開済みと保留中の統計情報を比較する。

SQL> begin
  2   DBMS_STATS.GATHER_TABLE_STATS('TEST','TEST');
  3  end;
  4  /

PL/SQL procedure successfully completed.

SQL> select to_char(last_analyzed,'hh24:mi:ss') last_analyzed, num_rows from user_tables
  2  where table_name = 'TEST';

LAST_ANA   NUM_ROWS
-------- ----------
20:49:44      50000

SQL> select table_name, to_char(last_analyzed,'hh24:mi:ss') last_analyzed, num_rows
  2  from user_tab_pending_stats where table_name = 'TEST';

TABLE_NAME   LAST_ANA   NUM_ROWS
------------ -------- ----------
TEST         21:18:42       1000

SQL> set long 10000
SQL> set pages 9999
SQL> set head off
SQL> select report from table (DBMS_STATS.DIFF_TABLE_STATS_IN_PENDING('TEST','TEST'));

###############################################################################

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

TABLE         : TEST
OWNER         : TEST
SOURCE A      : Current Statistics in dictionary
SOURCE B      : Pending Statistics
PCTTHRESHOLD  : 10
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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

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

TEST                        T   A   50000      244        15         50000
                                B   1000       244        14         1000
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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

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

COL1            A   50000   .00002     NO   0       5    C102  C306  50000
                B   1000    .001       NO   0       4    C102  C20B  1000
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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

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

                                 INDEX:TEST_IDX
                                 ...............

TEST_IDX        I   A   50000   99      50000   1     1     137     1   50000
                    B   1000    2       1000    1     1     3       NUL 1000
###############################################################################

SQL> set head on

DELETEの結果レコード件数が50,000件から1,000件になっても、ブロック数が「244」のままとなっていることに注目してください。
つまり、「TABLE ACCESS FULL」によって「244」ブロックもの無駄なI/Oが発生することになります。

13. テーブルの統計情報公開属性を「TRUE=公開」に設定変更し、同じ問い合わせを実行する。

SQL> begin
  2   DBMS_STATS.SET_TABLE_PREFS('TEST','TEST','PUBLISH','TRUE');
  3  end;
  4  /

PL/SQL procedure successfully completed.

SQL> select DBMS_STATS.GET_PREFS('PUBLISH','TEST','TEST') prefs from dual;

PREFS
----------
TRUE

SQL> set autot traceonly
SQL> select * from test where col1 > 999;

Execution Plan
----------------------------------------------------------
Plan hash value: 1357081020

--------------------------------------------------------------------------
| Id  | Operation         | Name | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |      | 49002 |   717K|    68   (0)| 00:00:01 |
|*  1 |  TABLE ACCESS FULL| TEST | 49002 |   717K|    68   (0)| 00:00:01 |
--------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - filter("COL1">999)

Statistics
----------------------------------------------------------
         19  recursive calls
          0  db block gets
        234  consistent gets
          0  physical reads
          0  redo size
        621  bytes sent via SQL*Net to client
        520  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          5  sorts (memory)
          0  sorts (disk)
          1  rows processed

テーブルの統計情報公開属性を「TRUE=公開」に設定変更したとしても、保留中の統計情報は即時に公開されるわけではないことに注目してください。

14. 保留中の統計情報を公開し、同じ問い合わせを実行する。

SQL> exec DBMS_STATS.PUBLISH_PENDING_STATS('TEST','TEST');

PL/SQL procedure successfully completed.

SQL> select * from test where col1 > 999;

Execution Plan
----------------------------------------------------------
Plan hash value: 1389866015

----------------------------------------------------------------------------------------
| Id  | Operation                   | Name     | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |          |     1 |    14 |     3   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID| TEST     |     1 |    14 |     3   (0)| 00:00:01 |
|*  2 |   INDEX RANGE SCAN          | TEST_IDX |     1 |       |     2   (0)| 00:00:01 |
----------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - access("COL1">999)

Statistics
----------------------------------------------------------
          0  recursive calls
          0  db block gets
        191  consistent gets
          0  physical reads
          0  redo size
        621  bytes sent via SQL*Net to client
        520  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          1  rows processed

保留中の統計情報は公開して初めて実行計画の算出に使われるようになります。
テーブル件数に応じた適切な実行計画が選択されました。

今回はマニュアルに記載のある基本的な動作について確認しました。
次回は保留中の統計情報がどのように保持されているのかを詳しく見てみることにします。

続く

「オプティマイザ統計の保留」の検証(その1)

「オプティマイザ統計の保留」とは?

今回から「オプティマイザ統計の保留」というOracle 11g以降で使えるようになった機能を紹介・検証していきます。

以下はマニュアルへのリンクです。
13.5.1 統計の保留
Oracle® Databaseパフォーマンス・チューニング・ガイド
11gリリース2 (11.2)
B56312-06

機能概要

  • 統計情報は収集後即時に公開(PUBLISH)される。(デフォルトの動作)
  • 公開された統計はXXXX_TAB_STATISTICSやXXXX_IND_STATISTICSデータ・ディクショナリ・ビューに格納される。
  • オプティマイザは原則的にデータ・ディクショナリ・ビューに格納されている公開済の統計を使用する。
  • 従って即時に公開された新しい統計情報は実行計画に影響を与える可能性がある。
  • 新しい統計を「保留中」として保存することができる。(統計の保留
  • PUBLISH設定は、スキーマまたはテーブル・レベルで変更することができる。
  • 保留中の統計は、XXXX_TAB_PENDING_STATSやXXXX_IND_PENDING_STATSビューに格納される。
  • 保留された統計情報は実行計画に反映されない。
  • OPTIMIZER_USE_PENDING_STATISTICS初期化パラメータをTRUEに設定(セッション単位で変更可)すると、保留中の統計情報を実行計画に反映することができる。
  • 現在の公開済み統計情報と新しく保留された統計情報を比較することができる。
  • 保留中の統計情報を手動で公開することができる。

関連コマンド等

検証を行う前に、関連するコマンド等を整理しておきます。

1. PUBLISH設定を変更するには

PUBLISH属性は、スキーマあるいはテーブルに関する属性になりますが、「保留中」に変更するには以下の要領でDBMS_STATS.SET_TABLE_PREFSパッケージ(プロシージャ)を使用して行います。(ALTER TABLEは使用しません。)

SQL> EXEC DBMS_STATS.SET_TABLE_PREFS('<スキーマ名>', '<テーブル名>;', 'PUBLISH', 'false');

例:

SQL> EXEC DBMS_STATS.SET_TABLE_PREFS('SH', 'CUSTOMERS', 'PUBLISH', 'false');

2. 現在のPUBLISH設定を確認するには

現在設定されているPUBLISH属性の状態を確認するためには、データ・ディクショナリ・ビューへの問い合わせではなく、DBMS_STATS.GET_PREFSパッケージ(ファンクション)を使用して行います。

SQL> select DBMS_STATS.GET_PREFS('PUBLISH', '<スキーマ名>', '<テーブル名>') prefs from dual;

例:

SQL> select DBMS_STATS.GET_PREFS('PUBLISH', 'SH', 'CUSTOMERS') prefs from dual;

3. 保留中の統計を一時的に使用して実行計画を作成するには

SQL> alter session set OPTIMIZER_USE_PENDING_STATISTICS = TRUE;

このコマンド実行後のセッションではオプティマイザは保留中の統計を使用して実行計画を作成します。

4. 現在の公開済み統計と新しく保留された統計を比較するには

この場合は、DBMS_STATS.DIFF_TABLE_STATS_IN_PENDINGパッケージ(ファンクション)を使用して行います。

SQL> set long 10000
SQL> set pages 9999
SQL> set head off
SQL> select report from table (DBMS_STATS.DIFF_TABLE_STATS_IN_PENDING('<スキーマ名>', '<テーブル名>'));

例:

SQL> select report from table (DBMS_STATS.DIFF_TABLE_STATS_IN_PENDING('SH', 'CUSTOMERS'));

5. 保留中の統計を永続的に公開するには

DBMS_STATS.PUBLISH_PENDING_STATSパッケージ(プロシージャ)を使用して行います。公開された統計はもはや保留中ではありません。オプティマイザは次に実行計画を作成する際にこの統計を使用します。

SQL> exec DBMS_STATS.PUBLISH_PENDING_STATS('<スキーマ名>', '<テーブル名>');

例:

SQL> exec DBMS_STATS.PUBLISH_PENDING_STATS('SH', 'CUSTOMERS');

検証シナリオ

それでは、この機能を検証するためのシナリオを以下に紹介します。

  1. テーブル作成
    • TEST表(ID, VALUE)
  2. 少量データ作成(1,000件)
  3. インデックス作成
    • ID列に対して
  4. クエリー実行①
    • インデックス・レンジ検索(1件取得)
  5. テーブルTRUNCATE
  6. 大量データ作成(50,000件)
  7. クエリー実行②
    • 4.と同じクエリー(ただし、検索レンジが異なる)
  8. 大量データ削除(50,000→1,000件)
  9. クエリー実行③
    • 4, 7と同じクエリー

レコード件数が変化したタイミングで同じクエリーを実行し実行計画を確認しますが、統計情報の取得によって実行計画がどのように変化する(あるいは変化しないか)を検証していきます。

次回へ続く