忘れかけのIT備忘録

今まで学んできた知識や小技、なるほど!と思ったことをメモするブログです。

Oracleの統計情報(ヒストグラム編)

今回はOracleの統計情報のうち、ヒストグラムについて調査しました。

Oracleの統計情報 - 忘れかけのIT備忘録で少しだけ触れましたが、ヒストグラムは表の列データの分布状況です。
セレクティビティ(行の選択率)のコストを見積もるために使用される統計情報です。
通常の統計情報に比べ、より最適な実行計画を作成/選択するのが目的です。
ヒストグラムには、頻度分布ヒストグラム、高さ調整ヒストグラム、上位頻度ヒストグラム、ハイブリッドヒストグラムがあります。
XXX_TAB_STATISTICSのHISTOGRAMで使用しているヒストグラムを確認できます。
XXX_HISTOGRAMS(XXX_TAB_HISTOGRAMS)でヒストグラムの詳細を確認できます。
※セレクティビティ:行が選択される割合。全表スキャンか索引スキャンかの判断指標になる
※上位頻度ヒストグラム、ハイブリッドヒストグラムは12c以降の機能

ヒストグラムがないと・・・
Oracleはデフォルトでヒストグラムを収集しますが、ヒストグラムが収集されていない場合、オプティマイザはデータが均等に分布されていると推測してしまいます。

実際のテーブルのデータの分布とオプティマイザが把握しているデータの分布に誤差が発生するため、最適な実行計画を作成/選択できません。
たとえば下記の図の場合、「D」のデータへアクセスする際、オプティマイザは「NDV=4」、「データは均等に分布されている」と見積もって全表スキャンの方が効率的と判断してしまう可能性があります。

【検証手順】
1. 検証テーブル作成、検証データ挿入
2. 統計情報取得(ヒストグラム収集なし)
3. ヒストグラムが存在しない状態でレコード取得、実行計画確認
4. 統計情報取得(ヒストグラム収集あり)
5. ヒストグラムが存在する状態でレコード取得、実行計画確認

【作業ログ】

1. 検証テーブル作成、検証データ挿入
SQL> CREATE TABLE TEST_TABLE1
  2         (COL1 NUMBER(7) CONSTRAINT PK_TEST_TABLE1 PRIMARY KEY,
  3          COL2 VARCHAR2(4));

表が作成されました。

SQL> CREATE INDEX IDX1_TEST_TABLE1 ON TEST_TABLE1(COL2);

索引が作成されました。

SQL> INSERT INTO TEST_TABLE1 SELECT LEVEL, 'A' FROM DUAL CONNECT BY LEVEL <= 1000;

1000行が作成されました。

SQL> INSERT INTO TEST_TABLE1 SELECT LEVEL + 1000, 'B' FROM DUAL CONNECT BY LEVEL <= 1000;

1000行が作成されました。

SQL> INSERT INTO TEST_TABLE1 SELECT LEVEL + 2000, 'C' FROM DUAL CONNECT BY LEVEL <= 1000;

1000行が作成されました。

SQL> INSERT INTO TEST_TABLE1 VALUES(3001, 'D');

1行が作成されました。

SQL> COMMIT;

コミットが完了しました。

SQL> SELECT COL2, COUNT(*) FROM TEST_TABLE1 GROUP BY COL2 ORDER BY 1;

COL2           COUNT(*)
------------ ----------
A                  1000
B                  1000
C                  1000
D                     1

2. 統計情報取得(ヒストグラム収集なし)
SQL> BEGIN
  2    DBMS_STATS.GATHER_TABLE_STATS(ownname    => 'SYS',
  3                                  tabname    => 'TEST_TABLE1',
  4                                  method_opt => 'FOR ALL COLUMNS SIZE 1',
  5                                  cascade    => FALSE);
  6  END;
  7  /

PL/SQLプロシージャが正常に完了しました。
★'FOR ALL COLUMNS SIZE 1'はヒストグラムを収集しないように明示的に指定している

SQL> SELECT OWNER, TABLE_NAME, COLUMN_NAME, NUM_DISTINCT, NUM_BUCKETS, LAST_ANALYZED, HISTOGRAM FROM ALL_TAB_COL_STATISTICS WHERE OWNER = 'SYS' AND TABLE_NAME = 'TEST_TABLE1' AND COLUMN_NAME = 'COL2';

