インデックスの選択

ストレージ エンジンからのデータの読み取りは、SQL 実行中に最も時間のかかる手順の 1 つです。現在、TiDB はさまざまなストレージ エンジンとさまざまなインデックスからのデータの読み取りをサポートしています。クエリ実行のパフォーマンスは、適切なインデックスを選択するかどうかに大きく依存します。

このドキュメントでは、インデックスを選択してテーブルにアクセスする方法と、インデックスの選択を制御するいくつかの関連する方法を紹介します。

テーブルにアクセスする

インデックスの選択を紹介する前に、TiDB がテーブルにアクセスする方法、それぞれの方法のトリガー、それぞれの方法の違い、長所と短所を理解することが重要です。

テーブルにアクセスするための演算子

オペレータートリガー条件該当するシナリオ説明
PointGet / BatchPointGet1 つ以上の単一ポイント範囲内のテーブルにアクセスする場合。あらゆるシナリオトリガーされた場合、コプロセッサー・インターフェースを呼び出すのではなく、kvget インターフェースを直接呼び出して計算を実行するため、通常は最速のオペレーターと見なされます。
テーブルリーダーなしあらゆるシナリオ一般に、TiKVレイヤーからテーブル データを直接スキャンする最も効率の悪いオペレーターと見なされます。 _tidb_rowid列に範囲クエリがある場合、または選択できるテーブルにアクセスするための演算子が他にない場合にのみ選択できます。
テーブルリーダーテーブルには、TiFlash ノードにレプリカがあります。読み取る列は少なくなりますが、評価する行は多くなります。Tiflash は列ベースのストレージです。少数の列と多数の行を計算する必要がある場合は、この演算子を選択することをお勧めします。
索引リーダーテーブルには 1 つ以上のインデックスがあり、計算に必要な列はインデックスに含まれています。インデックスに対してより小さな範囲のクエリがある場合、またはインデックス付きの列に順序要件がある場合。複数のインデックスが存在する場合は、コスト見積もりに基づいて適切なインデックスが選択されます。
IndexLookupReaderテーブルに 1 つ以上のインデックスがあり、計算に必要な列がインデックスに完全に含まれていません。IndexReader と同じ。インデックスは計算列を完全にはカバーしていないため、TiDB はインデックスを読み取った後にテーブルから行を取得する必要があります。 IndexReader オペレーターと比較して追加のコストがあります。

ノート:

TableReader 演算子は_tidb_rowid列インデックスに基づいており、TiFlash は列ストレージ インデックスを使用するため、インデックスの選択はテーブルにアクセスするための演算子の選択です。

インデックスの選択規則

TiDB は、ルールまたはコストに基づいてインデックスを選択します。ベース ルールには、事前ルールとスカイライン プルーニングが含まれます。インデックスを選択するとき、TiDB は最初にプレルールを試します。インデックスが事前ルールを満たす場合、TiDB はこのインデックスを直接選択します。それ以外の場合、TiDB はスカイライン プルーニングを使用して不適切なインデックスを除外し、テーブルにアクセスする各オペレーターのコスト見積もりに基づいて、コストが最も低いインデックスを選択します。

ルールベースの選択

事前ルール

TiDB は、次のヒューリスティックな事前ルールを使用してインデックスを選択します。

  • ルール 1: インデックスが「完全一致の一意のインデックス + テーブルから行を取得する必要がない (つまり、インデックスによって生成されるプランが IndexReader オペレーターであることを意味します)」を満たす場合、TiDB はこのインデックスを直接選択します。

  • ルール 2: インデックスが「完全一致の一意のインデックス + テーブルから行を取得する必要性 (つまり、インデックスによって生成されるプランが IndexReader オペレーターであることを意味します)」を満たす場合、TiDB は行数が最小のインデックスを選択して、候補インデックスとしてテーブルから取得されます。

  • ルール 3: 「通常のインデックス + テーブルから行を取得する必要がない + 読み込む行数が一定のしきい値未満」を満たすインデックスの場合、TiDB は読み込む行数が最も少ないインデックスを選択します。候補インデックスとして読み取ります。

  • ルール 4: ルール 2 および 3 に基づいて候補インデックスが 1 つだけ選択される場合は、この候補インデックスを選択します。ルール 2 とルール 3 に基づいて 2 つの候補インデックスがそれぞれ選択された場合、読み込む行数 (インデックスを持つ行数 + テーブルから取得する行数) が少ない方のインデックスを選択します。

