Wir haben eine Anwendung, die Artikel aus verschiedenen Quellen in einer MySQL-Tabelle speichert und es Benutzern ermöglicht, die nach Datum geordneten Artikel abzurufen. Artikel werden immer nach Quelle gefiltert, daher haben wir für Client SELECTs immer
WHERE source_id IN (...,...) ORDER BY date DESC/ASC
Wir verwenden IN, weil Benutzer viele Abonnements haben (einige haben Tausende).
Hier ist das Schema der Artikeltabelle:
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='';
Wir benötigen den (Datums-) Index, da wir manchmal Hintergrundoperationen für diese Tabelle ausführen, ohne nach Quelle zu filtern. Benutzer können dies jedoch nicht tun.
Die Tabelle enthält ungefähr 1 Milliarde Datensätze (ja, wir erwägen, für die Zukunft zu shardieren ...). Eine typische Abfrage sieht folgendermaßen aus:
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
Warum FORCE INDEX? Da sich herausstellte, dass MySQL manchmal den (Datums-) Index für solche Abfragen verwendet (möglicherweise aufgrund seiner geringeren Länge?), Führt dies zu Scans von Millionen von Datensätzen. Wenn wir den FORCE INDEX in der Produktion entfernen, sind die CPU-Kerne unseres Datenbankservers in Sekunden voll (es handelt sich um OLTP-Anwendungen, und Abfragen wie die oben genannten werden mit einer Geschwindigkeit von etwa 2000 pro Sekunde ausgeführt).
Das Problem bei diesem Ansatz ist, dass einige Abfragen (wir vermuten, dass sie irgendwie mit der Anzahl der Quell-IDs in der IN-Klausel zusammenhängen) mit dem Datumsindex wirklich schneller ausgeführt werden. Wenn wir EXPLAIN für diese ausführen, sehen wir, dass der source_id_date-Index zig Millionen Datensätze scannt, während der Datumsindex nur einige Tausend scannt. Normalerweise ist es umgekehrt, aber wir können keine feste Beziehung finden.
Im Idealfall wollten wir herausfinden, warum der MySQL-Optimierer den falschen Index auswählt und die FORCE INDEX-Anweisung entfernt. Eine Möglichkeit, vorherzusagen, wann der Datumsindex erzwungen werden soll, funktioniert jedoch auch für uns.
Einige Klarstellungen:
Die obige SELECT-Abfrage ist für die Zwecke dieser Frage stark vereinfacht. Es verfügt über mehrere JOINs zu Tabellen mit jeweils etwa 100 Millionen Zeilen, die der PK (articles_user_flags.id = article.id) beigetreten sind. Dies verschlimmert das Problem, wenn Millionen von Zeilen sortiert werden müssen. Auch einige Abfragen haben zusätzliche wo, zB:
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
Diese Abfrage listet nur markierte Artikel für den jeweiligen Benutzer auf (1).
Auf dem Server wird MySQL Version 5.5.32 (Percona) mit XtraDB ausgeführt. Hardware ist 2xE5-2620, 128 GB RAM, 4HDDx1 TB RAID10 mit batteriegepuffertem Controller. Die problematischen SELECTs sind vollständig CPU-gebunden.
my.cnf lautet wie folgt (einige nicht verwandte Anweisungen wie Server-ID, Port usw. wurden entfernt):
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
Wie gewünscht, hier einige ERLÄUTERUNGEN zu den problematischen Abfragen:
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)
Das eigentliche SELECT dauert ungefähr eine Minute und ist vollständig CPU-gebunden. Wenn ich den Index auf (Datum) ändere, wählt in diesem Fall der MySQL-Optimierer auch automatisch:
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)
Und das SELECT dauert nur 10ms.
Aber EXPLAINs können hier sehr kaputt sein! Wenn ich beispielsweise eine Abfrage mit nur einer Quell-ID in der IN-Klausel und dem erzwungenen Index am (Datum) erkläre, wird mir mitgeteilt, dass nur 20 Zeilen gescannt werden. Dies ist jedoch nicht möglich, da die Tabelle mehr als 1 Milliarde Zeilen und nur wenige enthält stimme mit dieser source_id überein.
date
ist ein DOUBLE
...?
EXPLAIN
?ANALYZE
ist etwas anderes und sollte wahrscheinlich in Betracht gezogen werden, wenn Sie dies nicht getan haben. Eine mögliche Erklärung ist, dass verzerrte Indexstatistiken den Optimierer von einer klugen Auswahl ablenken. Ich glaube nicht, dass die my.cnf in der Frage benötigt wird, und dieser Speicherplatz könnte besser verwendet werden, um einigeEXPLAIN
Ausgaben der Verhaltensschwankungen zu veröffentlichen, die Sie sehen ... nachdem SieANALYZE [LOCAL] TABLE