OWNER           TABLE_NAME      COLUMN_NAME     NUM_DISTINCT NUM_BUCKETS LAST_ANALYZED       HISTOGRAM
--------------- --------------- --------------- ------------ ----------- ------------------- --------------------
SYS             TEST_TABLE1     COL2                       4           1 2023-02-17 11:19:30 NONE
ヒストグラムは収集されていない

3. ヒストグラムが存在しない状態でレコード取得、実行計画確認
SQL> SELECT /* TEST1_1 */ * FROM TEST_TABLE1 WHERE COL2 = 'D';

      COL1 COL2
---------- ------------
      3001 D

SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('3nazjkcxu3x77'));

PLAN_TABLE_OUTPUT
-----------------
SQL_ID  3nazjkcxu3x77, child number 0
-------------------------------------
SELECT /* TEST1_1 */ * FROM TEST_TABLE1 WHERE COL2 = 'D'

Plan hash value: 3636527845

--------------------------------------------------------------------------------------------------------
| Id  | Operation                           | Name             | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                    |                  |       |       |     1 (100)|          |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| TEST_TABLE1      |   750 |  4500 |     1   (0)| 00:00:01 |
|*  2 |   INDEX RANGE SCAN                  | IDX1_TEST_TABLE1 |     1 |       |     1   (0)| 00:00:01 |
--------------------------------------------------------------------------------------------------------

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

   2 - access("COL2"='D')
★インデックスを範囲検索して1行ヒットしたがオプティマイザは750行のテーブルアクセス(マルチブロックリードorパラレルリード)が発生すると見積もった

4. 統計情報取得(ヒストグラム収集あり)
SQL> BEGIN
  2    DBMS_STATS.GATHER_TABLE_STATS(ownname          => 'SYS',
  3                                  tabname          => 'TEST_TABLE1',
  4                                  estimate_percent => 100,
  5                                  method_opt       => 'FOR COLUMNS COL2 SIZE AUTO',
  6                                  cascade          => TRUE);
  7  END;
  8  /

PL/SQLプロシージャが正常に完了しました。

SQL> SELECT OWNER, TABLE_NAME, COLUMN_NAME, NUM_DISTINCT, NUM_BUCKETS, LAST_ANALYZED, HISTOGRAM FROM ALL_TAB_COL_STATISTICS WHERE OWNER = 'SYS' AND TABLE_NAME = 'TEST_TABLE1' AND COLUMN_NAME = 'COL2';

OWNER           TABLE_NAME      COLUMN_NAME     NUM_DISTINCT NUM_BUCKETS LAST_ANALYZED       HISTOGRAM
--------------- --------------- --------------- ------------ ----------- ------------------- --------------------
SYS             TEST_TABLE1     COL2                       4           4 2023-02-17 11:20:21 FREQUENCY
ヒストグラムが収集された

5. ヒストグラムが存在する状態でレコード取得、実行計画確認
SQL> SELECT /* TEST1_2 */ * FROM TEST_TABLE1 WHERE COL2 = 'D';

      COL1 COL2
---------- ------------
      3001 D

SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('dm1pa05nmvdd7'));

PLAN_TABLE_OUTPUT
-----------------
SQL_ID  dm1pa05nmvdd7, child number 0
-------------------------------------
SELECT /* TEST1_2 */ * FROM TEST_TABLE1 WHERE COL2 = 'D'

Plan hash value: 3636527845

--------------------------------------------------------------------------------------------------------
| Id  | Operation                           | Name             | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                    |                  |       |       |     2 (100)|          |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| TEST_TABLE1      |     1 |     6 |     2   (0)| 00:00:01 |
|*  2 |   INDEX RANGE SCAN                  | IDX1_TEST_TABLE1 |     1 |       |     1   (0)| 00:00:01 |
--------------------------------------------------------------------------------------------------------

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

   2 - access("COL2"='D')
★インデックスを範囲検索して1行ヒットしてオプティマイザは1行のテーブルアクセスが発生すると見積もった

 

頻度分布ヒストグラム(Frequency)
列内の個別値がバケット数以下の場合、それぞれの値が何行あるのか正確に把握します(データの分布を列値の累計行数の差分から正確に計算して把握します)
個別値の数が指定されたバケット数以下の場合、頻度分布ヒストグラムを生成します。
「NDV <= バケット数」の場合、頻度分布ヒストグラムが採用されます。
バケット:分布データ(個別値)の格納領域