上記のルールの「完全一致のインデックス」は、インデックス付きの各列が等しい条件を持つことを意味します。 EXPLAIN FORMAT = 'verbose' ...ステートメントを実行するときに、事前ルールがインデックスに一致する場合、TiDB は、インデックスが事前ルールに一致することを示す NOTE レベルの警告を出力します。

次の例では、インデックスidx_bがルール 2 の「完全一致の一意のインデックス + テーブルから行を取得する必要がある」という条件を満たしているため、TiDB はインデックスidx_bをアクセス パスとして選択し、インデックスSHOW WARNINGは、インデックスidx_bは事前ルールに一致します。

mysql> CREATE TABLE t(a INT PRIMARY KEY, b INT, c INT, UNIQUE INDEX idx_b(b)); Query OK, 0 rows affected (0.01 sec) mysql> EXPLAIN FORMAT = 'verbose' SELECT b, c FROM t WHERE b = 3 OR b = 6; +-------------------+---------+---------+------+-------------------------+------------------------------+ | id | estRows | estCost | task | access object | operator info | +-------------------+---------+---------+------+-------------------------+------------------------------+ | Batch_Point_Get_5 | 2.00 | 8.80 | root | table:t, index:idx_b(b) | keep order:false, desc:false | +-------------------+---------+---------+------+-------------------------+------------------------------+ 1 row in set, 1 warning (0.00 sec) mysql> SHOW WARNINGS; +-------+------+-------------------------------------------------------------------------------------------+ | Level | Code | Message | +-------+------+-------------------------------------------------------------------------------------------+ | Note | 1105 | unique index idx_b of t is selected since the path only has point ranges with double scan | +-------+------+-------------------------------------------------------------------------------------------+ 1 row in set (0.00 sec)

スカイライン剪定

スカイライン プルーニングは、インデックスのヒューリスティック フィルタリング ルールであり、誤った推定によって引き起こされる誤ったインデックス選択の可能性を減らすことができます。インデックスを判断するには、次の 3 つのディメンションが必要です。

  • 索引付けされた列によってカバーされるアクセス条件の数。 「アクセス条件」は、列範囲に変換できる where 条件です。また、インデックス付きの列セットがカバーするアクセス条件が多いほど、この次元では優れています。

  • テーブルにアクセスするためにインデックスを選択するときに、テーブルから行を取得する必要があるかどうか (つまり、インデックスによって生成されるプランは IndexReader オペレーターまたは IndexLookupReader オペレーターです)。このディメンションでは、テーブルから行を取得しないインデックスの方が、取得するインデックスよりも優れています。テーブルから行を取得するために両方のインデックスに TiDB が必要な場合は、インデックス付きの列でカバーされるフィルター条件の数を比較します。フィルタリング条件とは、指標に基づいて判断できるwhereの条件を意味します。インデックスの列セットがより多くのアクセス条件をカバーする場合、テーブルから取得される行の数が少なくなり、インデックスはこの次元でより適切になります。

  • インデックスが特定の順序を満たすかどうかを選択します。インデックスの読み取りは特定の列セットの順序を保証できるため、クエリの順序を満たすインデックスは、このディメンションで満たさないインデックスよりも優れています。

上記の 3 つのディメンションについて、インデックスidx_aのパフォーマンスが 3 つのディメンションすべてでインデックスidx_bより悪くなく、1 つのディメンションでパフォーマンスがidx_bより優れている場合、 idx_aが優先されます。 EXPLAIN FORMAT = 'verbose' ...ステートメントを実行するときに、スカイライン プルーニングが一部のインデックスを除外する場合、TiDB は、スカイライン プルーニング除外後の残りのインデックスをリストする NOTE レベルの警告を出力します。

