2

全表スキャンは邪悪な情報源ではなく、テーブルが比較的小さい場合にのみ、TomKyteに同意します。したがって、そのようなテーブルの追加のインデックスを持つことは冗長です。ただし、100.000レコードのテーブルは小さいと見なすべきではありませんが、そのようなテーブルからの計画を説明すると、実行されたテーブルの全表スキャンが示されます。そこで、Oracleをローカルにインストールしたラップトップで小さな実験を行いました。

1)最初に、my_tableを作成しました:

CREATE TABLE my_table(
  "ID" NUMBER NOT NULL ENABLE, 
  "INVOICE_NO" VARCHAR2(10), 
  CONSTRAINT "test _PK" PRIMARY KEY ("ID")
)

2)次に、invoice_no列のインデックスを作成しました(これを使用してフィルタリングするため)。

CREATE INDEX "my_table_index1" ON my_table (invoice_no)

3)次に、100Kレコードを挿入します。

DECLARE
  mod_val NUMBER;
BEGIN
  FOR i IN 1..100000 LOOP
    mod_val := MOD(i,6);
    IF (mod_val = 0) THEN
      INSERT INTO my_table (ID, INVOICE_NO) VALUES (i, '5570-110');
    ELSIF (mod_val = 1) THEN
      INSERT INTO my_table (ID, INVOICE_NO) VALUES (i, '5570-111');
    ELSIF (mod_val = 2) THEN
      INSERT INTO my_table (ID, INVOICE_NO) VALUES (i, '5570-112');
    ELSIF (mod_val = 3) THEN
      INSERT INTO my_table (ID, INVOICE_NO) VALUES (i, '5570-113');
    ELSIF (mod_val = 4) THEN
      INSERT INTO my_table (ID, INVOICE_NO) VALUES (i, '5570-114');
    ELSIF (mod_val = 5) THEN
      INSERT INTO my_table (ID, INVOICE_NO) VALUES (i, '5570-115');
    END IF; 
  END LOOP;
  COMMIT;
END;

4)次に、1つのランダムレコードを更新しました(選択を強調するためだけに):

BEGIN
  UPDATE my_table SET INVOICENO = 'exception' WHERE id = 50000;
  COMMIT;
END;

5)次に、説明プランを使用して選択を実行しました。

EXPLAIN PLAN FOR
  SELECT * FROM my_table WHERE invoice_no = 'exception';

6)次に統計を取得しました:

 SELECT * FROM TABLE(dbms_xplan.display);

7)そして結果を得た:

"PLAN_TABLE_OUTPUT"
"Plan hash value: 3804444429"
" "
"------------------------------------------------------------------------------"
"| Id  | Operation         | Name     | Rows  | Bytes | Cost (%CPU)| Time     |"
"------------------------------------------------------------------------------"
"|   0 | SELECT STATEMENT  |          | 83256 |  1626K|   103   (1)| 00:00:02 |"
"|   1 |  TABLE ACCESS FULL| MY_TABLE | 83256 |  1626K|   103   (1)| 00:00:02 |"
"------------------------------------------------------------------------------"
" "
"Note"
"-----"
"   - dynamic sampling used for this statement (level=2)"

結論:それは奇妙で「魔法」の匂いがします。なぜOracleはinvoice_noフィールドのインデックスを使用しないことに決め、83256レコードをスキャンしたのでしょうか。私のラップトップは同時ユーザーで過負荷にならないことに同意します。テーブルのサイズはそれほど大きくありません(数値とvarcharが含まれています)が、この魔法は好きではなく、そのような動作の理由を知りたいです:)

更新:すべてのレコードのinvoice_noフィールドにダミー値(以下を参照)を追加しました-テーブルのサイズを大きくするためですが、テーブルの全表スキャンは残ります:

UPDATE2:分析テーブルも実行しましたが、結果は同じです:

ANALYZE TABLE my_table COMPUTE STATISTICS;

UPDATE3:インデックスの使用を強制しようとしましたが、結果は同じです(構文が間違っている可能性がありますか?):

EXPLAIN PLAN FOR 
  SELECT /*+ INDEX(my_table my_table_index1) */ * FROM my_table t WHERE invoice_no = 'exception'

UPDATE4:最後に、インデックスを使用するように「Oracleに指示」することができました-新しいgathertablestatsプロシージャを実行しました。

BEGIN
  DBMS_STATS.GATHER_TABLE_STATS ( OWNNAME=>user
                             , TABNAME=>'my_table');
END;

Explainプランの出力は次のとおりです。