メリット
・NDVが少ない中、他の個別値よりも極端に累計行数が少ない個別値を検索する場合、索引アクセスが選択される可能性が高くなる
・個別値の累計行数の差分を算出するため、正確な分布データが得られる(それぞれの個別値が何行あるか正確に把握できる)
デメリット
・NDVに比例してデータ量が多くなるため、ヒストグラム格納用のディクショナリが肥大化しやすい
・最大2048種類の値までしかヒストグラムが作成できない(11gは最大254種類まで)

【検証手順】
前提として下記のようなデータがあるとします
SQL> select * from emp;

     EMPNO ENAME           JOB                    MGR HIREDATE                   SAL       COMM     DEPTNO
---------- --------------- --------------- ---------- ------------------- ---------- ---------- ----------
      7369 SMITH           CLERK                 7902 1980-12-17 00:00:00        800                    10
      7499 ALLEN           SALESMAN              7698 1981-02-20 00:00:00       1600        300         10
      7521 WARD            SALESMAN              7698 1981-02-22 00:00:00       1250        500         10
      7566 JONES           MANAGER               7839 1981-04-02 00:00:00       2975                    10
      7654 MARTIN          SALESMAN              7698 1981-09-28 00:00:00       1250       1400         10
      7698 BLAKE           MANAGER               7839 1981-05-01 00:00:00       2850                    10
      7782 CLARK           MANAGER               7839 1981-06-09 00:00:00       2450                    10
      7839 KING            PRESIDENT                  1981-11-17 00:00:00       5000                    10
      7844 TURNER          SALESMAN              7698 1981-09-08 00:00:00       1500          0         20
      7900 JAMES           CLERK                 7698 1981-12-03 00:00:00        950                    20
      7902 FORD            ANALYST               7566 1981-12-03 00:00:00       3000                    30
      7934 MILLER          CLERK                 7782 1982-01-23 00:00:00       1300                    40

SQL> select * from dept;

    DEPTNO DNAME           LOC
---------- --------------- ---------------
        10 ACCOUNTING      NEW YORK
        20 RESEARCH        DALLAS
        30 SALES           CHICAGO
        40 OPERATIONS      BOSTON

1. 統計情報取得(ヒストグラム収集なし)
2. 統計情報取得(ヒストグラム収集あり)

【作業ログ】

1. 統計情報取得(ヒストグラム収集なし)
SQL> BEGIN
  2    DBMS_STATS.GATHER_TABLE_STATS(ownname    => 'SCOTT',
  3                                  tabname    => 'EMP',
  4                                  method_opt => 'FOR ALL COLUMNS SIZE 1',
  5                                  cascade    => FALSE);
  6  END;
  7  /

PL/SQLプロシージャが正常に完了しました。

SQL> SELECT OWNER, TABLE_NAME, COLUMN_NAME, NUM_DISTINCT, NUM_BUCKETS, LAST_ANALYZED, HISTOGRAM FROM ALL_TAB_COL_STATISTICS WHERE OWNER = 'SCOTT' AND TABLE_NAME = 'EMP' AND COLUMN_NAME = 'DEPTNO';

OWNER           TABLE_NAME      COLUMN_NAME     NUM_DISTINCT NUM_BUCKETS LAST_ANALYZED       HISTOGRAM
--------------- --------------- --------------- ------------ ----------- ------------------- --------------------
SCOTT           EMP             DEPTNO                     4           1 2023-02-17 10:04:48 NONE
★通常の統計情報は取得されたがヒストグラムは存在しない

SQL> SELECT TABLE_NAME, COLUMN_NAME, ENDPOINT_VALUE, ENDPOINT_NUMBER FROM ALL_HISTOGRAMS  WHERE OWNER = 'SCOTT' AND TABLE_NAME = 'EMP' AND COLUMN_NAME = 'DEPTNO' ORDER BY ENDPOINT_VALUE;

TABLE_NAME      COLUMN_NAME     ENDPOINT_VALUE ENDPOINT_NUMBER
--------------- --------------- -------------- ---------------
EMP             DEPTNO                      10               0
EMP             DEPTNO                      40               1
ヒストグラムが存在しない場合、2バケット(列データの最小値・最大値)があるように見える

