MySQL nécessite FORCE INDEX sur une table énorme et des SELECT simples

8

Nous avons une application qui stocke les articles de différentes sources dans une table MySQL et permet aux utilisateurs de récupérer les 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 application OLTP et les requêtes comme celles 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. De plus, certaines requêtes ont 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.

Veste
la source
"Quand nous lançons une analyse sur ces ..." Vous voulez dire EXPLAIN? ANALYZEest quelque chose de différent, et est probablement quelque chose à considérer si vous ne l'avez pas fait, car une explication possible est que les statistiques d'index biaisées distraient l'optimiseur de choisir judicieusement. Je ne pense pas qu'il y ait un besoin pour le my.cnf dans la question, et cet espace pourrait être mieux utilisé pour publier une EXPLAINsortie des variations de comportement que vous voyez ... après avoir enquêté ANALYZE [LOCAL] TABLE...
Michael - sqlbot
Oui, c'était une faute de frappe, merci pour la correction. Je l'ai corrigé. Bien sûr, nous avons fait ANALYSER, mais cela n'a pas aidé du tout. J'essaierai de capturer quelques EXPLAIN plus tard.
Veste
Et dateest-ce que DOUBLE...?
ypercubeᵀᴹ
Oui, car nous avons besoin d'une précision en microsecondes ici. Le taux d'insertion dans ce tableau est d'environ 400 000 entrées par heure et nous avons besoin que les dates soient aussi uniques que possible.
Veste
@Jacket Pouvez-vous publier un EXPLAIN hors de la requête incriminée? je pense que parce qu'il est lié au processeur, votre serveur effectue un tri rapide ("en utilisant le tri de fichiers dans expliquer)" votre jeu de résultats ..
Raymond Nijland

Réponses:

4

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.

Eric Rath
la source