"--------------------------------------------------------------------------------------"
"| Id  | Operation                   | Name            | Rows  | Bytes | Cost (%CPU)| Time     |"
"-----------------------------------------------------------------------------------------------"
"|   0 | SELECT STATEMENT            |                 |     1 |   294 |     5   (0)| 00:00:01 |"
"|   1 |  TABLE ACCESS BY INDEX ROWID| MY_TABLE        |     1 |   294 |     5   (0)| 00:00:01 |"
"|*  2 |   INDEX RANGE SCAN          | my_table_index1 |     1 |       |     4   (0)| 00:00:01 |"
"-----------------------------------------------------------------------------------------------"
" "
"Predicate Information (identified by operation id):"
"---------------------------------------------------"
" "
"   2 - access(""INVOICE_NO""='exception')"

したがって、Oracleはある時点で何らかのクエリ手法を使用することを決定し、状況が変わっても更新していないようです。私はそれに同意しますが、選択を作成、挿入、実行したときに、このテストケースに適切なアプローチが選択されなかったのは不思議です。Oracleに最適なクエリ手法を使用するように指示するには、少なくとも最初は常にDBMS_STATS.GATHER_TABLE_STATSを実行する必要がありますか?

4

3 に答える 3

2

最初にテーブルを作成するとき、。には7つの異なる値しかありませんINVOICE_NO。したがって、デフォルトでは、述語を指定するだけのテーブルに対するクエリは、INVOICE_NO7行ごとに約1行(行の約14.3%)を返すと予想されます。これは、通常、テーブルスキャンの方が効率的であることを意味します。インデックススキャンよりも(正確なカットオフポイントは多くの異なるパラメータに依存します。一部のシステムでは、行の15%を取得することが予想される場合、インデックススキャンを選択する可能性があります)。

最初にクエリを実行したとき、テーブルに統計がなかったため、Oracleは動的サンプリングを実行することを余儀なくされました(クエリプランのコメント「このステートメントに使用される動的サンプリング(レベル= 2)」に注意してください)。これは、オプティマイザーのいくつかの基本的な統計を非常に迅速に収集するように設計されています。ただし、動的サンプリングは、精度ではなく速度を最適化するように設計されているため、統計の品質は一般に最適とは言えません。最初の例では、Oracleはクエリが83256行(全体の83.2%)を返すと推定しています。これは、テーブルの行数を過大評価し、INVOICE_NO列の個別の値の数を過小評価したことを意味します。

を使用して統計を収集しましたか

BEGIN
  DBMS_STATS.GATHER_TABLE_STATS ( OWNNAME=>user
                             , TABNAME=>'my_table');
END;

手順4の後、手順5の前に、デフォルト設定を変更していないと仮定すると、DBMS_STATS統計は改善されますが、(ほとんどの場合)テーブルスキャンが実行されます。Oracleは、14286行(7行のうち1行)と見積もっています。

SQL> SELECT * FROM TABLE(dbms_xplan.display);

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------
Plan hash value: 3804444429

------------------------------------------------------------------------------
| Id  | Operation         | Name     | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |          | 14286 |   195K|   104   (2)| 00:00:02 |
|*  1 |  TABLE ACCESS FULL| MY_TABLE | 14286 |   195K|   104   (2)| 00:00:02 |
------------------------------------------------------------------------------

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

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

   1 - filter("INVOICE_NO"='exception')

INVOICE_NOより良い計画を立てるためには、列にヒストグラムが必要です。INVOIE_NOこれにより、のデータが均等に分散されていないため、一部の値(つまり「例外」)が他の列よりもはるかに選択的であることがOracleに通知されます。統計を収集するときに、個々の列、すべてのインデックス付き列でヒストグラムを収集するように指定するか、Oracleがヒストグラムを必要とする列を自動的に判別するように指定できます(すぐに戻ります)。 )。Oracleにすべてのインデックス付き列のヒストグラムを収集させる場合は、

SQL> exec dbms_stats.gather_table_stats( 'SCOTT', 
                                         'MY_TABLE', 
                                          method_opt => 'FOR ALL INDEXED COLUMNS SIZE 254' );

PL/SQL procedure successfully completed.

の個別の値が255以下であると仮定するとINVOICE_NO、このヒストグラムにより、Oracleは各個別の値がどれほど一般的であるかを正確に追跡できます(255を超える個別の値がある場合、Oracleは隣接する値を組み合わせる必要があり、ヒストグラムの精度が低下する可能性があります) 。

デフォルトのOracle10.2または11.2インストールでは、デフォルトmethod_opt設定は「FOR ALLCOLUMNSSIZEAUTO」になります。これにより、Oracleは、適切であると判断した列のヒストグラムを収集するようになります。これを行うために、Oracleは、データの分布が大きく偏っており、その列が述語に表示されている列を探します。そのため、前に、手順4と5の間で統計を収集することについて話していたとき、Oracleはヒストグラムを収集しませんでしたINVOICE_NO。これは、データが歪んでいることはわかっていても、その列に基づいてテーブルをクエリすることを知らなかったためです。 。