2. 統計情報取得(ヒストグラム収集あり)
SQL> BEGIN
  2    DBMS_STATS.GATHER_TABLE_STATS(ownname          => 'SCOTT',
  3                                  tabname          => 'EMP',
  4                                  estimate_percent => 100,
  5                                  method_opt       => 'FOR COLUMNS DEPTNO SIZE 2048',
  6                                  cascade          => TRUE);
  7  END;
  8  /

PL/SQLプロシージャが正常に完了しました。

SQL> SELECT OWNER, TABLE_NAME, COLUMN_NAME, NUM_DISTINCT, NUM_BUCKETS, LAST_ANALYZED, HISTOGRAM FROM ALL_TAB_COL_STATISTICS WHERE OWNER = 'SCOTT' AND TABLE_NAME = 'EMP' AND COLUMN_NAME = 'DEPTNO';

OWNER           TABLE_NAME      COLUMN_NAME     NUM_DISTINCT NUM_BUCKETS LAST_ANALYZED       HISTOGRAM
--------------- --------------- --------------- ------------ ----------- ------------------- --------------------
SCOTT           EMP             DEPTNO                     4           4 2023-02-17 10:05:40 FREQUENCY
★頻度分布ヒストグラムが取得された
★NDV=4に対して'FOR COLUMNS DEPTNO SIZE 2048'を指定してもバケット数(NUM_BUCKETS)は4になった。NDVより大きい値を指定してもバケット数はNDVと同じになるようです

SQL> SELECT TABLE_NAME, COLUMN_NAME, ENDPOINT_VALUE, ENDPOINT_NUMBER FROM ALL_HISTOGRAMS  WHERE OWNER = 'SCOTT' AND TABLE_NAME = 'EMP' AND COLUMN_NAME = 'DEPTNO' ORDER BY ENDPOINT_VALUE;

TABLE_NAME      COLUMN_NAME     ENDPOINT_VALUE ENDPOINT_NUMBER
--------------- --------------- -------------- ---------------
EMP             DEPTNO                      10               8
EMP             DEPTNO                      20              10
EMP             DEPTNO                      30              11
EMP             DEPTNO                      40              12
※ENDPOINT_VALUEバケットに格納する分布データ(個別値)
※ENDPOINT_NUMBER:バケットに格納する分布データ(個別値)の累計行数。累計行数と1つ前のエントリの累計行数の差分が列値の行数となる(オフセットみたいなイメージ)

 

高さ調整ヒストグラム(Height Balanced)
列内の個別値がバケット数より多い場合、指定バケット内にソートしたデータ(個別値)を順番・均等に配置して、それぞれのバケットの最大値を記録し、その値によってデータの偏りを把握します。
複数のバケットのうち、連続したバケットの最大値(ENDPOINT_VALUE)が同じ場合、その値が他の値より多く存在しているということになります。
複数のバケットの最大値を記録し、最も頻出度が高いものをポピュラ値と呼びます。
「NDV > バケット数」の場合、高さ調整ヒストグラムが採用されます。

メリット
・厳密な統計がなくても正しい実行計画が選択できる
・NDVが多い場合でもヒストグラムを使用できる
・ポピュラ値以外の個別値は省略できるため、頻度分布ヒストグラムより領域節約できる可能性がある
デメリット
・ポピュラ値でデータの偏りを見るため、等価条件では偏りを見つけられない(たとえばDEPTNOが全部「10」の場合、ポピュラ値は10となるため、データの偏りを把握できないということだと思います)
・頻度分布ヒストグラムより正確ではない

【検証手順】
1. 統計情報取得(ヒストグラム収集なし)
2. 統計情報取得(ヒストグラム収集あり)

【作業ログ】

1. 統計情報取得(ヒストグラム収集なし)
SQL> BEGIN
  2    DBMS_STATS.GATHER_TABLE_STATS(ownname    => 'SCOTT',
  3                                  tabname    => 'EMP',
  4                                  method_opt => 'FOR ALL COLUMNS SIZE 1',
  5                                  cascade    => FALSE);
  6  END;
  7  /

PL/SQLプロシージャが正常に完了しました。

