MySQLはなぜこの順序で強制的にインデックスを無視するのですか?


14

私は実行しEXPLAINます:

mysql> explain select last_name from employees order by last_name;
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
| id | select_type | table     | type | possible_keys | key  | key_len | ref  | rows  | Extra          |
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
|  1 | SIMPLE      | employees | ALL  | NULL          | NULL | NULL    | NULL | 10031 | Using filesort |
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
1 row in set (0.00 sec)  

私のテーブルのインデックス:

mysql> show index from employees;  
+-----------+------------+---------------+--------------+---------------+-----------+-------------+----------+--------+------+------------+---------+---------------+  
| Table     | Non_unique | Key_name      | Seq_in_index | Column_name   | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |  
+-----------+------------+---------------+--------------+---------------+-----------+-------------+----------+--------+------+------------+---------+---------------+  
| employees |          0 | PRIMARY       |            1 | subsidiary_id | A         |           6 |     NULL | NULL   |      | BTREE      |         |               |  
| employees |          0 | PRIMARY       |            2 | employee_id   | A         |       10031 |     NULL | NULL   |      | BTREE      |         |               |  
| employees |          1 | idx_last_name |            1 | last_name     | A         |       10031 |      700 | NULL   |      | BTREE      |         |               |  
| employees |          1 | date_of_birth |            1 | date_of_birth | A         |       10031 |     NULL | NULL   | YES  | BTREE      |         |               |  
| employees |          1 | date_of_birth |            2 | subsidiary_id | A         |       10031 |     NULL | NULL   |      | BTREE      |         |               |  
+-----------+------------+---------------+--------------+---------------+-----------+-------------+----------+--------+------+------------+---------+---------------+  
5 rows in set (0.02 sec)  

last_nameにはインデックスがありますが、オプティマイザーはそれを使用しません。
私もです:

mysql> explain select last_name from employees force index(idx_last_name) order by last_name;  
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
| id | select_type | table     | type | possible_keys | key  | key_len | ref  | rows  | Extra          |  
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
|  1 | SIMPLE      | employees | ALL  | NULL          | NULL | NULL    | NULL | 10031 | Using filesort |  
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
1 row in set (0.00 sec)  

しかし、まだインデックスは使用されていません!ここで何が間違っていますか?
インデックスが存在するという事実と関係があるのNON_UNIQUEでしょうか?ところで、last_nameはVARCHAR(1000)

@RolandoMySQLDBAによって要求された更新

mysql> SELECT COUNT(DISTINCT last_name) DistinctCount FROM employees;  
+---------------+  
| DistinctCount |  
+---------------+  
|         10000 |  
+---------------+  
1 row in set (0.05 sec)  


mysql> SELECT COUNT(1) FROM (SELECT COUNT(1) Count500,last_name FROM employees GROUP BY last_name HAVING COUNT(1) > 500) A;  
+----------+  
| COUNT(1) |  
+----------+  
|        0 |  
+----------+  
1 row in set (0.15 sec)  

次の2つのクエリを実行してください:1)SELECT COUNT(DISTINCT last_name) DistinctCount FROM employees;2)SELECT COUNT(1) FROM (SELECT COUNT(1) Count500,last_name FROM employees GROUP BY last_name HAVING COUNT(1) > 500) A;。各カウントの結果は何ですか?
RolandoMySQLDBA

@RolandoMySQLDBA:私はあなたが求めた情報でOPを更新しました。
クラチラス

さらに2つのクエリ、1)SELECT COUNT(1) FullTableCount FROM employees;と2)を入力してくださいSELECT * FROM (SELECT COUNT(1) Count500,last_name FROM employees GROUP BY last_name HAVING COUNT(1) > 500) A LIMIT 10;
RolandoMySQLDBA

気にしないでください、私は私が必要なもので説明を見る。
RolandoMySQLDBA

2
@Cratylusは間違った答えを受け入れました。Michael
sqlbotの

回答:


6

問題#1

クエリを見てください

select last_name from employees order by last_name;

意味のあるWHERE句が表示されず、MySQL Query Optimizerも表示されません。インデックスを使用するインセンティブはありません。

問題#2

クエリを見てください

select last_name from employees force index(idx_last_name) order by last_name; 

