次の構造とインデックスを持つ「customer」という名前のテーブルがあります。
DESCRIBE customer;
+--------------------+---------------+------+-----+-------------------+-----------------------------+
| Field | Type | Null | Key | Default | Extra |
+--------------------+---------------+------+-----+-------------------+-----------------------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| email | varchar(70) | NO | UNI | NULL | |
| orders | int(11) | YES | | NULL | |
| country | text | YES | | NULL | |
| last_date_order | datetime | YES | | NULL | |
| first_date_order | datetime | YES | | NULL | |
| name | varchar(100) | YES | | NULL | |
| lastname | varchar(100) | YES | | NULL | |
| male | tinyint(1) | YES | | NULL | |
| gender_probability | decimal(10,2) | YES | | NULL | |
| phone | varchar(20) | YES | | NULL | |
| phone_formatted | varchar(20) | YES | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | NO | MUL | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
+--------------------+---------------+------+-----+-------------------+-----------------------------+
SHOW INDEX FROM customer;
+----------+------------+------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+----------+------------+------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| customer | 0 | PRIMARY | 1 | id | A | 1899746 | NULL | NULL | | BTREE | | |
| customer | 0 | email | 1 | email | A | 1899746 | NULL | NULL | | BTREE | | |
| customer | 1 | updated_at | 1 | updated_at | A | 200 | NULL | NULL | | BTREE | | |
+----------+------------+------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
約15秒かかるこのクエリを実行できます。
SELECT * FROM customer WHERE updated_at >= '2018-02-16 00:00:00';
931817 rows in set (14.33 sec)
これは同じクエリですが、order by
:
SELECT * FROM customer WHERE updated_at >= '2018-02-16 00:00:00' ORDER BY updated_at ASC;
931817 rows in set (5 min 21.17 sec)
5分は遅すぎると思います、クエリの説明は次のように示しています:
EXPLAIN SELECT * FROM customer WHERE updated_at >= '2018-02-16 00:00:00' ORDER BY updated_at ASC;
+----+-------------+----------+------+---------------+------+---------+------+---------+-----------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+----------+------+---------------+------+---------+------+---------+-----------------------------+
| 1 | SIMPLE | customer | ALL | updated_at | NULL | NULL | NULL | 1899744 | Using where; Using filesort |
+----+-------------+----------+------+---------------+------+---------+------+---------+-----------------------------+
プロファイルはこれを言います:
SET profiling = 1;
SELECT * FROM customer WHERE updated_at >= '2018-02-16 00:00:00' ORDER BY updated_at ASC;
SHOW PROFILE;
SET profiling = 0;
+------------------------------+------------+
| Status | Duration |
+------------------------------+------------+
| Sending data | 0.036616 |
| Waiting for query cache lock | 0.000034 |
| Sending data | 0.088051 |
| Waiting for query cache lock | 0.000034 |
| Sending data | 0.064104 |
| Waiting for query cache lock | 0.000038 |
| Sending data | 0.045420 |
| Waiting for query cache lock | 0.000043 |
| Sending data | 0.035219 |
| Waiting for query cache lock | 0.000025 |
| Sending data | 0.049755 |
| Waiting for query cache lock | 0.000035 |
| Sending data | 0.069008 |
| Waiting for query cache lock | 0.000036 |
| Sending data | 0.068619 |
| Waiting for query cache lock | 0.000042 |
| Sending data | 0.067039 |
| Waiting for query cache lock | 0.000048 |
| Sending data | 0.051564 |
| Waiting for query cache lock | 0.000042 |
| Sending data | 0.052017 |
| Waiting for query cache lock | 0.000036 |
| Sending data | 0.049105 |
| Waiting for query cache lock | 0.000034 |
| Sending data | 0.060379 |
| Waiting for query cache lock | 0.000029 |
| Sending data | 0.118096 |
| Waiting for query cache lock | 0.000027 |
| Sending data | 0.046678 |
| Waiting for query cache lock | 0.000037 |
| Sending data | 0.058142 |
| Waiting for query cache lock | 0.000027 |
| Sending data | 0.137221 |
| Waiting for query cache lock | 0.000046 |
| Sending data | 0.061968 |
| Waiting for query cache lock | 0.000045 |
| Sending data | 0.054456 |
| Waiting for query cache lock | 0.000045 |
| Sending data | 0.054738 |
| Waiting for query cache lock | 0.000025 |
| Sending data | 0.073921 |
| Waiting for query cache lock | 0.000028 |
| Sending data | 0.040226 |
| Waiting for query cache lock | 0.000034 |
| Sending data | 0.036024 |
| Waiting for query cache lock | 0.000046 |
| Sending data | 0.063350 |
| Waiting for query cache lock | 0.000035 |
| Sending data | 0.044174 |
| Waiting for query cache lock | 0.000028 |
| Sending data | 0.057658 |
| Waiting for query cache lock | 0.000031 |
| Sending data | 0.081529 |
| Waiting for query cache lock | 0.000041 |
| Sending data | 0.074443 |
| Waiting for query cache lock | 0.000027 |
| Sending data | 0.058293 |
| Waiting for query cache lock | 0.000033 |
| Sending data | 0.061443 |
| Waiting for query cache lock | 0.000037 |
| Sending data | 0.075515 |
| Waiting for query cache lock | 0.000035 |
| Sending data | 0.075264 |
| Waiting for query cache lock | 0.000039 |
| Sending data | 0.049696 |
| Waiting for query cache lock | 0.000027 |
| Sending data | 0.052088 |
| Waiting for query cache lock | 0.000036 |
| Sending data | 0.049119 |
| Waiting for query cache lock | 0.000039 |
| Sending data | 0.053930 |
| Waiting for query cache lock | 0.000028 |
| Sending data | 0.064894 |
| Waiting for query cache lock | 0.000039 |
| Sending data | 0.046105 |
| Waiting for query cache lock | 0.000027 |
| Sending data | 0.061840 |
| Waiting for query cache lock | 0.000033 |
| Sending data | 0.065950 |
| Waiting for query cache lock | 0.000038 |
| Sending data | 0.053379 |
| Waiting for query cache lock | 0.000028 |
| Sending data | 0.034599 |
| Waiting for query cache lock | 0.000027 |
| Sending data | 0.046371 |
| Waiting for query cache lock | 0.000033 |
| Sending data | 0.039776 |
| Waiting for query cache lock | 0.000035 |
| Sending data | 0.046480 |
| Waiting for query cache lock | 0.000040 |
| Sending data | 0.039515 |
| Waiting for query cache lock | 0.000036 |
| Sending data | 405.206648 |
| end | 0.000040 |
| query end | 0.000011 |
| closing tables | 0.000017 |
| freeing items | 0.000036 |
| logging slow query | 0.000006 |
| logging slow query | 0.000011 |
| cleaning up | 0.000006 |
+------------------------------+------------+
100 rows in set (0.01 sec)
なぜorder by
はパフォーマンスに大きな影響を与えますか? 15秒から5分以上は大きなジャンプです。
RolandoMySQLDBAのクエリのEXPLAIN
SELECT B.* FROM (SELECT id FROM customer WHERE updated_at >= '2018-02-16 00:00:00' ORDER BY updated_at ASC) A LEFT JOIN customer B USING (id);
932016 rows in set (6 min 6.39 sec)
+----+-------------+------------+--------+---------------+------------+---------+------+--------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+--------+---------------+------------+---------+------+--------+--------------------------+
| 1 | PRIMARY | <derived2> | ALL | NULL | NULL | NULL | NULL | 932025 | |
| 1 | PRIMARY | B | eq_ref | PRIMARY | PRIMARY | 4 | A.id | 1 | |
| 2 | DERIVED | customer | range | updated_at | updated_at | 4 | NULL | 949956 | Using where; Using index |
+----+-------------+------------+--------+---------------+------------+---------+------+--------+--------------------------+
いくつかの説明...
SELECT * ... WHERE ...
オプティマイザーはテーブルの多くが必要であることを認識しているため、インデックスを無視してデータを読み取るだけです。
SELECT * ... WHERE ... ORDER BY ...
これで、オプティマイザーにはタッチの選択肢があります1)テーブルをスキャンし、100万行をソートします。 (2)インデックスをスキャンします(ソートを回避するため)が、インデックスとデータの間でバウンスする必要があります。どちらの方法もコストがかかります。
SELECT ... FROM ( SELECT id ... ) JOIN ...
100万のIDを持つ一時テーブルを作成する必要があるため、これは事態をさらに悪化させます。何も保存されていません。余分なものが追加されました。
PRIMARY KEY(updated_at, id)
id
を含めることにより、PKは一意になります(要件)。 ---(テーブルがInnoDBであると仮定、startingによるPKとupdated_at
、WHERE
およびORDER BY
は両方とも、データの「範囲」スキャンによって満たされます。そして、スキャンする必要があるのはテーブルの一部(約半分)だけです。
プロファイリングは本質的に役に立たない。時間の99%は、意味のない「データの送信」または1つまたは2つの他の不可解なフェーズに費やされています。
いくつかの定式化に役立つ可能性がある1つのこと:TEXT
はcountry
の過剰です。適切な短いVARCHAR
に変更します。これにより、一時テーブルを処理する少し高速な方法が可能になります。
質問の内容に基づいて
1899746
行931817
行> 2018-02-16 00:00:00
updated_at
のインデックスには200個の異なる値がありますEXPLAINプランは、フルテーブルスキャンと、すべての列を持つ一時テーブルの一種を示しています。 WHERE句はテーブル行の49.0496%を参照するため、これは理にかなっています。インデックスへのアクセスにはテーブルへのアクセスも必要であるというキー分布から把握するよりも、テーブル全体を実行する方が簡単です。これは、EXPLAINプランがupdated_at
インデックスを選択しなかった理由を説明しています。
updated_at
インデックスのキー値の分布は、これを実行することで確認できます
SELECT COUNT(1) rowcount,updated_at FROM customer GROUP BY updated_at;
インデックスを使用して最初にIDを収集し、後で結合することができます
SELECT B.* FROM
(SELECT id FROM customer
WHERE updated_at >= '2018-02-16 00:00:00'
ORDER BY updated_at ASC) A
LEFT JOIN customer B USING (id);
IDを収集するサブクエリがインデックスを選択する場合、クエリ全体が高速になる可能性があります。これに対してEXPLAINを実行し、これが当てはまるかどうかを確認してください。
これがアプリケーションの重要な部分である場合、PRIMARY KEY
をupdated_at
で始まるように変更できます(そして既存の主キーをUNIQUE
キー/制約に変更します)。
-- table/index reconstruction
-- this will take some time
-- but only needs to be done once
ALTER TABLE customer
DROP PRIMARY KEY,
DROP INDEX updated_at,
ADD PRIMARY KEY (updated_at, id),
ADD UNIQUE INDEX id_uq (id) ;
上記の変更により、テーブルのクラスタリングキーがupdated_at
に基づくようになるため、クエリには2つの利点があります。