SQL> SELECT OWNER, TABLE_NAME, COLUMN_NAME, NUM_DISTINCT, NUM_BUCKETS, LAST_ANALYZED, HISTOGRAM FROM ALL_TAB_COL_STATISTICS WHERE OWNER = 'SCOTT' AND TABLE_NAME = 'EMP' AND COLUMN_NAME = 'DEPTNO';

OWNER           TABLE_NAME      COLUMN_NAME     NUM_DISTINCT NUM_BUCKETS LAST_ANALYZED       HISTOGRAM
--------------- --------------- --------------- ------------ ----------- ------------------- --------------------
SCOTT           EMP             DEPTNO                     4           1 2023-02-17 10:04:48 NONE
★通常の統計情報は取得されたがヒストグラムは存在しない

SQL> SELECT TABLE_NAME, COLUMN_NAME, ENDPOINT_VALUE, ENDPOINT_NUMBER FROM ALL_HISTOGRAMS  WHERE OWNER = 'SCOTT' AND TABLE_NAME = 'EMP' AND COLUMN_NAME = 'DEPTNO' ORDER BY ENDPOINT_VALUE;

TABLE_NAME      COLUMN_NAME     ENDPOINT_VALUE ENDPOINT_NUMBER
--------------- --------------- -------------- ---------------
EMP             DEPTNO                      10               0
EMP             DEPTNO                      40               1

2. 統計情報取得(ヒストグラム収集あり)
SQL> BEGIN
  2    DBMS_STATS.GATHER_TABLE_STATS(ownname          => 'SCOTT',
  3                                  tabname          => 'EMP',
  4                                  estimate_percent => 100,
  5                                  method_opt       => 'FOR COLUMNS DEPTNO SIZE 3',
  6                                  cascade          => TRUE);
  7  END;
  8  /

PL/SQLプロシージャが正常に完了しました。

SQL> SELECT OWNER, TABLE_NAME, COLUMN_NAME, NUM_DISTINCT, NUM_BUCKETS, LAST_ANALYZED, HISTOGRAM FROM ALL_TAB_COL_STATISTICS WHERE OWNER = 'SCOTT' AND TABLE_NAME = 'EMP' AND COLUMN_NAME = 'DEPTNO';

OWNER           TABLE_NAME      COLUMN_NAME     NUM_DISTINCT NUM_BUCKETS LAST_ANALYZED       HISTOGRAM
--------------- --------------- --------------- ------------ ----------- ------------------- --------------------
SCOTT           EMP             DEPTNO                     4           3 2023-02-17 10:09:01 HEIGHT BALANCED
★高さ調整ヒストグラムが取得された

SQL> SELECT TABLE_NAME, COLUMN_NAME, ENDPOINT_VALUE, ENDPOINT_NUMBER FROM ALL_HISTOGRAMS  WHERE OWNER = 'SCOTT' AND TABLE_NAME = 'EMP' AND COLUMN_NAME = 'DEPTNO' ORDER BY ENDPOINT_VALUE;

TABLE_NAME      COLUMN_NAME     ENDPOINT_VALUE ENDPOINT_NUMBER
--------------- --------------- -------------- ---------------
EMP             DEPTNO                      10               2
EMP             DEPTNO                      40               3
※ENDPOINT_VALUEバケットに格納する分布データ(分布データの最大値)
※ENDPOINT_NUMBER:格納する分布データを格納するバケット番号

 

上位頻度ヒストグラム(12c~) ※未検証
頻度分布ヒストグラムから派生したヒストグラムで個別値の中で特に割合の多い個別値だけを対象としたヒストグラムです。
上位N個の個別値がデータの大半を占めている場合、上位頻度ヒストグラムが作成されます。
上位N個についてはオプティマイザが正確に行数を見積もることができます。

ハイブリッドヒストグラム(12c~) ※未検証
高さ調整ヒストグラムと頻度分布ヒストグラムを組み合わせたヒストグラムです。
高さ調整ヒストグラムの場合、ほぼポピュラな値について正確ではない見積りが作成される場合があります。
たとえば1つのバケットのエンドポイント値(最大値)として出現しますが、2つのバケットをほぼ占めているエンドポイント値(最大値)はポピュラ値とはみなされません。(ポピュラ値が見つからない)
この問題を解決するため、複数のバケットを占めるエンドポイント値(最大値)がないように個別値が配分され、エンドポイント繰返しカウント値が格納されます。
エンドポイント繰返しカウント値はヒストグラム内の各エンドポイント(バケット)について、エンドポイント値(最大値)が繰り返された回数です。
繰返しカウントを使用することで、オプティマイザはほぼポピュラな値について正確な見積りができます。

