Optimisation de la condition WHERE pour le champ TIMESTAMP dans l'instruction MySQL SELECT

8

Je travaille sur un schéma pour un système d'analyse qui suit les temps d'utilisation, et il est nécessaire de voir le temps d'utilisation total dans une certaine plage de dates.

Pour donner un exemple simple, ce type de requête serait exécuté souvent:

select sum(diff_ms) from writetest_table where time_on > ("2015-07-13 15:11:56");

Cette requête prend généralement environ 7 secondes sur une table fortement peuplée. Il compte environ 35 millions de lignes, MyISAM sur MySQL fonctionnant sur Amazon RDS (db.m3.xlarge).

La suppression de la clause WHERE fait que la requête ne prend que 4 secondes et l'ajout d'une seconde clause (time_off> XXX) ajoute 1,5 seconde supplémentaire, ce qui porte le temps de requête à 8,5 secondes.

Comme je sais que ces types de requêtes seront généralement effectués, je voudrais optimiser les choses afin qu'elles soient plus rapides, idéalement en dessous de 5 secondes.

J'ai commencé par ajouter un index sur time_on, et bien que cela ait considérablement accéléré une requête WHERE "=", cela n'a eu aucun effet sur la requête ">". Existe-t-il un moyen de créer un index qui accélérerait les requêtes WHERE ">" ou "<"?

Ou s'il y a d'autres suggestions sur les performances de ce type de requête, faites-le moi savoir.

Remarque: J'utilise le champ "diff_ms" comme étape de dénormalisation (il est égal à time_off - time_on) qui améliore les performances de l'agrégation d'environ 30% -40%.

Je crée l'index avec cette commande:

ALTER TABLE writetest_table ADD INDEX time_on (time_on) USING BTREE;

Exécuter "expliquer" sur la requête d'origine (avec "time_on>") indique que time_on est une "possible_key" et le select_type est "SIMPLE". La colonne "extra" indique "Utiliser où" et "type" est "TOUT". Après l'ajout de l'index, le tableau indique que "time_on" est de type "MUL", ce qui semble correct car le même temps peut être présent deux fois.

Voici le schéma de la table:

CREATE TABLE `writetest_table` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `sessionID` int(11) DEFAULT NULL,
  `time_on` timestamp NULL DEFAULT NULL,
  `time_off` timestamp NULL DEFAULT NULL,
  `diff_ms` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `time_on` (`time_on`)
) ENGINE=MyISAM AUTO_INCREMENT=50410902 DEFAULT CHARSET=latin1;

MISE À JOUR: J'ai créé l'index suivant basé sur la réponse de ypercube, mais cela augmente le temps de requête pour la première requête à environ 17 secondes!

ALTER TABLE writetest_table  ADD INDEX time_on__diff_ms__ix (time_on, diff_ms) ;

MISE À JOUR 2: sortie EXPLAIN

mysql> explain select sum(diff_ms) from writetest_table where time_on > '2015-07-13 15:11:56';
+----+-------------+---------------------+-------+----------------------+----------------------+---------+------+----------+--------------------------+
| id | select_type | table               | type  | possible_keys        | key                  | key_len | ref  | rows     | Extra                    |
+----+-------------+---------------------+-------+----------------------+----------------------+---------+------+----------+--------------------------+
|  1 | SIMPLE      | writetest_table_old | index | time_on__diff_ms__ix | time_on__diff_ms__ix | 10      | NULL | 35831102 | Using where; Using index |
+----+-------------+---------------------+-------+----------------------+----------------------+---------+------+----------+--------------------------+
1 row in set (0.00 sec)

Mise à jour 3: résultat de la requête demandée

mysql> SELECT time_on FROM writetest_table ORDER BY time_on LIMIT 1;
+---------------------+
| time_on             |
+---------------------+
| 2015-07-13 15:11:56 |
+---------------------+
1 row in set (0.01 sec)
Locksleyu
la source
Avez-vous réellement des valeurs nulles dans ces 2 colonnes ( time_onet diff_ms)? Que se passe-t-il si vous ajoutez une requête WHERE ... AND diff_ms IS NOT NULL?
ypercubeᵀᴹ
Pouvez-vous s'il vous plaît nous montrer la sortie deSELECT COUNT(*), COUNT(diff_ms) FROM writetest_table;
ypercubeᵀᴹ
De plus, l'explication dans votre "Mise à jour 2" affiche " table:writetest_table_old " pendant la requête from writetest_table. Est-ce une faute de frappe ou vous exécutez la requête dans une table différente?
ypercubeᵀᴹ

Réponses:

3

Je pense que je commence à comprendre.

Quand je t'ai demandé de courir

SELECT time_on FROM writetest_table ORDER BY time_on LIMIT 1;

Vous avez dit que c'était 2015-07-13 15:11:56ce que vous aviez dans votre WHEREclause

Quand vous avez fait la requête

select sum(diff_ms) from writetest_table;

Il a effectué une analyse complète de la table de 35,8 millions de lignes.

Quand vous avez fait la requête

select sum(diff_ms) from writetest_table where time_on > ("2015-07-13 15:11:56");

