Nous avons une application qui stocke des articles de différentes sources dans une table MySQL et permet aux utilisateurs de récupérer ces articles classés par date. Les articles sont toujours filtrés par source, donc pour les SELECT clients, nous avons toujours
WHERE source_id IN (...,...) ORDER BY date DESC/ASC
Nous utilisons IN, car les utilisateurs ont de nombreux abonnements (certains en ont des milliers).
Voici le schéma du tableau des 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='';
Nous avons besoin de l'index (date), car parfois nous exécutons des opérations en arrière-plan sur cette table sans filtrer par source. Cependant, les utilisateurs ne peuvent pas le faire.
Le tableau compte environ 1 milliard d'enregistrements (oui, nous envisageons de partager pour l'avenir ...). Une requête typique ressemble à ceci:
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
Pourquoi FORCE INDEX? Parce qu'il s'est avéré que MySQL choisit parfois d'utiliser l'index (date) pour de telles requêtes (peut-être en raison de sa longueur plus petite?), Ce qui entraîne des analyses de millions d'enregistrements. Si nous supprimons FORCE INDEX en production, nos cœurs de processeur de serveur de base de données sont maximisés en quelques secondes (il s'agit d'une OLTP et les requêtes comme ci-dessus sont exécutées à des taux d'environ 2000 par seconde).
Le problème avec cette approche est que certaines requêtes (nous soupçonnons que cela est en quelque sorte lié au nombre d'ID source dans la clause IN) s'exécutent vraiment plus rapidement avec l'index de date. Lorsque nous exécutons EXPLAIN sur ceux-ci, nous voyons que l'index source_id_date analyse des dizaines de millions d'enregistrements, tandis que l'index de date n'en analyse que quelques milliers. Habituellement, c'est l'inverse, mais nous ne pouvons pas trouver une relation solide.
Idéalement, nous voulions savoir pourquoi l'optimiseur MySQL choisit le mauvais index et supprimer l'instruction FORCE INDEX, mais un moyen de prédire quand forcer l'index de date fonctionnera également pour nous.
Quelques clarifications:
La requête SELECT ci-dessus est beaucoup simplifiée aux fins de cette question. Il a plusieurs JOINs à des tables avec environ 100 millions de lignes chacune, jointes au PK (articles_user_flags.id = article.id), ce qui aggrave le problème lorsqu'il y a des millions de lignes à trier. Certaines requêtes ont également des emplacements supplémentaires, par exemple:
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
Cette requête répertorie uniquement les articles favoris pour l'utilisateur particulier (1).
Le serveur exécute MySQL version 5.5.32 (Percona) avec XtraDB. Le matériel est 2xE5-2620, 128 Go de RAM, 4HDDx1TB RAID10 avec contrôleur soutenu par batterie. Les SELECT problématiques sont complètement liés au CPU.
my.cnf est le suivant (suppression de certaines directives non liées telles que server-id, port, etc ...):
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
Comme demandé, voici quelques EXPLICATIONS des requêtes problématiques:
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)
Le SELECT réel prend environ une minute et est complètement lié au CPU. Lorsque je change l'index en (date), dans ce cas, l'optimiseur MySQL choisit également automatiquement:
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)
Et le SELECT ne prend que 10 ms.
Mais les EXPLAIN peuvent être beaucoup cassés ici! Par exemple, si j'EXPLIQUE une requête avec un seul source_id dans la clause IN et un index forcé le (date), cela m'indique qu'il analysera seulement 20 lignes, mais ce n'est pas possible, car la table a plus de 1 milliard de lignes et seulement quelques-unes correspond à ce source_id.
Vous pouvez vérifier votre valeur pour le paramètre innodb_stats_sample_pages . Il contrôle le nombre de plongées d'index que MySQL effectue sur une table lors de la mise à jour des statistiques d'index, qui à leur tour sont utilisées pour calculer le coût d'un plan de jointure candidat. La valeur par défaut était 8 pour la version que nous utilisions. Nous l'avons changé à 128 et avons observé moins de plans de jointure inattendus.