下記の手順でヒストグラムを作成します。

1. 高さ調整ヒストグラムのように各バケットに同数ずつ個別値を格納する
2. 頻度分布ヒストグラムのように同じ個別値が複数のバケットに格納されないよう調整する

バケットの最後の個別値が何行あるのか記録していき、頻出する個別値に対して正確な見積もりができます。

※図を作成しようとしましたがそもそも仕組みがイメージできませんでした。。

オプティマイザの勘違い
【検証手順】
1. 検証テーブル作成、検証データ挿入
2. 統計情報取得(ヒストグラム収集なし)
3. ヒストグラムが存在しない状態でレコード取得、実行計画確認
4. 統計情報取得(ヒストグラム収集あり)
5. ヒストグラムが存在する状態でレコード取得、実行計画確認

【作業ログ】

1. 検証テーブル作成、検証データ挿入
SQL> CREATE TABLE TEST_TABLE1
  2         (COL1 NUMBER(7) CONSTRAINT PK_TEST_TABLE1 PRIMARY KEY,
  3          COL2 VARCHAR2(4));

表が作成されました。

SQL> CREATE INDEX IDX1_TEST_TABLE1 ON TEST_TABLE1(COL2);

索引が作成されました。

SQL> INSERT INTO TEST_TABLE1 SELECT LEVEL, 'A' FROM DUAL CONNECT BY LEVEL <= 1000;

1000行が作成されました。

SQL> INSERT INTO TEST_TABLE1 SELECT LEVEL + 1000, 'B' FROM DUAL CONNECT BY LEVEL <= 1000;

1000行が作成されました。

SQL> INSERT INTO TEST_TABLE1 SELECT LEVEL + 2000, 'C' FROM DUAL CONNECT BY LEVEL <= 1000;

1000行が作成されました。

SQL> INSERT INTO TEST_TABLE1 VALUES(100000, 'D');

1行が作成されました。

SQL> COMMIT;

コミットが完了しました。

SQL> SELECT COL2, COUNT(*) FROM TEST_TABLE1 GROUP BY COL2 ORDER BY 1;

COL2           COUNT(*)
------------ ----------
A                  1000
B                  1000
C                  1000
D                     1

2. 統計情報取得(ヒストグラム収集なし)
SQL> BEGIN
  2    DBMS_STATS.GATHER_TABLE_STATS(ownname    => 'SYS',
  3                                  tabname    => 'TEST_TABLE1',
  4                                  method_opt => 'FOR ALL COLUMNS SIZE 1',
  5                                  cascade    => FALSE);
  6  END;
  7  /

PL/SQLプロシージャが正常に完了しました。

SQL> SELECT OWNER, TABLE_NAME, COLUMN_NAME, NUM_DISTINCT, NUM_BUCKETS, LAST_ANALYZED, HISTOGRAM FROM ALL_TAB_COL_STATISTICS WHERE OWNER = 'SYS' AND TABLE_NAME = 'TEST_TABLE1' AND COLUMN_NAME = 'COL2';

OWNER           TABLE_NAME      COLUMN_NAME     NUM_DISTINCT NUM_BUCKETS LAST_ANALYZED       HISTOGRAM
--------------- --------------- --------------- ------------ ----------- ------------------- --------------------
SYS             TEST_TABLE1     COL2                       4           1 2023-02-17 11:19:30 NONE

3. ヒストグラムが存在しない状態でレコード取得、実行計画確認
SQL> SELECT /* TEST2_1 */ * FROM TEST_TABLE2 WHERE COL1 >= 50000;

      COL1 COL2
---------- ------------
    100000 D

SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('1xp5hx4tqgd68'));

PLAN_TABLE_OUTPUT
-----------------
SQL_ID  1xp5hx4tqgd68, child number 0
-------------------------------------
SELECT /* TEST2_1 */ * FROM TEST_TABLE2 WHERE COL1 >= 50000

Plan hash value: 775196657

