7

MySQL のパフォーマンスの問題をトラブルシューティングしようとしているので、操作するテーブルの小さいバージョンを作成したいと考えていました。クエリに LIMIT 句を追加すると、約 2 秒 (完全な挿入の場合) から天文学的な時間 (42 分) になります。

mysql> select pr.player_id, max(pr.insert_date) as insert_date from player_record pr
inner join date_curr dc on pr.player_id = dc.player_id where pr.insert_date < '2012-05-15'
group by pr.player_id;
+------------+-------------+
| 1002395119 | 2012-05-14  |
...
| 1002395157 | 2012-05-14  |
| 1002395187 | 2012-05-14  |
| 1002395475 | 2012-05-14  |
+------------+-------------+
105776 rows in set (2.19 sec)

mysql> select pr.player_id, max(pr.insert_date) as insert_date from player_record pr
inner join date_curr dc on pr.player_id = dc.player_id where pr.insert_date < '2012-05-15' 
group by pr.player_id limit 1;
+------------+-------------+
| player_id  | insert_date |
+------------+-------------+
| 1000000080 | 2012-05-14  |
+------------+-------------+
1 row in set (42 min 23.26 sec)

mysql> describe player_record;
+------------------------+------------------------+------+-----+---------+-------+
| Field                  | Type                   | Null | Key | Default | Extra |
+------------------------+------------------------+------+-----+---------+-------+
| player_id              | int(10) unsigned       | NO   | PRI | NULL    |       |
| insert_date            | date                   | NO   | PRI | NULL    |       |
| xp                     | int(10) unsigned       | YES  |     | NULL    |       |
+------------------------+------------------------+------+-----+---------+-------+
17 rows in set (0.01 sec) (most columns removed)

player_record テーブルには 2,000 万行あるため、比較する特定の日付用にメモリ内に 2 つのテーブルを作成しています。

CREATE temporary TABLE date_curr 
(
      player_id INT UNSIGNED NOT NULL, 
      insert_date DATE,     
      PRIMARY KEY player_id (player_id, insert_date)
 ) ENGINE=MEMORY;
INSERT into date_curr 
SELECT  player_id, 
        MAX(insert_date) AS insert_date 
FROM player_record 
WHERE insert_date BETWEEN '2012-05-15' AND '2012-05-15' + INTERVAL 6 DAY
GROUP BY player_id;

CREATE TEMPORARY TABLE date_prev LIKE date_curr;
INSERT into date_prev 
SELECT pr.player_id,
       MAX(pr.insert_date) AS insert_date 
FROM  player_record pr 
INNER join date_curr dc 
      ON pr.player_id = dc.player_id 
WHERE pr.insert_date < '2012-05-15' 
GROUP BY pr.player_id limit 0,20000;

制限を使用しない場合、date_curr には 216,000 のエントリがあり、date_prev には 105,000 のエントリがあります。

これらのテーブルはプロセスの一部にすぎず、別のテーブル (5 億行) を管理しやすいものに切り詰めるために使用されます。date_curr には、現在の週の player_id と insert_date が含まれ、date_prev には、date_curr に存在する player_id の現在の週より前の player_id と最新の insert_date が含まれます。

説明の出力は次のとおりです。

mysql> explain SELECT pr.player_id, 
                      MAX(pr.insert_date) AS insert_date 
               FROM   player_record pr 
               INNER  JOIN date_curr dc 
                      ON pr.player_id = dc.player_id
               WHERE  pr.insert_date < '2012-05-15' 
               GROUP  BY pr.player_id 
               LIMIT  0,20000;                    
+----+-------------+-------+-------+---------------------+-------------+---------+------+--------+----------------------------------------------+
| id | select_type | table | type  | possible_keys       | key         | key_len | ref  | rows   | Extra                                        |
+----+-------------+-------+-------+---------------------+-------------+---------+------+--------+----------------------------------------------+
|  1 | SIMPLE      | pr    | range | PRIMARY,insert_date | insert_date | 3       | NULL     | 396828 | Using where; Using temporary; Using filesort |
|  1 | SIMPLE      | dc    | ALL   | PRIMARY             | NULL        | NULL    | NULL | 216825 | Using where; Using join buffer               |
+----+-------------+-------+-------+---------------------+-------------+---------+------+--------+----------------------------------------------+
2 rows in set (0.03 sec)

これは、データベース専用の 24G RAM を備えたシステム上にあり、現在はほとんどアイドル状態です。この特定のデータベースはテストであるため、完全に静的です。私はmysqlを再起動しましたが、それでも同じ動作をしています。

これは「show profile all」の出力で、ほとんどの時間が tmp テーブルへのコピーに費やされています。

| Status               | Duration   | CPU_user   | CPU_system | Context_voluntary | Context_involuntary | Block_ops_in | Block_ops_out | Messages_sent | Messages_received | Page_faults_major | Page_faults_minor | Swaps | Source_function       | Source_file   | Source_line |
| Copying to tmp table | 999.999999 | 999.999999 |   0.383941 |            110240 |               18983 |        16160 |           448 |             0 |                 0 |                 0 |                43 |     0 | exec                  | sql_select.cc |        1976 |
4

2 に答える 2

10

少し長い答えですが、これから何かを学んでいただければ幸いです。

