さまざまなソースからの記事をMySQLテーブルに保存し、ユーザーがそれらの記事を日付順に取得できるようにするアプリケーションがあります。記事は常にソースによってフィルタリングされるため、クライアントSELECTの場合は常に
WHERE source_id IN (...,...) ORDER BY date DESC/ASC
INを使用しているのは、ユーザーが多くのサブスクリプションを持っているためです(一部は数千を持っている)。
次に、articlesテーブルのスキーマを示します。
CREATE TABLE `articles` (
`id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
`source_id` INTEGER(11) UNSIGNED NOT NULL,
`date` DOUBLE(16,6) NOT NULL,
PRIMARY KEY (`id`),
KEY `source_id_date` (`source_id`, `date`),
KEY `date` (`date`)
)ENGINE=InnoDB
AUTO_INCREMENT=1
CHARACTER SET 'utf8' COLLATE 'utf8_general_ci'
COMMENT='';
(日付)インデックスが必要です。ソースでフィルタリングせずにこのテーブルでバックグラウンド操作を実行している場合があるためです。ただし、ユーザーはこれを行うことはできません。
テーブルには約10億のレコードがあります(そうです、将来のためにシャーディングを検討しています...)。典型的なクエリは次のようになります。
SELECT a.id, a.date, s.name
FROM articles a FORCE INDEX (source_id_date)
JOIN sources s ON s.id = a.source_id
WHERE a.source_id IN (1,2,3,...)
ORDER BY a.date DESC
LIMIT 10
FORCE INDEXを使用する理由MySQLがそのようなクエリに(日付)インデックスを使用することを選択したことが判明したため(おそらく長さが短いためか?)、これにより数百万のレコードがスキャンされます。本番環境でFORCE INDEXを削除すると、データベースサーバーのCPUコアが数秒で最大になります(これはOLTPアプリケーションであり、上記のようなクエリは毎秒約2000のレートで実行されます)。
このアプローチの問題は、一部のクエリ(IN句のsource_idの数に何らかの関係があると思われる)が、日付インデックスを使用すると実際に高速に実行されることです。それらに対してEXPLAINを実行すると、source_id_dateインデックスは数千万のレコードをスキャンするのに対して、日付インデックスは数千しかスキャンしないことがわかります。通常は逆ですが、しっかりした関係は見つかりません。
理想的には、MySQLオプティマイザーが誤ったインデックスを選択してFORCE INDEXステートメントを削除する理由を見つけたかったのですが、日付インデックスを強制するタイミングを予測する方法も機能します。
いくつかの説明:
上記のSELECTクエリは、この質問のためにかなり単純化されています。これには、それぞれ約1億行のテーブルへのJOINがいくつかあり、PK(articles_user_flags.id = article.id)に結合されます。これにより、並べ替える行が数百万ある場合に問題が悪化します。また、一部のクエリには追加の場所があります。例:
SELECT a.id, a.date, s.name
FROM articles a FORCE INDEX (source_id_date)
JOIN sources s ON s.id = a.source_id
LEFT JOIN articles_user_flags auf ON auf.article_id=a.id AND auf.user_id=1
WHERE a.source_id IN (1,2,3,...)
AND auf.starred=1
ORDER BY a.date DESC
LIMIT 10
このクエリは、特定のユーザー(1)のスター付き記事のみを一覧表示します。
サーバーは、XtraDBを使用してMySQLバージョン5.5.32(Percona)を実行しています。ハードウェアは、2xE5-2620、128GB RAM、4HDDx1TB RAID10で、バッテリーバックアップ式コントローラーを備えています。問題のあるSELECTは完全にCPUバウンドです。
my.cnfは次のとおりです(server-id、portなどの無関係なディレクティブをいくつか削除しました)。
transaction-isolation = READ-COMMITTED
binlog_cache_size = 256K
max_connections = 2500
max_user_connections = 2000
back_log = 2048
thread_concurrency = 12
max_allowed_packet = 32M
sort_buffer_size = 256K
read_buffer_size = 128K
read_rnd_buffer_size = 256K
join_buffer_size = 8M
myisam_sort_buffer_size = 8M
query_cache_limit = 1M
query_cache_size = 0
query_cache_type = 0
key_buffer = 10M
table_cache = 10000
thread_stack = 256K
thread_cache_size = 100
tmp_table_size = 256M
max_heap_table_size = 4G
query_cache_min_res_unit = 1K
slow-query-log = 1
slow-query-log-file = /mysql_database/log/mysql-slow.log
long_query_time = 1
general_log = 0
general_log_file = /mysql_database/log/mysql-general.log
log_error = /mysql_database/log/mysql.log
character-set-server = utf8
innodb_flush_method = O_DIRECT
innodb_flush_log_at_trx_commit = 2
innodb_buffer_pool_size = 105G
innodb_buffer_pool_instances = 32
innodb_log_file_size = 1G
innodb_log_buffer_size = 16M
innodb_thread_concurrency = 25
innodb_file_per_table = 1
#percona specific
innodb_buffer_pool_restore_at_startup = 60
要求されたとおり、ここに問題のあるクエリのいくつかの説明があります:
mysql> EXPLAIN SELECT a.id,a.date AS date_double
-> FROM articles a
-> FORCE INDEX (source_id_date)
-> JOIN sources s ON s.id = a.source_id WHERE
-> a.source_id IN (...) --Around 1000 IDs
-> ORDER BY a.date LIMIT 20;
+----+-------------+-------+--------+-----------------+----------------+---------+---------------------------+----------+------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+--------+-----------------+----------------+---------+---------------------------+----------+------------------------------------------+
| 1 | SIMPLE | a | range | source_id_date | source_id_date | 4 | NULL | 13744277 | Using where; Using index; Using filesort |
| 1 | SIMPLE | s | eq_ref | PRIMARY | PRIMARY | 4 | articles_db.a.source_id | 1 | Using where; Using index |
+----+-------------+-------+--------+-----------------+----------------+---------+---------------------------+----------+------------------------------------------+
2 rows in set (0.01 sec)
実際のSELECTには約1分かかり、完全にCPUの制約を受けます。インデックスを(日付)に変更すると、この場合MySQLオプティマイザも自動的に選択します。
mysql> EXPLAIN SELECT a.id,a.date AS date_double
-> FROM articles a
-> FORCE INDEX (date)
-> JOIN sources s ON s.id = a.source_id WHERE
-> a.source_id IN (...) --Around 1000 IDs
-> ORDER BY a.date LIMIT 20;
+----+-------------+-------+--------+---------------+---------+---------+---------------------------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+--------+---------------+---------+---------+---------------------------+------+--------------------------+
| 1 | SIMPLE | a | index | NULL | date | 8 | NULL | 20 | Using where |
| 1 | SIMPLE | s | eq_ref | PRIMARY | PRIMARY | 4 | articles_db.a.source_id | 1 | Using where; Using index |
+----+-------------+-------+--------+---------------+---------+---------+---------------------------+------+--------------------------+
2 rows in set (0.01 sec)
そして、SELECTはわずか10msかかります。
しかし、EXPLAINはここでかなり壊れることがあります!たとえば、IN句に1つのsource_idのみを含むクエリをEXPLAINし、(日付)に強制インデックスを作成すると、20行しかスキャンされないことがわかりますが、これは不可能です。このsource_idと一致します。
innodb_stats_sample_pages パラメータの値を確認する場合があります。これは、インデックス統計を更新するときにMySQLがテーブルに対して実行するインデックスダイブの数を制御します。インデックス統計は、候補の結合プランのコストを計算するために使用されます。使用していたバージョンのデフォルト値は8でした。これを128に変更し、予期しない参加計画の数を減らしました。