ORDER BYに使用するには、選択したすべての列をインデックスでカバーする必要がありますか?


15

SOで、誰かが最近インデックスを使用してORDER BYを使用しないのなぜかと尋ねました

この状況には、3つの列と1万行からなるMySQLの単純なInnoDBテーブルが含まれていました。列の1つである整数にインデックスが付けられ、OPはその列でソートされたテーブル全体を取得しようとしました。

SELECT * FROM person ORDER BY age

彼はEXPLAIN、このクエリがfilesort(インデックスではなく)で解決されたことを示す出力を添付し、その理由を尋ねました。

インデックスが使用される原因となるヒントに もかかわらず、誰かが(他からのコメント/賛成をサポートして)選択された列がすべてインデックスから読み取られたときにソートにのみ使用されると回答しました(つまり、通常は列に出力)。インデックスを走査してからテーブルからカラムをフェッチすると、ランダムI / Oが発生するという説明が後で与えられました。FORCE INDEX FOR ORDER BY (age) Using indexExtraEXPLAINfilesort

これが表示されますが、上のマニュアルの章の顔に飛ぶためにORDER BY最適化だけでなく、満足していることに強い印象伝え、ORDER BYインデックスからすると、確かに(追加のソートを実行することが好ましいが、filesortクイックソートとマージソートの組み合わせで、それゆえ 必要があります下の結合しました;順番にインデックスを歩きながらテーブルをシークする必要があります-したがって、これは完全に理にかなっています)が、次のように述べながら、この「最適化」の申し立てを無視することもできません。Ω(nlog n)O(n)

次のクエリは、インデックスを使用してORDER BYパーツを解決します。

SELECT * FROM t1
  ORDER BY key_part1,key_part2,... ;

私の読書では、これはまさにこの状況の場合です(ただし、明示的なヒントなしにインデックスが使用されていませんでした)。

私の質問は:

  • MySQLがインデックスを使用することを選択するために、選択されたすべての列にインデックスを付ける必要がありますか

    • ある場合、これはどこに文書化されていますか(もしあれば)?

    • そうでない場合、ここで何が起こっていましたか?

回答:


14

MySQLがインデックスを使用することを選択するために、選択されたすべての列にインデックスを付ける必要がありますか

インデックスを使用する価値があるかどうかを決定する要因があるため、これはロードされた質問です。

要因#1

特定のインデックスについて、重要な母集団は何ですか?言い換えると、インデックスに記録されているすべてのタプルのカーディナリティ(明確なカウント)は何ですか?

要因#2

どのストレージエンジンを使用していますか?必要なすべての列にインデックスからアクセスできますか?

次は何ですか ???

簡単な例を見てみましょう:2つの値(男性と女性)を保持するテーブル

インデックスの使用状況をテストして、このようなテーブルを作成しましょう