Il a effectué un balayage d'index complet de 35,8 millions de lignes.

Il est parfaitement logique que la requête sans la clause WHERE soit plus rapide. Pourquoi ?

L'analyse de la table lirait 35,8 millions de lignes en un seul passage linéaire.

L'EXPLAIN sur la requête avec le WHERE a également généré 35,8 millions de lignes. Un balayage d'index se comporterait un peu différemment. Bien que le BTREE conserve l'ordre des touches, il est horrible de faire des analyses de portée. Dans votre cas particulier, vous effectuez la pire analyse de plage possible, qui aurait le même nombre d'entrées BTREE qu'il y a de lignes dans le tableau. MySQL doit traverser les pages BTREE (au moins à travers les nœuds terminaux) pour lire les valeurs. De plus, la time_oncolonne doit être comparée en cours de route dans l'ordre dicté par l'indice. Par conséquent, les nœuds BTREE non-feuilles doivent également être traversés.

S'il vous plaît voir mes messages sur BTREEs

Si la requête était à minuit aujourd'hui

select sum(diff_ms) from writetest_table where time_on >= ("2015-07-14 00:00:00");

ou même midi aujourd'hui

select sum(diff_ms) from writetest_table where time_on >= ("2015-07-14 12:00:00");

cela devrait prendre moins de temps.

MORAL OF THE STORY: N'utilisez pas de clause WHERE qui effectue une analyse de plage ordonnée égale au nombre de lignes de la table cible.

RolandoMySQLDBA
la source
Mon seul problème est de savoir comment aller d'ici. J'ai fait une requête avec une date qui a abouti à seulement 1 million de lignes filtrées et la somme n'a pris qu'une seconde. Mais parfois, je devrai peut-être faire des sommes agrégées sur la plupart des données. Des suggestions sur la façon de gérer cela? J'espérais que MySQL serait assez intelligent pour savoir quand utiliser l'index et quand pas trop, mais je suppose qu'il n'a pas assez d'informations dans ce cas.
Locksleyu
Je souhaite vraiment qu'il y ait une sorte d'index qui a été organisé pour accélérer les clauses WHERE spécifiant les plages de dates, ce qui semble techniquement possible à implémenter, mais je suppose qu'il n'est pas pris en charge.
Locksleyu
Vous avez beaucoup trop de données dans une si courte portée. Aucune clause WHERE ne pourra jamais être compensée. Pourquoi ? Ce n'est pas l'indice qui pose problème. C'est l'opinion de l'optimiseur de requêtes MySQL sur l'index. Lorsque vous commencez à accumuler beaucoup plus de données (disons environ deux semaines), les statistiques de l'index devraient se stabiliser et vous devriez voir une amélioration des performances. Ne faites tout simplement pas des analyses d'index complètes.
RolandoMySQLDBA
4

Pour la requête spécifique:

select sum(diff_ms) 
from writetest_table 
where time_on > '2015-07-13 15:11:56' ;     -- use single quotes, not double

un indice sur (time_on, diff_ms)serait la meilleure option. Donc, si la requête s'exécute suffisamment souvent ou si son efficacité est cruciale pour votre application, ajoutez cet index:

ALTER TABLE writetest_table 
  ADD INDEX time_on__diff_ms__ix      -- pick a name for the index
    (time_on, diff_ms) ;

(Pas lié à la question)
Et vraiment, changez le moteur de la table en InnoDB. Nous sommes en 2015 et les funérailles de MyISAM ont eu lieu il y a quelques années.
(/ diatribe)

ypercubeᵀᴹ
la source
J'ai créé l'index exact que vous avez suggéré, puis j'ai exécuté la requête exacte que vous avez mentionnée en premier dans votre réponse, mais le temps est maintenant bien pire, prenant environ 17 secondes de manière cohérente (j'ai essayé plusieurs fois).
Locksleyu
Je n'ai aucune idée de ce qui en est la cause. Dans le cas où cela importe, il n'y a que 3671 valeurs distinctes de time_on dans la table (cela est dû à la façon dont mon script de test remplit les données).
Locksleyu
Vous devez faire trois (3) choses: 1. exécuter ALTER TABLE writetest_table DROP INDEX time_on;, 2) exécuter ANALYZE TABLE writetest_table;et 3) réexécuter la requête. Le temps revient-il à 7 secondes?
RolandoMySQLDBA
1
Vous devriez également courir EXPLAIN select sum(diff_ms) from writetest_table where time_on > ("2015-07-13 15:11:56");. Le nouvel index est-il utilisé? S'il n'est pas utilisé, je dirais que c'est votre population clé, surtout si votre premier time_on est il y a seulement quelques jours.Comme le nombre de lignes augmente avec des jours plus distincts, la distribution des clés devrait se stabiliser et EXPLAIN devrait être meilleur .
RolandoMySQLDBA
RolandoMySQLDBA - J'ai essayé vos trois étapes, et oui le temps remonte à 7 secondes. J'ai fait l'explication et il indique que l'index est utilisé. Je ne sais toujours pas pourquoi l'ajout d'un index comme celui-ci pourrait rendre les performances supérieures à 2x aussi mauvaises.
Locksleyu