インデックスを付けましたが、Query Opitmizerが引き継ぎました。以前にこの動作を見たことがあります(MySQLで特定のインデックスを強制的にJOINに使用させるにはどうすればよいですか?

なぜこれが起こるのですか?

WHERE句がない場合、クエリオプティマイザーは次のことを自身に伝えます。

  • これはInnoDBテーブルです
  • インデックス付きの列です
  • インデックスには、gen_clust_indexのrow_id (別名クラスター化インデックス)があります。
  • なぜインデックスを見る必要があるのですか?
    • ありません WHERE条項か?
    • 私はいつもテーブルに戻る必要がありますか?
  • InnoDBテーブルのすべての行はgen_clust_indexと同じ16Kブロックにあるため、代わりにフルテーブルスキャンを実行します。

クエリオプティマイザーは、最小の抵抗のパスを選択しました。

あなたは少しショックであることを行っているが、ここでは行く:あなたは、クエリオプティマイザはかなり違ったMyISAMテーブルを処理することを知っていますか?

あなたはおそらくHUHと言っているのでしょうか???? どうやって ????

MyISAMはデータを.MYDファイルに保存し、すべてのインデックスを.MYIファイルに保存します。

インデックスはデータとは異なるファイルにあるため、同じクエリは異なるEXPLAINプランを生成します。どうして ?その理由は次のとおりです。

  • 必要なデータ(last_name列)は既に.MYI
  • 最悪の場合、完全なインデックススキャンが行われます
  • 列にのみアクセスします last_nameインデックスからのみ
  • 不要なものをふるいにかける必要はありません
  • ソートのために一時ファイルの作成をトリガーしません

どうすればこれを確認できますか?別のストレージを使用すると異なるEXPLAINプラン(場合によってはより良いプラン)がどのように生成されるかについて、この作業理論をテストしました


1
-1 @Rolandoこの答えは、Michael-sqlbotの正しい答えほど正確ではありませんが、間違っています。たとえば、マニュアルでは次のように述べています。グループ化は、使用可能なインデックスの左端のプレフィックス(...)で行われます。また、あなたの投稿の他の声明のいくつかは議論の余地があります。この回答を削除するか、修正することをお勧めします。
奇跡173

この答えは正しくありません。ソートを回避する場合、WHERE句がなくてもインデックスは使用できます。
oysteing

19

実際、ここでの問題は、これがプレフィックスインデックスのように見えることです。質問にテーブル定義が表示されませんが、sub_part= 700ですか?列全体にインデックスを付けていないため、インデックスを並べ替えに使用することはできず、カバーインデックスとしても役立ちません。「一致する可能性がある」行を見つけるためにのみ使用できWHERE、サーバー層(ストレージエンジンの上)はさらに一致した行をフィルタリングする必要があります。あなたは本当に姓に1000文字が必要ですか?


説明のための更新:500行を超える列があり、各列にWebサイトのドメイン名がdomain_name VARCHAR(254) NOT NULLあり、インデックスがないテーブルテストテーブルがあります。

mysql> alter table keydemo add key(domain_name);
Query OK, 0 rows affected (0.17 sec)
Records: 0  Duplicates: 0  Warnings: 0

列全体にインデックスが付けられている場合、クエリはインデックスを使用します。

mysql> explain select domain_name from keydemo order by domain_name;
+----+-------------+---------+-------+---------------+-------------+---------+------+------+-------------+
| id | select_type | table   | type  | possible_keys | key         | key_len | ref  | rows | Extra       |
+----+-------------+---------+-------+---------------+-------------+---------+------+------+-------------+
|  1 | SIMPLE      | keydemo | index | NULL          | domain_name | 764     | NULL |  541 | Using index |
+----+-------------+---------+-------+---------------+-------------+---------+------+------+-------------+
1 row in set (0.01 sec)

そのため、ここでそのインデックスを削除し、domain_nameの最初の200文字にインデックスを付けます。

mysql> alter table keydemo drop key domain_name;
Query OK, 0 rows affected (0.11 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> alter table keydemo add key(domain_name(200));
Query OK, 0 rows affected (0.08 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> explain select domain_name from keydemo order by domain_name;
+----+-------------+---------+------+---------------+------+---------+------+------+----------------+
| id | select_type | table   | type | possible_keys | key  | key_len | ref  | rows | Extra          |
+----+-------------+---------+------+---------------+------+---------+------+------+----------------+
|  1 | SIMPLE      | keydemo | ALL  | NULL          | NULL | NULL    | NULL |  541 | Using filesort |
+----+-------------+---------+------+---------------+------+---------+------+------+----------------+
1 row in set (0.00 sec)

mysql>

出来上がり。

また、200文字のインデックスは、列の最長値よりも長いことに注意してください...

mysql> select max(length(domain_name)) from keydemo;
+--------------------------+
| max(length(domain_name)) |
+--------------------------+
|                       43 |
+--------------------------+
1 row in set (0.04 sec)

...それでも違いはありません。プレフィックス長で宣言されたインデックスは、定義では完全な列値を含まないため、ルックアップにのみ使用でき、並べ替えには使用できず、カバーインデックスとしても使用できません。

また、上記のクエリはInnoDBテーブルで実行されましたが、MyISAMテーブルで実行すると実質的に同じ結果が得られます。この場合の唯一の違いは、InnoDBがrowsがわずかにオフ(541)であるのに対し、MyISAMは正確な行数(563)を表示することです。

私はまだlast_name列は、おそらく必要以上に大きいが、それはまだであることを主張だろう可能性:あなたはInnoDBテーブルを使用してMySQLの5.5または5.6を実行している場合、インデックスに列全体

既定では、単一列インデックスのインデックスキーは最大767バイトです。同じ長さの制限は、すべてのインデックスキープレフィックスに適用されます。セクション13.1.13「CREATE INDEX構文」を参照してください。たとえば、文字セットと各文字の最大3バイトを想定して、TEXTor VARCHARカラムで255文字を超えるカラムプレフィックスインデックスでこの制限にUTF-8達する可能性があります。innodb_large_prefix構成オプションが有効になっInnoDBている場合、DYNAMICおよびCOMPRESSED行形式を使用するテーブルの場合、この長さ制限は3072バイトに引き上げられます。

http://dev.mysql.com/doc/refman/5.5/en/innodb-restrictions.html


興味深い視点。列はありますがvarchar(1000)、これはインデックスの最大許容値である750を超えています
-Cratylus

8
この答えは受け入れられるべきです。
ypercubeᵀᴹ

1
@ypercubeこの答えは私の答えよりも正確です。コメントに+1、この回答に+1。私の代わりにこれが受け入れられるように。
-RolandoMySQLDBA

1
@Timo、それは興味深い質問です...ここでは、コンテキストのために、おそらくこの回答へのリンクとともに、新しい質問として投稿することをお勧めします。以下からのポスト完全な出力EXPLAIN SELECT ...だけでなく、SHOW CREATE TABLE ...およびSELECT @@VERSION;バージョン間オプティマイザへの変更が関連する可能性があるため。
マイケル-sqlbot

1
上記のコメントで求めたように、(少なくとも5.7では)プレフィックスインデックス nullのインデックス付けに役立たないことを報告できます。
ティモ

2

コメントはフォーマットをサポートしないため、RolandoMySQL DBAはgen_clust_indexとinnodbについて話していたので、私は答えました。そして、これはinnodbベースのテーブルでは非常に重要です。これは、Cコードを分析できる必要があるため、通常のDBAの知識よりもさらに進んでいます。

Innodbを使用している場合は、常にプライマリキーまたはユニークキーを作成する必要があります。innodbを使用しない場合は、独自の生成されたROW_IDを使用します。

証明はCコードに基づいているため、簡単に説明してみましょう。

/**********************************************************************//**
Returns a new row id.
@return the new id */
UNIV_INLINE
row_id_t
dict_sys_get_new_row_id(void)
/*=========================*/
{
    row_id_t    id;

    mutex_enter(&(dict_sys->mutex));

    id = dict_sys->row_id;

    if (0 == (id % DICT_HDR_ROW_ID_WRITE_MARGIN)) {
          dict_hdr_flush_row_id();
    }

    dict_sys->row_id++;
    mutex_exit(&(dict_sys->mutex));
    return(id);
}

最初の問題

mutex_enter(&(dict_sys-> mutex));

この行により、1つのスレッドのみが同時にdict_sys-> mutexにアクセスできるようになります。すでに値がミューテックスされていたら...はいスレッドは待機する必要があるので、スレッドロックなどの素敵なランダム機能のようなものを取得するか、独自のプライマリキーまたはユニークキーのないテーブルがさらにある場合は、素敵な機能がありますinnodb ' table locking 'は、これがMyISAMがInnoDBに置き換えられた理由ではありません。これは、レコード/行ベースのロックと呼ばれる優れた機能がオフになっているためです。

第二の問題

(0 ==(id%DICT_HDR_ROW_ID_WRITE_MARGIN))

モジュロ(%)計算は、毎回再計算する必要があるため、バッチ挿入の場合は遅くなります。また、DICT_HDR_ROW_ID_WRITE_MARGIN(値256)は2のべき乗であるため、これははるかに高速になります。

(0 ==(id&(DICT_HDR_ROW_ID_WRITE_MARGIN-1))))

Cコンパイラが最適化するように設定されていて、最適なオプティマイザである場合、Cオプティマイザは「重い」コードをより軽いバージョンに修正します。

ストーリーのモットーは、常に独自のプライマリキーを作成するか、最初からテーブルを作成するときに一意のインデックスがあることを確認します。


行ベースのレプリケーションと、行IDがサーバー間で一貫していないという事実と、主キーを常に作成することに関するレイモンドのポイントがさらに重要です。

UNIQUEそれだけでは十分ではありません。一意のインデックスをPKに昇格させるには、NULL以外の列のみを含める必要があります。
リックジェームズ

「モジュロ(%)計算は遅い」-より重要なのはINSERT、この関数に費やされる時間の何パーセントかです。私は取るに足らないと思う。周りのシャベル列に努力コントラストなど時折ブロック分割を含むBツリーの操作、BUFFER_POOL上の様々なミューテックス、変更バッファものを、行う
リック・ジェームズ

第一の問題は、ほとんどのトラブルいくつかあるだけでなく、真@RickJames ..オーバーヘッドが非常に少ないかもしれないが、多くの小さな数字も(まだマイクロ最適でしょう)まで追加
レイモンドNijland
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.