USE test
DROP TABLE IF EXISTS mf;
CREATE TABLE mf
(
    id int not null auto_increment,
    gender char(1),
    primary key (id),
    key (gender)
) ENGINE=InnODB;
INSERT INTO mf (gender) VALUES
('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
('M'),('M'),('M'),('M'),('F'),('F'),('M'),('M'),
('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
('F'),('M'),('M'),('M'),('M'),('M'),('M'),('M');
ANALYZE TABLE mf;
EXPLAIN SELECT gender FROM mf WHERE gender='F';
EXPLAIN SELECT gender FROM mf WHERE gender='M';
EXPLAIN SELECT id FROM mf WHERE gender='F';
EXPLAIN SELECT id FROM mf WHERE gender='M';

InnoDBのテスト

mysql> USE test
Database changed
mysql> DROP TABLE IF EXISTS mf;
Query OK, 0 rows affected (0.00 sec)

mysql> CREATE TABLE mf
    -> (
    ->     id int not null auto_increment,
    ->     gender char(1),
    ->     primary key (id),
    ->     key (gender)
    -> ) ENGINE=InnoDB;
Query OK, 0 rows affected (0.07 sec)

mysql> INSERT INTO mf (gender) VALUES
    -> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
    -> ('M'),('M'),('M'),('M'),('F'),('F'),('M'),('M'),
    -> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
    -> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
    -> ('F'),('M'),('M'),('M'),('M'),('M'),('M'),('M');
Query OK, 40 rows affected (0.06 sec)
Records: 40  Duplicates: 0  Warnings: 0

mysql> ANALYZE TABLE mf;
+---------+---------+----------+----------+
| Table   | Op      | Msg_type | Msg_text |
+---------+---------+----------+----------+
| test.mf | analyze | status   | OK       |
+---------+---------+----------+----------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT gender FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref   | rows | Extra                    |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | mf    | ref  | gender        | gender | 2       | const |    3 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT gender FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref   | rows | Extra                    |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | mf    | ref  | gender        | gender | 2       | const |   37 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT id FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref   | rows | Extra                    |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | mf    | ref  | gender        | gender | 2       | const |    3 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT id FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref   | rows | Extra                    |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | mf    | ref  | gender        | gender | 2       | const |   37 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)

mysql>

MyISAMをテストする

mysql> USE test
Database changed
mysql> DROP TABLE IF EXISTS mf;
Query OK, 0 rows affected (0.00 sec)

mysql> CREATE TABLE mf
    -> (
    ->     id int not null auto_increment,
    ->     gender char(1),
    ->     primary key (id),
    ->     key (gender)
    -> ) ENGINE=MyISAM;
Query OK, 0 rows affected (0.05 sec)

mysql> INSERT INTO mf (gender) VALUES
    -> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
    -> ('M'),('M'),('M'),('M'),('F'),('F'),('M'),('M'),
    -> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
    -> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
    -> ('F'),('M'),('M'),('M'),('M'),('M'),('M'),('M');
Query OK, 40 rows affected (0.00 sec)
Records: 40  Duplicates: 0  Warnings: 0

mysql> ANALYZE TABLE mf;
+---------+---------+----------+----------+
| Table   | Op      | Msg_type | Msg_text |
+---------+---------+----------+----------+
| test.mf | analyze | status   | OK       |
+---------+---------+----------+----------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT gender FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref   | rows | Extra                    |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | mf    | ref  | gender        | gender | 2       | const |    3 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT gender FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref   | rows | Extra                    |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | mf    | ref  | gender        | gender | 2       | const |   36 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT id FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref   | rows | Extra       |
+----+-------------+-------+------+---------------+--------+---------+-------+------+-------------+
|  1 | SIMPLE      | mf    | ref  | gender        | gender | 2       | const |    3 | Using where |
+----+-------------+-------+------+---------------+--------+---------+-------+------+-------------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT id FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra       |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
|  1 | SIMPLE      | mf    | ALL  | gender        | NULL | NULL    | NULL |   40 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)

mysql>

InnoDBの分析

データがInnoDBとしてロードされたとき、4つのEXPLAINプランすべてがgenderインデックスを使用したことに注意してください。要求されたデータがであったとしても、3番目と4番目のEXPLAIN計画はgenderインデックスを使用しましたid。どうして?ためidでありPRIMARY KEY、すべてのセカンダリインデックスの参照ポインタがバックアップにしているPRIMARY KEY(介しgen_clust_index)。

MyISAMの分析

データがMyISAMとしてロードされたとき、最初の3つのEXPLAINプランがgenderインデックスを使用したことに注意してください。4番目のEXPLAIN計画では、クエリオプティマイザーはインデックスをまったく使用しないことを決定しました。代わりに全表スキャンを選択しました。どうして?

DBMSに関係なく、クエリオプティマイザーは非常に単純な経験則に基づいて動作します:インデックスがルックアップの実行に使用される候補としてスクリーニングされている場合、クエリオプティマイザーは合計数の5%以上をルックアップする必要があると計算しますテーブルの行:

  • 取得に必要なすべての列が選択したインデックスにある場合、フルインデックススキャンが実行されます
  • それ以外の場合は全表スキャン

結論

