Question

Mise à jour: J'ai trouvé une solution. Voir ma réponse ci-dessous.

Ma Question

Comment puis-je optimiser cette requête afin de minimiser les temps d'arrêt mon? Je dois mettre à jour plus de 50 schémas avec le nombre de billets allant de 100 000 à 2 millions. Est-il souhaitable d'essayer de mettre tous les champs tickets_extra en même temps? Je pense qu'il ya une solution ici que je suis tout simplement pas voir. Ive été cogne la tête contre ce problème depuis plus d'un jour.

En outre, j'ai essayé d'abord sans utiliser un sous SELECT, mais la performance était beaucoup pire que ce que j'ai actuellement.

Arrière-plan

Je suis en train d'optimiser ma base de données pour un rapport qui doit être exécuté. Les champs dont j'ai besoin d'agréger sur sont très chers pour calculer, donc je suis dénormalisation mon de schéma existant un peu pour tenir compte de ce rapport. Notez que j'ai simplifié la table des billets un peu en supprimant quelques dizaines de colonnes non pertinentes.

Mon rapport sera agrège compte des billets par Gestionnaire lors de la création et Gestionnaire Lorsque résolu . Cette relation complexe est schématisée ici:


(source: mosso.com )

Pour éviter la demi-douzaine nasty nécessaire pour calculer les jointures ce sur la volée J'ai ajouté le tableau suivant à mon schéma:

mysql> show create table tickets_extra\G
*************************** 1. row ***************************
       Table: tickets_extra
