Existe-t-il un moyen d'optimiser le tri par colonnes de tables jointes?

10

Voici ma lente requête:

SELECT `products_counts`.`cid`
FROM
  `products_counts` `products_counts`

  LEFT OUTER JOIN `products` `products` ON (
  `products_counts`.`product_id` = `products`.`id`
  )
  LEFT OUTER JOIN `trademarks` `trademark` ON (
  `products`.`trademark_id` = `trademark`.`id`
  )
  LEFT OUTER JOIN `suppliers` `supplier` ON (
  `products_counts`.`supplier_id` = `supplier`.`id`
  )
WHERE
  `products_counts`.product_id IN
  (159, 572, 1075, 1102, 1145, 1162, 1660, 2355, 2356, 2357, 3236, 6471, 6472, 6473, 8779, 9043, 9095, 9336, 9337, 9338, 9445, 10198, 10966, 10967, 10974, 11124, 11168, 16387, 16689, 16827, 17689, 17920, 17938, 17946, 17957, 21341, 21352, 21420, 21421, 21429, 21544, 27944, 27988, 30194, 30196, 30230, 30278, 30699, 31306, 31340, 32625, 34021, 34047, 38043, 43743, 48639, 48720, 52453, 55667, 56847, 57478, 58034, 61477, 62301, 65983, 66013, 66181, 66197, 66204, 66407, 66844, 66879, 67308, 68637, 73944, 74037, 74060, 77502, 90963, 101630, 101900, 101977, 101985, 101987, 105906, 108112, 123839, 126316, 135156, 135184, 138903, 142755, 143046, 143193, 143247, 144054, 150164, 150406, 154001, 154546, 157998, 159896, 161695, 163367, 170173, 172257, 172732, 173581, 174001, 175126, 181900, 182168, 182342, 182858, 182976, 183706, 183902, 183936, 184939, 185744, 287831, 362832, 363923, 7083107, 7173092, 7342593, 7342594, 7342595, 7728766)
ORDER BY
  products_counts.inflow ASC,
  supplier.delivery_period ASC,
  trademark.sort DESC,
  trademark.name ASC
LIMIT
  0, 3;

Le temps de requête moyen est de 4,5 secondes sur mon jeu de données, ce qui est inacceptable.

Solutions que je vois:

Ajoutez toutes les colonnes de la clause order à la products_countstable. Mais j'ai ~ 10 types de commandes en application, donc je devrais créer beaucoup de colonnes et d'index. De products_countsplus, les mises à jour / insertions / suppressions sont très intensives, j'ai donc besoin de mettre à jour immédiatement toutes les colonnes liées à la commande (à l'aide de déclencheurs?).

Y a-t-il une autre solution?

Explique:

+----+-------------+-----------------+--------+---------------------------------------------+------------------------+---------+----------------------------------+------+----------------------------------------------+
| id | select_type | table           | type   | possible_keys                               | key                    | key_len | ref                              | rows | Extra                                        |
+----+-------------+-----------------+--------+---------------------------------------------+------------------------+---------+----------------------------------+------+----------------------------------------------+
|  1 | SIMPLE      | products_counts | range  | product_id_supplier_id,product_id,pid_count | product_id_supplier_id | 4       | NULL                             |  227 | Using where; Using temporary; Using filesort |
|  1 | SIMPLE      | products        | eq_ref | PRIMARY                                     | PRIMARY                | 4       | uaot.products_counts.product_id  |    1 |                                              |
|  1 | SIMPLE      | trademark       | eq_ref | PRIMARY                                     | PRIMARY                | 4       | uaot.products.trademark_id       |    1 |                                              |
|  1 | SIMPLE      | supplier        | eq_ref | PRIMARY                                     | PRIMARY                | 4       | uaot.products_counts.supplier_id |    1 |                                              |
+----+-------------+-----------------+--------+---------------------------------------------+------------------------+---------+----------------------------------+------+----------------------------------------------+

Structure des tables:

CREATE TABLE `products_counts` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `product_id` int(11) unsigned NOT NULL,
  `supplier_id` int(11) unsigned NOT NULL,
  `count` int(11) unsigned NOT NULL,
  `cid` varchar(64) NOT NULL,
  `inflow` varchar(10) NOT NULL,
  `for_delete` tinyint(1) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `cid` (`cid`),
  UNIQUE KEY `product_id_supplier_id` (`product_id`,`supplier_id`),
  KEY `product_id` (`product_id`),
  KEY `count` (`count`),
  KEY `pid_count` (`product_id`,`count`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `products` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `external_id` varchar(36) NOT NULL,
  `name` varchar(255) NOT NULL,
  `category_id` int(11) unsigned NOT NULL,
  `trademark_id` int(11) unsigned NOT NULL,
  `photo` varchar(255) NOT NULL,
  `sort` int(11) unsigned NOT NULL,
  `otech` tinyint(1) unsigned NOT NULL,
  `not_liquid` tinyint(1) unsigned NOT NULL DEFAULT '0',
  `applicable` varchar(255) NOT NULL,
  `code_main` varchar(64) NOT NULL,
  `code_searchable` varchar(128) NOT NULL,
  `total` int(11) unsigned NOT NULL,
  `slider` int(11) unsigned NOT NULL,
  `slider_title` varchar(255) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `external_id` (`external_id`),
  KEY `category_id` (`category_id`),
  KEY `trademark_id` (`trademark_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `trademarks` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `external_id` varchar(36) NOT NULL,
  `name` varchar(255) NOT NULL,
  `country_id` int(11) NOT NULL,
  `sort` int(11) unsigned NOT NULL DEFAULT '0',
  `sort_list` int(10) unsigned NOT NULL DEFAULT '0',
  `is_featured` tinyint(1) unsigned NOT NULL,
  `is_direct` tinyint(1) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `external_id` (`external_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `suppliers` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `external_id` varchar(36) NOT NULL,
  `code` varchar(64) NOT NULL,
  `name` varchar(255) NOT NULL,
  `delivery_period` tinyint(1) unsigned NOT NULL,
  `is_default` tinyint(1) unsigned NOT NULL,
  PRIMARY KEY (`id`),
  KEY `external_id` (`external_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Informations sur le serveur MySQL:

mysqld  Ver 5.5.45-1+deb.sury.org~trusty+1 for debian-linux-gnu on i686 ((Ubuntu))
Stanislav Gamayunov
la source
3
Pouvez-vous fournir un SQL Fiddle avec des index, un schéma de table et des données de test? Quel est également votre temps cible? Cherchez-vous à le terminer en 3 secondes, 1 seconde, 50 millisecondes? Combien d'enregistrements avez-vous dans les différents tableaux 1k, 100k, 100M?
Erik
Si les champs que vous triez ne sont pas indexés et que l'ensemble de données est vraiment volumineux, pourriez-vous peut-être rechercher un problème sort_buffer_size? Vous pouvez essayer de modifier votre valeur sur votre session et exécuter la requête pour voir si elle s'améliore.
Brian Efting du
Avez-vous essayé d'ajouter un index (inflow, product_id)?
ypercubeᵀᴹ
Assurez-vous que vous avez un niveau décent innodb_buffer_pool_size. En général, environ 70% de la RAM disponible est bonne.
Rick James

Réponses:

6

L'examen de vos définitions de table montre que vous avez des index correspondant à travers les tables impliquées. Cela devrait permettre aux jointures de se produire le plus rapidement possible dans les limites de la MySQL'slogique de jointure.

Cependant, le tri à partir de plusieurs tables est plus complexe.

En 2007, Sergey Petrunia a décrit les 3 MySQLalgorithmes de tri par ordre de vitesse pour MySQLà: http://s.petrunia.net/blog/?m=201407

  1. Utiliser une méthode d'accès basée sur un index qui produit une sortie ordonnée
  2. Utilisation filesort()sur la 1ère table non constante
  3. Mettez le résultat de la jointure dans une table temporaire et utilisez- filesort()le

À partir des définitions de table et des jointures ci-dessus, vous pouvez voir que vous n'obtiendrez jamais le tri le plus rapide . Cela signifie que vous serez tributaire filesort()des critères de tri que vous utilisez.

Cependant, si vous concevez et utilisez une vue matérialisée, vous pourrez utiliser l' algorithme de tri le plus rapide.

Pour voir les détails définis pour MySQL 5.5les méthodes de tri, voir: http://dev.mysql.com/doc/refman/5.5/en/order-by-optimization.html

Pour MySQL 5.5(dans cet exemple) augmenter la ORDER BYvitesse si vous ne pouvez pas MySQLutiliser les index plutôt qu'une phase de tri supplémentaire, essayez les stratégies suivantes:

• Augmentez la sort_buffer_sizevaleur de la variable.

• Augmentez la read_rnd_buffer_sizevaleur de la variable.

• Utilisez moins de RAM par ligne en déclarant les colonnes uniquement aussi grandes que nécessaire pour les valeurs réelles à stocker. [Par exemple, réduire un varchar (256) en varchar (ActualLongestString)]

• Modifiez la tmpdirvariable système pour pointer vers un système de fichiers dédié avec de grandes quantités d'espace libre. (D'autres détails sont proposés dans le lien ci-dessus.)

Il y a plus de détails fournis dans la MySQL 5.7documentation pour augmenter la ORDERvitesse, dont certains peuvent être des comportements légèrement améliorés :

http://dev.mysql.com/doc/refman/5.7/en/order-by-optimization.html

Vues matérialisées - Une approche différente pour trier les tables jointes

Vous avez fait allusion aux vues matérialisées avec votre question concernant l'utilisation des déclencheurs. MySQL n'a pas de fonctionnalité intégrée pour créer une vue matérialisée, mais vous disposez des outils nécessaires. En utilisant des déclencheurs pour répartir la charge, vous pouvez maintenir la vue matérialisée jusqu'à présent.

La vue matérialisée est en fait une table qui est remplie à travers du code procédural pour construire ou reconstruire la vue matérialisée et maintenue par des déclencheurs pour garder les données à jour.

Puisque vous créez une table qui aura un index , la vue matérialisée interrogée peut utiliser la méthode de tri la plus rapide : utilisez une méthode d'accès basée sur un index qui produit une sortie ordonnée

Comme MySQL 5.5utilise des déclencheurs pour maintenir une vue matérialisée , vous aurez également besoin d'un processus, d'un script ou d'une procédure stockée pour créer la vue matérialisée initiale .

Mais c'est évidemment un processus trop lourd à exécuter après chaque mise à jour des tables de base où vous gérez les données. C'est là que les déclencheurs entrent en jeu pour maintenir les données à jour lorsque des modifications sont apportées. De cette façon , chacun insert, updateet deletese propager leurs changements, en utilisant vos déclencheurs, à la vue matérialisée .

L'organisation FROMDUAL sur http://www.fromdual.com/ a un exemple de code pour maintenir une vue matérialisée . Donc, plutôt que d'écrire mes propres échantillons, je vous indiquerai leurs échantillons:

http://www.fromdual.com/mysql-materialized-views

Exemple 1: création d'une vue matérialisée

DROP TABLE sales_mv;
CREATE TABLE sales_mv (
    product_name VARCHAR(128)  NOT NULL
  , price_sum    DECIMAL(10,2) NOT NULL
  , amount_sum   INT           NOT NULL
  , price_avg    FLOAT         NOT NULL
  , amount_avg   FLOAT         NOT NULL
  , sales_cnt    INT           NOT NULL
  , UNIQUE INDEX product (product_name)
);

INSERT INTO sales_mv
SELECT product_name
    , SUM(product_price), SUM(product_amount)
    , AVG(product_price), AVG(product_amount)
    , COUNT(*)
  FROM sales
GROUP BY product_name;

Cela vous donne la vue matérialisée au moment de l'actualisation. Cependant, étant donné que vous disposez d'une base de données évoluant rapidement, vous souhaitez également conserver cette vue aussi à jour que possible.

Par conséquent, les tables de données de base affectées doivent avoir des déclencheurs pour propager les modifications d'une table de base vers la table Vue matérialisée . À titre d'exemple:

Exemple 2: insertion de nouvelles données dans une vue matérialisée

DELIMITER $$

CREATE TRIGGER sales_ins
AFTER INSERT ON sales
FOR EACH ROW
BEGIN

  SET @old_price_sum = 0;
  SET @old_amount_sum = 0;
  SET @old_price_avg = 0;
  SET @old_amount_avg = 0;
  SET @old_sales_cnt = 0;

  SELECT IFNULL(price_sum, 0), IFNULL(amount_sum, 0), IFNULL(price_avg, 0)
       , IFNULL(amount_avg, 0), IFNULL(sales_cnt, 0)
    FROM sales_mv
   WHERE product_name = NEW.product_name
    INTO @old_price_sum, @old_amount_sum, @old_price_avg
       , @old_amount_avg, @old_sales_cnt
  ;

  SET @new_price_sum = @old_price_sum + NEW.product_price;
  SET @new_amount_sum = @old_amount_sum + NEW.product_amount;
  SET @new_sales_cnt = @old_sales_cnt + 1;
  SET @new_price_avg = @new_price_sum / @new_sales_cnt;
  SET @new_amount_avg = @new_amount_sum / @new_sales_cnt;

  REPLACE INTO sales_mv
  VALUES(NEW.product_name, @new_price_sum, @new_amount_sum, @new_price_avg
       , @new_amount_avg, @new_sales_cnt)
  ;

END;
$$
DELIMITER ;

Bien sûr, vous aurez également besoin de déclencheurs pour maintenir la suppression des données d'une vue matérialisée et la mise à jour des données dans une vue matérialisée . Des échantillons sont également disponibles pour ces déclencheurs.

ENFIN: Comment cela rend-il le tri des tables jointes plus rapide?

La vue matérialisée est constamment construite au fur et à mesure des mises à jour. Par conséquent, vous pouvez définir l' index (ou les index ) que vous souhaitez utiliser pour trier les données dans la vue matérialisée ou la table .

Si les frais généraux liés à la maintenance des données ne sont pas trop lourds, vous dépensez des ressources (CPU / IO / etc) pour chaque modification de données pertinente pour conserver la vue matérialisée et, par conséquent, les données d'index sont à jour et facilement disponibles. Par conséquent, la sélection sera plus rapide, car vous:

  1. Processeur incrémentiel et IO déjà dépensés pour préparer les données pour votre SELECT.
  2. L'index sur la vue matérialisée peut utiliser la méthode de tri la plus rapide disponible pour MySQL, à savoir Utiliser une méthode d'accès basée sur l'index qui produit une sortie ordonnée .

En fonction de votre situation et de ce que vous pensez du processus global, vous souhaiterez peut-être reconstruire les vues matérialisées chaque nuit pendant une période lente.

Remarque: Dans Microsoft SQL Server les vues matérialisées, les vues indexées sont référencées et sont automatiquement mises à jour en fonction des métadonnées de la vue indexée .

RLF
la source
6

Il n'y a pas grand-chose à faire ici, mais je suppose que le principal problème est que vous créez à chaque fois une table temporaire et un fichier de tri assez volumineux sur le disque. La raison étant:

  1. Vous utilisez UTF8
  2. Vous utilisez de grands champs varchar (255) pour le tri

Cela signifie que votre table temporaire et votre fichier de tri peuvent être assez volumineux, car lors de la création de la table temporaire, les champs sont créés à la longueur MAX et lorsque le tri des enregistrements est à la longueur MAX (et UTF8 est de 3 octets par caractère). Celles-ci empêchent également probablement l'utilisation d'une table temporaire en mémoire. Pour plus d'informations, voir les détails des tables temporaires internes .

La LIMIT ne nous sert également à rien ici, car nous devons matérialiser et ordonner l'ensemble des résultats avant de savoir quelles sont les 3 premières lignes.

Avez-vous essayé de déplacer votre tmpdir vers un système de fichiers tmpfs ? Si / tmp n'utilise pas déjà tmpfs (MySQL utilise tmpdir=/tmppar défaut sur * nix), vous pouvez utiliser directement / dev / shm. Dans votre fichier my.cnf:

[mysqld]
...
tmpdir=/dev/shm  

Ensuite, vous devrez redémarrer mysqld.

Cela pourrait faire une énorme différence. Si vous êtes susceptible de subir une pression mémoire sur le système, vous souhaiterez probablement limiter la taille (généralement, les distributions Linux plafonnent tmpfs à 50% de la RAM totale par défaut) afin d'éviter d'échanger des segments de mémoire sur le disque, ou même pire une situation de MOO . Vous pouvez le faire en modifiant la ligne dans /etc/fstab:

tmpfs                   /dev/shm                tmpfs   rw,size=2G,noexec,nodev,noatime,nodiratime        0 0

Vous pouvez également le redimensionner "en ligne". Par exemple:

mount -o remount,size=2G,noexec,nodev,noatime,nodiratime /dev/shm

Vous pouvez également passer à MySQL 5.6 - qui a des sous-requêtes performantes et des tables dérivées - et jouer un peu plus avec la requête. Je ne pense pas que nous verrons de grandes victoires dans cette voie, d'après ce que je vois.

Bonne chance!

Matt Lord
la source
Merci pour votre réponse. Déplacer tmpdir vers tmpfs a donné un bon gain de performances.
Stanislav Gamayunov