---------------------------------------------------------------------------------
| Id  | Operation         | Name        | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |             |       |       |     3 (100)|          |
|*  1 |  TABLE ACCESS FULL| TEST_TABLE2 |  1502 |  9012 |     3   (0)| 00:00:01 |
---------------------------------------------------------------------------------

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

   1 - filter("COL1">=50000)
★COL1にはインデックスを作成しているが全表スキャンの方が効率的と判断された

4. 統計情報取得(ヒストグラム収集あり)
SQL> BEGIN
  2    DBMS_STATS.GATHER_TABLE_STATS(OWNNAME          => 'SYS',
  3                                  TABNAME          => 'TEST_TABLE2',
  4                                  ESTIMATE_PERCENT => 100,
  5                                  METHOD_OPT       => 'FOR COLUMNS COL1 SIZE AUTO',
  6                                  CASCADE          => TRUE);
  7  END;
  8  /

PL/SQLプロシージャが正常に完了しました。

SQL> SELECT OWNER, TABLE_NAME, COLUMN_NAME, NUM_DISTINCT, NUM_BUCKETS, LAST_ANALYZED, HISTOGRAM FROM ALL_TAB_COL_STATISTICS WHERE OWNER = 'SYS' AND TABLE_NAME = 'TEST_TABLE2' AND COLUMN_NAME = 'COL1';

OWNER           TABLE_NAME      COLUMN_NAME     NUM_DISTINCT NUM_BUCKETS LAST_ANALYZED       HISTOGRAM
--------------- --------------- --------------- ------------ ----------- ------------------- --------------------
SYS             TEST_TABLE2     COL1                    3001         254 2023-02-17 11:25:52 HEIGHT BALANCED

5. ヒストグラムが存在する状態でレコード取得、実行計画確認
SQL> SELECT /* TEST2_2 */ * FROM TEST_TABLE2 WHERE COL1 >= 50000;

      COL1 COL2
---------- ------------
    100000 D

SQL> SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('5b1sk5jbpx7gx'));

PLAN_TABLE_OUTPUT
-----------------
SQL_ID  5b1sk5jbpx7gx, child number 0
-------------------------------------
SELECT /* TEST2_2 */ * FROM TEST_TABLE2 WHERE COL1 >= 50000

Plan hash value: 3003225609

------------------------------------------------------------------------------------------------------
| Id  | Operation                           | Name           | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                    |                |       |       |     3 (100)|          |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| TEST_TABLE2    |     6 |    36 |     3   (0)| 00:00:01 |
|*  2 |   INDEX RANGE SCAN                  | PK_TEST_TABLE2 |     6 |       |     2   (0)| 00:00:01 |
------------------------------------------------------------------------------------------------------

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

   2 - access("COL1">=50000)
★索引スキャンが採用された

実データは合計3001行ですが「D」のEMPNOを「100000」にしたことでオプティマイザは合計100000行存在すると見積もってしまい、ヒストグラム収集前のSQL(TEST2_1)で全体の50%以上にアクセスするなら全表スキャンの方が効率的と判断してしまったようです

 

■参考資料
DBMS_STATS

ヒストグラム
https://www.oracle.com/technetwork/jp/ondemand/branch/120328-cbo-1593994-ja.pdf
移行前に知っておきたい、Oracle Database 12c新機能のオモテとウラ Vol.6 | アシスト
パフォーマンス改善と事前対策に役立つ Oracle SQLチューニング 本気で学ぶ実践的な考え方とテクニック
新・門外不出のOracle現場ワザ エキスパートが明かす運用・管理の極意

■おわりに
「上位頻度ヒストグラム」、「ハイブリッドヒストグラム」はどちらも未検証ですが、上位頻度ヒストグラムについてはヒストグラムの作成方法は頻度分布ヒストグラムと似ていますが、上位N個にフォーカスすることで頻度分布ヒストグラムより領域節約できる印象でした。
ハイブリッドヒストグラムについては「最初から頻度分布ヒストグラムで良いのでは?」と思いましたが、NDVが最大数(12cは2048)を超えた場合、頻度分布ヒストグラムで対応できないため、いったん高さ調整ヒストグラムヒストグラムを作成して最終的に頻度分布ヒストグラムを作成している印象でした。
なお、どのヒストグラムが作成されるかは参考資料(ヒストグラム)の「図11-1 ヒストグラム作成のディシジョン・ツリー」のフローで決まります。