手順7の後、まったく同じコマンドを使用して統計を再度収集した場合

BEGIN
  DBMS_STATS.GATHER_TABLE_STATS ( OWNNAME=>user
                             , TABNAME=>'my_table');
END;

次に、Oracleは、共有プールにMY_TABLE述語があるクエリがあったことを確認します。INVOICE_IDこれにより、INVOICE_NO両方の条件を満たすことでヒストグラムを取得できるため、今回はでヒストグラムを収集しINVOICE_NOます。これにより、オプティマイザは、クエリが1行しか返さないこと、およびインデックススキャンが最も効率的なプランであることを認識できます。

SQL> SELECT * FROM TABLE(dbms_xplan.display);

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
Plan hash value: 3377519735

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

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

   2 - access("INVOICE_NO"='exception')

したがって、幸いなことに、Oracleは、このクエリの最適な計画を作成するためにヒストグラムが必要であると最終的に判断できるほど賢いです。悪いニュースは、テーブルにデータを入力するときに必要に応じてヒストグラムを含む統計を収集してOracleにこれを伝えないと、Oracleが必要なものを理解するまで計画が不十分になる可能性があることです。

実際のシステムでは、通常、クエリの大部分でリテラルではなくバインド変数を使用します。ヒストグラムのある列に対するクエリでバインド変数を使用すると、新しい一連の問題が発生します。アプリケーションにクエリがある場合

 SELECT * 
   FROM my_table 
  WHERE invoice_no = :1;

値「5570-110」をバインドする場合はテーブルスキャンが必要ですが、「例外」の値をバインドする場合はインデックススキャンが必要です。Oracle 10.2では、Oracleはバインド変数のピークを実行します。つまり、Oracleがハード解析を実行すると、バインド変数の値をピークし、そのバインド値を最適化するプランを生成します。残念ながら、10gでは、クエリごとに1つのプランしか持てないため、一度に2つのケースのいずれかに対して最適なプランしか取得できません。どちらのプランを取得するかは、最初に検出されたバインド値の運によって異なります。11gでは、Oracleがさまざまなバインド変数値に対して複数のクエリプランを維持するアダプティブカーソル共有を取得できますが、これにより、注意が必要な複雑さが増します。

余談ですが、インデックスで大文字と小文字を区別する名前を使用したため、ヒントは機能しませんでした。ヒントでは、大文字と小文字を区別するインデックス名を使用する必要があります。また、テーブル名ではなくエイリアス名を使用する必要があります

SELECT /*+ INDEX(t "my_table_index1") */ * 
  FROM my_table t 
 WHERE invoice_no = 'exception'

これは、大文字と小文字を区別する識別子を使用することが一般的に大きな問題となる(多くの)理由の1つです。

于 2012-10-15T02:39:25.387 に答える
1

Oracleに最適なクエリ手法を使用するように指示するには、少なくとも最初は常にDBMS_STATS.GATHER_TABLE_STATSを実行する必要がありますか?

いいえ、必ずしもそうとは限りません。

Oracleはそれを自動的に行います(オフにしていない限り)。ただし、デフォルトのインストールでは、統計は1日1回しか収集されません。したがって、大きな負荷がかかった直後は、統計は最新ではありません。ただし、11.xは、テーブルにデータを入力した直後にインデックスを使用することを期待していました。

したがって、データの部分を変更するたびに、またはさらに多くのテーブルを変更した場合dbms_stats.gather_table_stats()でも、実行することをお勧めします。dbms_stats.gather_schema_stats()

「1日1回」の戦略は、ほとんどのワークロードで十分に機能しますが、条件の変化が速い場合は、Oracleが統計を計算する方法のパラメータを調整することをお勧めします。

詳細については、マニュアルを参照してください:http: //docs.oracle.com/cd/E11882_01/server.112/e16638/stats.htm#g49431

于 2012-10-14T21:02:22.247 に答える
1

私はあなたのテストでこれを手に入れました、統計の更新なし:計画:

SELECT STATEMENT ALL_ROWS:コスト:1バイト:20カーディナリティ:1

TABLE ACCESS BY INDEX ROWID TABLE SYS.MY_TABLE:コスト:1バイト:20カーディナリティ:1

INDEX RANGE SCAN INDEX SYS.my_table_index1:コスト:1カーディナリティ:1

于 2012-10-14T21:02:47.193 に答える