適切なカバリングインデックスがない場合、または特定のタプルのキー人口がテーブルの5%を超える場合、6つのことが発生する必要があります。

  1. クエリのプロファイルを作成する必要があることに気付きます
  2. すべての検索WHEREGROUP BYこれらのクエリから、およびORDER BY`句を
  3. この順序でインデックスを作成する
    • WHERE 静的な値を持つ句の列
    • GROUP BY
    • ORDER BY
  4. 全表スキャンを回避する(賢明なWHERE句のないクエリ)
  5. 不正なキー集団の回避(または少なくともこれらの不正なキー集団のキャッシュ)
  6. テーブルに最適なMySQLストレージエンジン(InnoDBまたはMyISAM)を決定する

私は過去にこの5%の経験則について書きました。

更新2012-11-14 13:05 EDT

あなたの質問と元のSO投稿を振り返った。それから、Analysis for InnoDB前に言った私のことを考えました。personテーブルと一致します。どうして?

テーブルmfperson

  • ストレージエンジンはInnoDBです
  • 主キーは id
  • テーブルアクセスはセカンダリインデックスによる
  • テーブルがMyISAMの場合、まったく異なるEXPLAIN計画が表示されます

ここで、SO質問からのクエリを見てくださいselect * from person order by age\GWHERE句がないため、明示的に全表スキャンを要求しました。テーブルのデフォルトのソート順は、idauto_incrementのために(PRIMARY KEY)であり、gen_clust_index(別名Clustered Index)は内部ROWIDによって順序付けられます。インデックスで注文した場合、InnoDBセカンダリインデックスには各インデックスエントリに添付されたROWIDがあることに注意してください。これにより、毎回完全な行アクセスが内部的に必要になります。

ORDER BYInnoDBインデックスの編成方法に関するこれらの事実を無視すると、InnoDBテーブルでのセットアップはかなり困難な作業になる可能性があります。

そのSOクエリに戻ると、完全なテーブルスキャンが明示的に要求されたため、MySQLクエリオプティマイザーが正しいことを行いました(または、少なくとも、最も抵抗の少ないパスを選択しました)。InnoDBとSOクエリに関してはfilesort、各セカンダリインデックスエントリに対してgen_clust_indexを介したフルインデックススキャンと行ルックアップを実行するよりも、テーブル全体をスキャンしてからいくつかを実行する方がはるかに簡単です。

EXPLAINプランを無視するため、Index Hintsの使用を推奨していません。それにもかかわらず、InnoDBよりもデータを実際によく知っている場合は、特にWHERE句がないクエリでは、インデックスヒントに頼る必要があります。

更新2012-11-14 14:21 EDT

書籍MySQL Internalsの理解によると

ここに画像の説明を入力してください

202ページ7項は次のように述べています。

データはクラスター化インデックスと呼ばれる特別な構造に格納されますクラスター化インデックスは、キー値として機能する主キーとデータ部分の実際のレコード(ポインターではなく)を持つBツリーです。したがって、各InnoDBテーブルには主キーが必要です。指定されていない場合、通常はユーザーに表示されない特別な行ID列が追加され、主キーとして機能します。セカンダリキーには、レコードを識別するプライマリキーの値が格納されます。Bツリーコードはinnobase / btr / btr0btr.cにあります。

これが私が以前に述べた理由です:各セカンダリインデックスエントリに対してgen_clust_indexを介したフルインデックススキャンと行ルックアップを行うよりも、フルテーブルスキャンを実行してからファイルソートを実行する方がはるかに簡単です。InnoDBは毎回二重インデックス検索を実行します。それはある種残忍なように聞こえますが、それは事実です。繰り返しますが、WHERE条項の欠如を考慮してください。これ自体は、MySQL Query Optimizerが全テーブルスキャンを実行するためのヒントです。


ローランド、徹底的かつ詳細な回答をありがとうございます。ただし、インデックスの選択には関係がないようですFOR ORDER BY(この質問の特定のケースです)。質問では、この場合、ストレージエンジンはそうであると述べていましたInnoDB(そして、元のSOの質問は、10,000行が8つのアイテムにかなり均一に分散されていることを示しています。悲しいことに、私はこれが質問に答えるとは思わない。
-eggyal

最初の部分も私の最初の本能だったので、これは興味深いです(カーディナリティが良くなかったので、mysqlはフルスキャンを使用することを選択しました)。しかし、私が読めば読むほど、そのルールは最適化による順序付けには適用されないようでした。innodbクラスター化インデックスの主キーで順序付けしますか?この投稿は、主キーが最後に追加されることを示しているので、インデックスの明示的な列でソートが行われないのでしょうか?要するに、私はまだ困惑しています!
デレクダウニー

1
filesort選択は1つの単純な理由のためにクエリオプティマイザによって時に決定されました:それはあなたが持っているデータの予備知識を欠いています。インデックスヒントを使用するという選択(問題#2に基づく)で満足のいく実行時間が得られる場合は、ぜひとも試してください。私が提供した答えは、MySQL Query Optimizerがいかに気まぐれなものであるかを示すための学術的な演習であり、行動方針を提案することでした。
-RolandoMySQLDBA

1
私はこれと他の投稿を読んで再読みしましたが、すべてをカバーしているため(カバーインデックスではない)、主キーでのinnodbの順序に関係していることに同意するだけです。ORDER BY最適化ドキュメントページには、このInnoDB固有の奇妙な点に関する言及がないことに驚いています。とにかく、ローランドに+1
デレクダウニー

1
@eggyal これは今週書かれました。同じEXPLAINプランに注意してください。データセットがメモリに収まらない場合、フルスキャンに時間がかかります。
デレクダウニー

0

SOに関する別の質問へのDenisの回答から(許可を得て)適応:

すべてのレコード(またはほぼすべてのレコード)がクエリによってフェッチされるため、通常はインデックスがまったくない方が良いでしょう。その理由は、実際にはインデックスを読み取るのにいくらかの費用がかかるからです。

テーブル全体を対象とする場合、テーブルを順番に読み取り、メモリ内の行を並べ替えることが最も安価な計画です。数行だけが必要で、ほとんどがwhere句に一致する場合は、最小のインデックスに行くとうまくいきます。

理由を理解するには、関連するディスクI / Oを想像してください。

インデックスなしでテーブル全体が必要だとします。これを行うには、data_page1、data_page2、data_page3などを読み取り、テーブルの最後に到達するまで、関係するさまざまなディスクページにアクセスします。次に、ソートして戻ります。

インデックスなしで上位5行が必要な場合は、以前のようにテーブル全体を順番に読み取り、上位5行をヒープソートします。確かに、それはほんの一握りの行の多くの読み取りとソートです。

ここで、テーブル全体にインデックスが必要だとします。これを行うには、index_page1、index_page2などを順番に読み取ります。次に、たとえばdata_page3、data_page1、data_page3、data_page3、data_page2などに、完全にランダムな順序(ソートされた行がデータに表示される順序)でアクセスします。関係するIOにより、混乱全体を順番に読み取り、メモリ内のグラブバッグをソートするだけで済むようになります。

対照的に、インデックス付きテーブルの上位5行のみが必要な場合は、対照的に、インデックスの使用が正しい戦略になります。最悪のシナリオでは、メモリに5データページをロードして先に進みます。

優れたSQLクエリプランナーであるbtwは、データの断片化の程度に基づいて、インデックスを使用するかどうかを決定します。行を順番にフェッチすることがテーブル全体でズームすることを意味する場合、優れたプランナーはインデックスを使用する価値がないと判断する場合があります。対照的に、テーブルがその同じインデックスを使用してクラスタ化されている場合、行の順序が保証され、使用される可能性が高くなります。

しかし、その後、同じクエリを別のテーブルに結合し、その別のテーブルに小さなインデックスを使用できる非常に選択的なwhere句がある場合、プランナーは、たとえばfoo、テーブルを結合し、メモリ内でヒープをソートします。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.