したがって、Explain ステートメントの証拠に基づいて、MySQL クエリ オプティマイザーが使用できた可能性のある 2 つのインデックスが次のようにあることがわかります。

possible_keys
PRIMARY,insert_date 

ただし、MySQL クエリ オプティマイザは次のインデックスを使用することにしました。

key
insert_date

これは、MySQL クエリ オプティマイザーが間違ったインデックスを使用するまれなケースです。現在、これには考えられる原因があります。あなたは静的開発データベースで作業しています。おそらく、開発を行うためにこれを本番環境から復元したでしょう。

MySQL オプティマイザは、クエリで使用するインデックスを決定する必要がある場合、考えられるすべてのインデックスに関する統計を調べます。統計の詳細について は、 http://dev.mysql.com/doc/innodb-plugin/1.0/en/innodb-other-changes-statistics-estimation.htmlを参照してください。

したがって、テーブルを更新、挿入、および削除すると、インデックス統計が変更されます。静的データが原因で、MySQL サーバーが間違った統計を持ち、間違ったインデックスを選択した可能性があります。ただし、これは考えられる根本原因として、現時点では推測にすぎません。

次に、インデックスに飛び込みましょう。主キー インデックスと insert_date のインデックスを使用する 2 つの可能なインデックスがありました。MySQL は insert_date を使用しました。クエリの実行中、MySQL は常に 1 つのインデックスしか使用できないことに注意してください。主キー インデックスと insert_date インデックスの違いを見てみましょう。

主キー インデックス (別名クラスター化) に関する簡単な事実:

  1. 主キー インデックスは通常、データ行を含む btree 構造です。つまり、日付を含むテーブルです。

セカンダリ インデックス (クラスター化されていない) に関する簡単な事実:

  1. セカンダリ インデックスは通常、インデックスが作成されるデータ (インデックス内の列) と、主キー インデックス上のデータ行の場所へのポインターを含む btree 構造です。

これは微妙ですが大きな違いです。

テーブルを読んでいる主キーインデックスを読むときを説明しましょう。表も一次索引順です。したがって、値を見つけるには、1回の操作であるデータを読み取るインデックスを検索します。

セカンダリ インデックスを読み取るときは、インデックスを検索してポインターを見つけてから、主キー インデックスを読み取り、ポインターに基づいてデータを見つけます。これは基本的に 2 つの操作であり、セカンダリ インデックスを読み取る操作は、プライマリ キー インデックスを読み取る操作の 2 倍のコストがかかります。

あなたの場合、使用するインデックスとしてinsert_dateを選択したため、結合を行うためだけに2倍の作業を行っていました。それが問題の 1 です。

レコードセットを LIMIT すると、クエリの実行の最後の部分になります。MySQL は、ORDER BY および GROUP BY 条件に基づいてレコードセット全体をソートし (まだソートされていない場合)、必要な数のレコードを取得し、LIMIT BY セクションに基づいて送り返す必要があります。MySQL は、送信するレコードやレコード セット内の場所などを追跡するために多くの作業を行う必要があります。LIMIT BY にはパフォーマンス ヒットがありますが、読み取りに寄与する要因があるのではないかと思います。

GROUP BY を見てください。それは player_id によるものです。使用されるインデックスは insert_date です。GROUP BY は基本的にレコード セットを並べ替えますが、順序付けに使用するインデックスがないためです (インデックスはそれに含まれる列の順序で並べ替えられることに注意してください)。基本的に、player_id で並べ替え/順序を尋ねていましたが、使用されたインデックスは insert_date で並べ替えられました。

この手順により、基本的にセカンダリ インデックスとプライマリ インデックスの読み取りから返されたデータを取得し (2 つの操作を思い出してください)、それらを並べ替える必要があるファイル並べ替えの問題が発生しました。メモリ内で行うには非常にコストのかかる操作であるため、通常、並べ替えはディスク上で行われます。したがって、クエリ結果全体がディスクに書き込まれ、ソートが非常に遅くなり、結果が得られませんでした。

insert_date インデックスを削除することで、MySQL は主キー インデックスを使用するようになりました。これは、データが順序付けられている (ORDER BY/GROUP BY) player_id と insert_date であることを意味します。これにより、セカンダリ インデックスを読み取り、ポインタを使用してプライマリ キー インデックス、つまりテーブルを読み取る必要がなくなります。また、データは既にソートされているため、クエリの GROUP BY 部分を適用するときに MySQL はほとんど作業を行いません。

インデックスが削除された後に Explain ステートメントの結果を投稿できれば、おそらく私の考えを確認できるでしょう。そのため、間違ったインデックスを使用することで、結果がディスク上でソートされ、LIMIT BY が適切に適用されました。LIMIT BY を削除すると、MySQL は LIMIT BY を適用して何が返されたかを追跡する必要がないため、おそらくメモリ内でソートできます。LIMIT BY が原因で一時テーブルが作成された可能性があります。繰り返しますが、ステートメントの違い、つまり Explain の出力を見ずに言うのは困難です。

これにより、インデックスと、インデックスが諸刃の剣である理由について理解を深めることができれば幸いです。

于 2012-12-14T06:55:44.727 に答える
1

同じ問題がありました。私が追加FORCE INDEX (id)したとき、クエリの数ミリ秒に戻り、制限はありませんでしたが、同じ結果が得られました。

于 2016-05-08T13:00:17.190 に答える