Create Table: CREATE TABLE `tickets_extra` (
  `ticket_id` int(11) NOT NULL,
  `manager_created` int(11) DEFAULT NULL,
  `manager_resolved` int(11) DEFAULT NULL,
  PRIMARY KEY (`ticket_id`),
  KEY `manager_created` (`manager_created`,`manager_resolved`),
  KEY `manager_resolved` (`manager_resolved`,`manager_created`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8
1 row in set (0.00 sec)

Le problème est maintenant, je n'ai pas été stocker ces données partout. Le gestionnaire a toujours été calculé dynamiquement. J'ai millions de billets à travers plusieurs bases de données avec le même schéma qui doivent avoir cette table peuplée. Je veux le faire en aussi efficace d'une manière possible, mais ont échoué dans l'optimisation des requêtes que je utilise pour le faire:

INSERT INTO tickets_extra (ticket_id, manager_created)
SELECT
  t.id, 
  su.user_id
FROM (
  SELECT 
    t.id, 
    shift_times.shift_id AS shift_id 
  FROM tickets t
  JOIN shifts ON t.shop_id = shifts.shop_id 
  JOIN shift_times ON (shifts.id = shift_times.shift_id
  AND shift_times.dow = DAYOFWEEK(t.created)
  AND TIME(t.created) BETWEEN shift_times.start AND shift_times.end)
) t
LEFT JOIN shifts_users su ON t.shift_id = su.shift_id
LEFT JOIN shift_positions ON su.shift_position_id = shift_positions.id
WHERE shift_positions.level = 1

Cette requête prend plus d'une heure pour fonctionner sur un schéma qui a> 1,7 million de billets. Ceci est inacceptable pour la fenêtre de maintenance je. , Il ne gère même pas le calcul du champ manager_resolved, comme essayant de combiner cela dans la même requête pousse le temps de requête dans la stratosphère. Mon inclinaison actuelle est de les garder séparés, et un UPDATE pour utiliser remplir le champ manager_resolved, mais je ne suis pas sûr.

Enfin, voici la sortie EXPLIQUEZ de la partie SELECT de cette requête:

*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: <derived2>
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 167661
        Extra: 
*************************** 2. row ***************************
           id: 1
  select_type: PRIMARY
        table: su
         type: ref
possible_keys: shift_id_fk_idx,shift_position_id_fk_idx
          key: shift_id_fk_idx
      key_len: 4
          ref: t.shift_id
         rows: 5
        Extra: Using where
*************************** 3. row ***************************
           id: 1
  select_type: PRIMARY
        table: shift_positions
         type: ALL
possible_keys: PRIMARY
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 6
        Extra: Using where; Using join buffer
*************************** 4. row ***************************
           id: 2
  select_type: DERIVED
        table: t
         type: ALL
possible_keys: fk_tickets_shop_id
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 173825
        Extra: 
*************************** 5. row ***************************
           id: 2
  select_type: DERIVED
        table: shifts
         type: ref
possible_keys: PRIMARY,shop_id_fk_idx
          key: shop_id_fk_idx
      key_len: 4
          ref: dev_acmc.t.shop_id
         rows: 1
        Extra: 
*************************** 6. row ***************************
           id: 2
  select_type: DERIVED
        table: shift_times
         type: ref
possible_keys: shift_id_fk_idx
          key: shift_id_fk_idx
      key_len: 4
          ref: dev_acmc.shifts.id
         rows: 4
        Extra: Using where
6 rows in set (6.30 sec)

Merci beaucoup pour la lecture!

Était-ce utile?

La solution

Eh bien, je trouve une solution. Il a fallu beaucoup d'expérimentation, et je pense un bon peu de chance aveugle, mais ici il est:

CREATE TABLE magic ENGINE=MEMORY
SELECT
  s.shop_id AS shop_id,
  s.id AS shift_id,
  st.dow AS dow,
  st.start AS start,
  st.end AS end,
  su.user_id AS manager_id
FROM shifts s
JOIN shift_times st ON s.id = st.shift_id
JOIN shifts_users su ON s.id = su.shift_id
JOIN shift_positions sp ON su.shift_position_id = sp.id AND sp.level = 1

ALTER TABLE magic ADD INDEX (shop_id, dow);

CREATE TABLE tickets_extra ENGINE=MyISAM
SELECT 
  t.id AS ticket_id,
  (
    SELECT m.manager_id
    FROM magic m
    WHERE DAYOFWEEK(t.created) = m.dow
    AND TIME(t.created) BETWEEN m.start AND m.end
    AND m.shop_id = t.shop_id
  ) AS manager_created,
  (
    SELECT m.manager_id
    FROM magic m
    WHERE DAYOFWEEK(t.resolved) = m.dow
    AND TIME(t.resolved) BETWEEN m.start AND m.end
    AND m.shop_id = t.shop_id
  ) AS manager_resolved
FROM tickets t;
DROP TABLE magic;

Explication Long

Maintenant, je vais vous expliquer pourquoi cela fonctionne, et mon rapport que processus et les étapes pour y arriver.

D'abord, je savais que la requête que je tentais souffrait à cause de la grande table dérivée, et la suite sur ce SE JOINT. Je prenais ma table de billets bien indexé et se joindre à toutes les données sur shift_times, puis laisser MySQL mâcher que pendant qu'elle tente de rejoindre les quarts de travail et table shift_positions. Ce mastodonte serait dérivé jusqu'à un gâchis non indexé de 2 millions de lignes.

Maintenant, je savais ce qui se passait. La raison pour laquelle je descendais cette route était bien parce que la façon de le faire, en utilisant strictement les JOIN sont était « bonne » en prenant un montant encore plus de temps. Cela est dû au peu méchant du chaos nécessaire pour déterminer qui est le gestionnaire d'un quart de travail donné est. Je dois rejoindre jusqu'à shift_times pour savoir ce que le changement correct même est, tout en se joignant simultanément jusqu'à shift_positions pour déterminer le niveau de l'utilisateur. Je ne pense pas que l'optimiseur MySQL gère très bien, et finit par créer une monstruosité énorme d'une table temporaire du joint, puis filtrer ce qui ne convient pas.

Alors, comme la table dérivée semblait être la « voie à suivre » J'obstina dans ce pendant un certain temps. J'ai essayé bottés de dégagement vers le bas dans une clause JOIN, aucune amélioration. J'ai essayé de créer dans une table temporaire avec la table dérivée, mais encore une fois il était trop lent que la table temporaire était non indexée.

Je me suis rendu compte que je devais gérer ce calcul de décalage, les temps, les positions sanely. Je pensais que, peut-être un point de vue serait le chemin à parcourir. Que faire si je créé une vue qui contenait ces informations: (shop_id, shift_id, dow, début, fin, manager_id). , J'ai simplement ensuite à se joindre à la table des billets par shop_id et tout le calcul DAYOFWEEK / TIME, et je serais dans les affaires. Bien sûr, je ne ai pas de se rappeler que MySQL gère VIEWs plutôt assily. Il ne les pas du tout, il fonctionne tout simplement la requête que vous avez utilisé matérialise pas pour obtenir le point de vue pour vous. En se joignant à des billets sur ce, je courais essentiellement ma requête initiale -. Pas d'amélioration

Ainsi, au lieu d'une vue que j'ai décidé d'utiliser une table temporaire. Cela a bien fonctionné si je n'allé chercher l'un des gestionnaires (créés ou résolus) à un moment, mais il était encore assez lent. En outre, j'ai découvert que MySQL vous ne pouvez pas se référer à la même table deux fois dans la même requête (je dois rejoindre ma table temporaire deux fois pour pouvoir faire la différence entre manager_created et manager_resolved). C'est un grand WTF, comme je peux le faire aussi longtemps que je ne précise pas « TEMPORAIRE » - c'est où le CREATE TABLE magique MOTEUR = MEMORY est entré en jeu.

Avec cette pseudo table temporaire à la main, j'ai essayé mon JOIN pour seulement manager_created à nouveau. Il se est bien, mais encore assez lent. Pourtant, quand je suis entré à nouveau pour obtenir manager_resolved dans la même requête le temps de requête tic tac retour dans la stratosphère. En regardant le EXPLIQUER a montré l'analyse complète de la table des billets (lignes ~ 2mln), comme prévu, et le JOIN sur la table magique à ~ 2087 chacun. Encore une fois, il me semblait être en cours d'exécution en fail.

Je commençais à réfléchir à la façon d'éviter l'JOIN tout à fait et c'est quand je trouve une carte obscure un message ancien poste où quelqu'un a suggéré d'utiliser des sous-requêtes (ne peut pas trouver le lien dans mon histoire). Ce qui a conduit à la deuxième requête SELECT ci-dessus (la création d'un tickets_extra). Dans le cas de la sélection un seul champ de gestionnaire, il a exécuté bien, mais encore une fois avec les deux était de la merde. Je regardais Explain et vu ceci:

*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: t
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 173825
        Extra: 
*************************** 2. row ***************************
           id: 3
  select_type: DEPENDENT SUBQUERY
        table: m
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 2037
        Extra: Using where
*************************** 3. row ***************************
           id: 2
  select_type: DEPENDENT SUBQUERY
        table: m
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 2037
        Extra: Using where
3 rows in set (0.00 sec)

Ack, la sous-requête redoutée DEPENDANT. Il est souvent suggéré d'éviter ceux-ci, comme MySQL va les exécuter habituellement de façon à l'extérieur dans, l'exécution de la requête interne pour chaque ligne de l'extérieur. J'ai ignoré cela, et je me demandais: « Eh bien ... Et si j'indicés cette table magique stupide? ». Ainsi, l'indice ADD (de shop_id, dow) est né.

Check this out:

mysql> CREATE TABLE magic ENGINE=MEMORY
<snip>
Query OK, 3220 rows affected (0.40 sec)

mysql> ALTER TABLE magic ADD INDEX (shop_id, dow);
Query OK, 3220 rows affected (0.02 sec)

mysql> CREATE TABLE tickets_extra ENGINE=MyISAM
<snip>
Query OK, 1933769 rows affected (24.18 sec)

mysql> drop table magic;
Query OK, 0 rows affected (0.00 sec)

'S ce que je te parle au sujet!

Conclusion

Ceci est sans aucun doute la première fois que je l'ai créé une table non TEMPORAIRE à la volée, et indicielle à la volée, il suffit de faire une seule requête efficacement. Je suppose que je suppose toujours que l'ajout d'un index à la volée est une opération extrêmement coûteux. (Ajout d'un index peut prendre plus d'une heure de ma table de billets de 2mln lignes). Pourtant, pour seulement 3000 lignes c'est une partie de plaisir.

Ne pas avoir peur de sous-requêtes DEPENDANTES, la création de tables temporaires qui ne sont pas vraiment, l'indexation à la volée, ou étrangers. Ils peuvent tous être de bonnes choses dans la bonne situation.

Merci pour tous les StackOverflow d'aide. :-D

Autres conseils

Vous devriez avoir utilisé Postgres, lol. Une simple requête comme cela ne devrait pas prendre plus de quelques dizaines de secondes à condition que vous avez suffisamment de RAM pour éviter raclée disque.

Quoi qu'il en soit.

=> Est-ce le problème dans le SELECT ou INSERT?

(exécuter le seul SELECT sur un serveur de test et l'heure).

=> Votre disque de requête lié ou lié CPU?

Lancez-le sur un serveur de test et vérifier vmstat sortie. S'il est lié, ignorez cette CPU. S'il est disque lié, vérifiez la taille du jeu de travail (la taille de votre base de données). Si le jeu de travail est plus petit que votre RAM, il ne doit pas être lié disque. Vous pouvez forcer le chargement d'une table dans le cache du système d'exploitation avant d'exécuter une requête en lançant un mannequin sélectionner comme somme SELECT (une colonne) FROM table. Cela peut être utile si une requête sélectionne plusieurs lignes dans un ordre aléatoire d'une table qui ne sont pas mises en cache dans la RAM ... vous déclenchez une analyse séquentielle de la table, qu'il charge dans le cache, l'accès aléatoire est beaucoup plus rapide. Avec quelques astuces, vous pouvez également les index de cache (ou tout simplement goudrons votre répertoire de base de données> / dev / null, lol).

Bien sûr, en ajoutant plus de RAM pourrait aider (mais vous devez vérifier si la requête est en train de tuer d'abord le disque ou l'unité centrale de traitement). Ou dire MySQL d'utiliser plus de votre RAM dans la configuration (key_buffer, etc.).

Si vous faites des millions de disque dur au hasard, vous êtes dans la douleur cherche.

=> OK maintenant la requête

FIRST, ANALYSER vos tables.

  

LEFT JOIN shift_positions SUR su.shift_position_id = shift_positions.id   OÙ shift_positions.level = 1

Pourquoi avez-vous LEFT JOIN puis ajouter WHERE là-dessus? La gauche n'a pas de sens. S'il n'y a pas de ligne dans shift_positions, LEFT JOIN va générer une valeur NULL, et WHERE rejeter.

Solution:. Au lieu de l'utilisation REJOIGNEZ LEFT JOIN et déplacer (niveau = 1) dans l'état JOIN sur ()

Pendant que vous y êtes, aussi se débarrasser de l'autre LEFT JOIN (Replace par JOIN) à moins que vous êtes vraiment intéressé par tous ces NULLs? (Je suppose que vous n'êtes pas).

Maintenant, vous pouvez probablement vous débarrasser de la sous-sélection.

Suivant.

  

où le temps (t.created) ENTRE shift_times.start ET shift_times.end)

