Abbiamo un'applicazione che memorizza articoli da diverse fonti in una tabella MySQL e consente agli utenti di recuperare quegli articoli ordinati per data. Gli articoli vengono sempre filtrati per origine, quindi per i SELECT client abbiamo sempre
WHERE source_id IN (...,...) ORDER BY date DESC/ASC
Stiamo usando IN, perché gli utenti hanno molti abbonamenti (alcuni ne hanno migliaia).
Ecco lo schema della tabella degli articoli:
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='';
Abbiamo bisogno dell'indice (data), perché a volte eseguiamo operazioni in background su questa tabella senza filtrare per origine. Gli utenti tuttavia non possono farlo.
La tabella ha circa 1 miliardo di record (sì, stiamo prendendo in considerazione lo sharding per il futuro ...). Una query tipica è simile alla seguente:
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
Perché FORCE INDEX? Perché si è scoperto che MySQL a volte sceglie di utilizzare l'indice (data) per tali query (forse a causa della sua lunghezza inferiore?) E questo si traduce in scansioni di milioni di record. Se rimuoviamo l'INDICE FORZA in produzione, i core della CPU del nostro server di database vengono massimizzati in pochi secondi (si tratta di applicazioni OLTP e query come quelle sopra vengono eseguite a velocità di circa 2000 al secondo).
Il problema con questo approccio è che alcune query (sospettiamo che siano in qualche modo correlate al numero di source_ids nella clausola IN) funzionano davvero più velocemente con l'indice della data. Quando eseguiamo EXPLAIN su quelli vediamo che l'indice source_id_date analizza decine di milioni di record, mentre l'indice della data analizza solo alcune migliaia. Di solito è il contrario, ma non possiamo trovare una relazione solida.
Idealmente, volevamo scoprire perché MySQL Optimizer sceglie l'indice errato e rimuove l'istruzione FORCE INDEX, ma un modo per prevedere quando forzare l'indice delle date funzionerà anche per noi.
Alcuni chiarimenti:
La query SELECT sopra è molto semplificata ai fini di questa domanda. Ha diversi JOIN su tabelle con circa 100 milioni di righe ciascuna, uniti al PK (articles_user_flags.id = article.id), che aggrava il problema quando ci sono milioni di righe da ordinare. Inoltre alcune query hanno ulteriori dove, ad esempio:
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
Questa query elenca solo articoli speciali per l'utente specifico (1).
Il server esegue MySQL versione 5.5.32 (Percona) con XtraDB. L'hardware è 2xE5-2620, 128 GB di RAM, 4HDDx1TB RAID10 con controller a batteria. I SELECT problematici sono completamente associati alla CPU.
my.cnf è il seguente (rimosse alcune direttive non correlate come id server, porta, ecc ...):
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
Come richiesto, ecco alcuni SPIEGAZIONE delle domande problematiche:
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)
L'attuale SELECT richiede circa un minuto ed è completamente associato alla CPU. Quando cambio l'indice in (data) che in questo caso l'ottimizzatore MySQL sceglie anche automaticamente:
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)
E il SELEZIONA richiede solo 10ms.
Ma EXPLAINs può essere molto rotto qui! Ad esempio, se SPIEGO una query con un solo source_id nella clausola IN e un indice forzato il (data), mi dice che scansionerà solo 20 righe, ma ciò non è possibile, poiché la tabella ha oltre 1 miliardo di righe e solo alcune corrisponde a questo source_id.
date
è un DOUBLE
...?
EXPLAIN
?ANALYZE
è qualcosa di diverso, ed è probabilmente qualcosa da considerare se non lo hai fatto, come una possibile spiegazione è che le statistiche dell'indice distorte stanno distraendo l'ottimizzatore dalla scelta saggia. Non penso che ci sia bisogno di my.cnf nella domanda, e quello spazio potrebbe essere meglio usato per pubblicare alcuniEXPLAIN
output delle variazioni nel comportamento che vedi ... dopo aver investigatoANALYZE [LOCAL] TABLE
...