次の例では、インデックスidx_bidx_eは両方ともidx_b_cよりも劣っているため、スカイライン プルーニングによって除外されます。返されたSHOW WARNINGの結果は、スカイライン プルーニング後の残りのインデックスを示しています。

mysql> CREATE TABLE t(a INT PRIMARY KEY, b INT, c INT, d INT, e INT, INDEX idx_b(b), INDEX idx_b_c(b, c), INDEX idx_e(e)); Query OK, 0 rows affected (0.01 sec) mysql> EXPLAIN FORMAT = 'verbose' SELECT * FROM t WHERE b = 2 AND c > 4; +-------------------------------+---------+---------+-----------+------------------------------+----------------------------------------------------+ | id | estRows | estCost | task | access object | operator info | +-------------------------------+---------+---------+-----------+------------------------------+----------------------------------------------------+ | IndexLookUp_10 | 33.33 | 738.29 | root | | | | ├─IndexRangeScan_8(Build) | 33.33 | 2370.00 | cop[tikv] | table:t, index:idx_b_c(b, c) | range:(2 4,2 +inf], keep order:false, stats:pseudo | | └─TableRowIDScan_9(Probe) | 33.33 | 2370.00 | cop[tikv] | table:t | keep order:false, stats:pseudo | +-------------------------------+---------+---------+-----------+------------------------------+----------------------------------------------------+ 3 rows in set, 1 warning (0.00 sec) mysql> SHOW WARNINGS; +-------+------+------------------------------------------------------------------------------------------+ | Level | Code | Message | +-------+------+------------------------------------------------------------------------------------------+ | Note | 1105 | [t,idx_b_c] remain after pruning paths for t given Prop{SortItems: [], TaskTp: rootTask} | +-------+------+------------------------------------------------------------------------------------------+ 1 row in set (0.00 sec)

コスト見積もりベースの選択

スカイライン プルーニング ルールを使用して不適切なインデックスを除外した後、インデックスの選択は完全にコストの見積もりに基づいて行われます。テーブルにアクセスするコストの見積もりには、次の考慮事項が必要です。

  • ストレージ エンジン内のインデックス付きデータの各行の平均長。
  • インデックスによって生成されたクエリ範囲内の行数。
  • テーブルから行を取得するためのコスト。
  • クエリの実行中にインデックスによって生成された範囲の数。

これらの要因とコスト モデルに従って、オプティマイザーは、テーブルにアクセスするためのコストが最も低いインデックスを選択します。

コスト見積もりベースの選択に関する一般的なチューニングの問題

  1. 推定行数は正確ではありませんか?

    これは通常、統計が古いか不正確であることが原因です。 analyze tableステートメントを再実行するか、 analyze tableステートメントのパラメーターを変更できます。

  2. 統計は正確で、TiFlash からの読み取りは高速ですが、オプティマイザーが TiKV からの読み取りを選択するのはなぜですか?

    現時点では、TiFlash と TiKV を区別するためのコスト モデルはまだ大雑把です。 tidb_opt_seek_factorつのパラメーターの値を減らすことができます。そうすると、オプティマイザーは TiFlash を選択することを優先します。

  3. 統計は正確です。インデックス A はテーブルから行を取得する必要がありますが、実際には、テーブルから行を取得しないインデックス B よりも高速に実行されます。オプティマイザーがインデックス B を選択するのはなぜですか?

    この場合、テーブルから行を取得するには、コストの見積もりが大きすぎる可能性があります。 tidb_opt_network_factorつのパラメーターの値を減らすと、テーブルから行を取得するコストを削減できます。

制御指標の選択

インデックスの選択は、 オプティマイザーのヒントを介して単一のクエリで制御できます。

  • USE_INDEX / IGNORE_INDEXを指定すると、オプティマイザーは特定のインデックスを使用する/使用しないように強制できます。 FORCE_INDEXUSE_INDEXは同じ効果です。

  • READ_FROM_STORAGEを指定すると、オプティマイザはクエリを実行する特定のテーブルに対して TiKV / TiFlash ストレージ エンジンを選択するように強制できます。