Kami memiliki aplikasi yang menyimpan artikel dari berbagai sumber dalam tabel MySQL dan memungkinkan pengguna untuk mengambil artikel yang dipesan berdasarkan tanggal. Artikel selalu disaring berdasarkan sumber, jadi untuk SELECT klien selalu kami miliki
WHERE source_id IN (...,...) ORDER BY date DESC/ASC
Kami menggunakan IN, karena pengguna memiliki banyak langganan (beberapa memiliki ribuan).
Berikut adalah skema tabel artikel:
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='';
Kita memerlukan indeks (tanggal), karena kadang-kadang kita menjalankan operasi latar belakang pada tabel ini tanpa memfilter berdasarkan sumber. Namun pengguna tidak dapat melakukan ini.
Tabel ini memiliki sekitar 1 Miliar catatan (ya, kami sedang mempertimbangkan sharding untuk masa depan ...). Kueri yang khas terlihat seperti ini:
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
Mengapa FORCE INDEX? Karena ternyata MySQL terkadang memilih untuk menggunakan indeks (tanggal) untuk pertanyaan seperti itu (mungkin karena panjangnya lebih kecil?) Dan ini menghasilkan pindaian jutaan catatan. Jika kita menghapus FORCE INDEX dalam produksi, inti server CPU server kami akan maksimal dalam hitungan detik (Ini adalah aplikasi OLTP dan pertanyaan seperti di atas dijalankan dengan kecepatan sekitar 2000 per detik).
Masalah dengan pendekatan ini adalah bahwa beberapa kueri (kami menduga itu entah bagaimana terkait dengan jumlah source_ids dalam klausa IN) benar-benar berjalan lebih cepat dengan indeks tanggal. Ketika kami menjalankan EXPLAIN pada yang kami lihat bahwa indeks source_id_date memindai puluhan juta catatan, sedangkan indeks tanggal hanya memindai beberapa ribu. Biasanya sebaliknya, tetapi kita tidak dapat menemukan hubungan yang solid.
Idealnya kami ingin mencari tahu mengapa pengoptimal MySQL memilih indeks yang salah dan menghapus pernyataan FORCE INDEX, tetapi cara untuk memprediksi kapan memaksakan indeks tanggal juga akan bekerja untuk kami.
Beberapa klarifikasi:
Permintaan SELECT di atas sangat disederhanakan untuk keperluan pertanyaan ini. Ini memiliki beberapa GABUNG ke tabel dengan sekitar 100 Juta baris masing-masing, bergabung dengan PK (articles_user_flags.id = article.id), yang memperburuk masalah ketika ada jutaan baris untuk disortir. Juga beberapa pertanyaan memiliki tambahan di mana, misalnya:
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
Kueri ini mencantumkan hanya artikel yang berkilau bintangnya untuk pengguna tertentu (1).
Server menjalankan MySQL versi 5.5.32 (Percona) dengan XtraDB. Perangkat kerasnya 2xE5-2620, 128GB RAM, 4HDDx1TB RAID10 dengan pengontrol yang didukung baterai. SELECT yang bermasalah sepenuhnya terikat dengan CPU.
my.cnf adalah sebagai berikut (menghapus beberapa arahan yang tidak terkait seperti server-id, port, dll ...):
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
Seperti yang diminta, berikut adalah beberapa PENJELASAN pertanyaan yang bermasalah:
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 sebenarnya membutuhkan waktu sekitar satu menit dan sepenuhnya terikat CPU. Ketika saya mengubah indeks ke (tanggal) yang dalam hal ini pengoptimal MySQL juga memilih secara otomatis:
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)
Dan SELECT hanya membutuhkan 10 ms.
Tetapi MENJELASKAN bisa sangat rusak di sini! Sebagai contoh jika saya MENJELASKAN kueri dengan hanya satu source_id dalam klausa IN dan indeks paksa pada (tanggal) itu memberitahu saya bahwa itu akan memindai hanya 20 baris, tetapi itu tidak mungkin, karena tabel memiliki lebih dari 1 Miliar baris dan hanya beberapa cocok dengan source_id ini.
date
apakah DOUBLE
...?
EXPLAIN
?ANALYZE
adalah sesuatu yang berbeda, dan mungkin sesuatu yang perlu dipertimbangkan jika Anda belum, karena satu penjelasan yang mungkin adalah bahwa statistik indeks miring mengganggu pengoptimal dari memilih dengan bijak. Saya tidak berpikir ada kebutuhan untuk my.cnf dalam pertanyaan, dan ruang yang mungkin lebih baik digunakan untuk mengirim beberapaEXPLAIN
output dari variasi perilaku yang Anda lihat ... setelah Anda menyelidikiANALYZE [LOCAL] TABLE
...