Ce n'est pas indexables, parce que vous avez une fonction TIME () dans la condition (utiliser Postgres, lol). Voyons cela:

  

JOIN shift_times ON (shifts.id = shift_times.shift_id    ET shift_times.dow = DAYOFWEEK (t.created)    ET TEMPS (t.created) ENTRE shift_times.start ET shift_times.end)

Vous voudriez avoir un indice sur multicolumn shift_times (shift_id, DAYOFWEEK (t.created), TIME (t.created)) si cette jointure peut être indexé.

Solution:. Ajouter de jour 'colonnes, le 'temps' à shift_times, contenant DAYOFWEEK (t.created), TIME (t.created), rempli de valeurs correctes à l'aide d'un déclencheur de tir sur INSERT ou UPDATE

créer un index sur plusieurs colonnes (shift_id, jour, heure)

Cela vous permettra d'avoir un accès en lecture seule pour la durée des changements:

create table_new (new schema);
insert into table_new select * from table order by primary_key_column;
rename table to table_old;
rename table_new to table;
-- recreate triggers if necessary

Lors de l'insertion des données aux tables InnoDB il est crucial que vous faites cela dans l'ordre de clé primaire (sinon avec de grands ensembles de données, il est quelques ordres de grandeur plus lente).

A propos de ENTRE

SELECT * FROM a WHERE a.column BETWEEN x AND y 
  • est indexable et correspond à une recherche de plage sur l'index a.column (si vous en avez un)
  • 100% est équivalent à a.column >= x AND a.column <= y

Bien que ceci:

SELECT * FROM a WHERE somevalue BETWEEN a.column1 AND a.column2
  • 100% est équivalent à somevalue >= a.column1 AND somevalue <= a.column2
  • est très différente de la chose première ci-dessus
  • n'est pas indexable par une recherche de plage (il n'y a pas de plage, vous avez 2 colonnes ici)
  • conduit généralement à horribles performances des requêtes

Je pense qu'il y avait une confusion à ce sujet dans le débat sur « entre » ci-dessus.

OP a le premier type, donc pas de souci.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top