Les requêtes individuelles s'exécutent sur 10 ms, avec UNION ALL, elles prennent 290 ms + (7,7 millions d'enregistrements de base de données MySQL). Comment optimiser?

9

J'ai une table qui stocke les rendez-vous disponibles pour les enseignants, permettant deux types d'insertions:

  1. Horaire : avec une liberté totale d'ajouter des créneaux horaires illimités par jour et par enseignant (tant que les créneaux ne se chevauchent pas): le 15 / avril, un enseignant peut avoir des créneaux horaires à 10h00, 11h00, 12h00 et 16h00 . Une personne est servie après avoir choisi un horaire / créneau spécifique pour l'enseignant.

  2. Période / plage horaire : le 15 / avril, un autre enseignant peut travailler de 10h00 à 12h00 puis de 14h00 à 18h00. Une personne est servie par ordre d'arrivée, donc si un enseignant travaille de 10h00 à 12h00, toutes les personnes qui arrivent pendant cette période seront suivies par ordre d'arrivée (file d'attente locale).

Étant donné que je dois renvoyer tous les enseignants disponibles dans une recherche, j'ai besoin que tous les créneaux soient enregistrés dans le même tableau que l'ordre des plages d'arrivée. De cette façon, je peux commander par date_from ASC, en affichant les premiers emplacements disponibles en premier dans les résultats de recherche.

Structure actuelle de la table

CREATE TABLE `teacher_slots` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `teacher_id` mediumint(8) unsigned NOT NULL,
  `city_id` smallint(5) unsigned NOT NULL,
  `subject_id` smallint(5) unsigned NOT NULL,
  `date_from` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  `date_to` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  `status` tinyint(4) NOT NULL DEFAULT '0',
  `order_of_arrival` tinyint(1) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  KEY `by_hour_idx` (`teacher_id`,`order_of_arrival`,`status`,`city_id`,`subject_id`,`date_from`),
  KEY `order_arrival_idx` (`order_of_arrival`,`status`,`city_id`,`subject_id`,`date_from`,`date_to`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

Requête de recherche

J'ai besoin de filtrer par: datetime réel, city_id, subject_id et si un slot est disponible (status = 0).

Pour les horaires, je dois montrer tous les créneaux horaires disponibles pour le premier jour disponible le plus proche pour chaque enseignant (afficher tous les créneaux horaires d'un jour donné et ne peut pas afficher plus d'un jour pour le même enseignant). (J'ai reçu la requête avec l'aide de mattedgod ).

Pour la plage basée sur l'ordre (order_of_arrival = 1), je dois montrer la plage disponible la plus proche, une seule fois par enseignant.

La première requête s'exécute individuellement en environ 0,10 ms, la seconde requête 0,08 ms et l'UNION ALL en moyenne 300 ms.

(
    SELECT id, teacher_slots.teacher_id, date_from, date_to, order_of_arrival
    FROM teacher_slots
    JOIN (
        SELECT DATE(MIN(date_from)) as closestDay, teacher_id
        FROM teacher_slots
        WHERE   date_from >= '2014-04-10 08:00:00' AND order_of_arrival = 0
                AND status = 0 AND city_id = 6015 AND subject_id = 1
        GROUP BY teacher_id
    ) a ON a.teacher_id = teacher_slots.teacher_id
    AND DATE(teacher_slots.date_from) = closestDay
    WHERE teacher_slots.date_from >= '2014-04-10 08:00:00'
        AND teacher_slots.order_of_arrival = 0
        AND teacher_slots.status = 0
        AND teacher_slots.city_id = 6015
        AND teacher_slots.subject_id = 1
)

UNION ALL

(
    SELECT id, teacher_id, date_from, date_to, order_of_arrival
    FROM teacher_slots
    WHERE order_of_arrival = 1 AND status = 0 AND city_id = 6015 AND subject_id = 1
        AND (
            (date_from <= '2014-04-10 08:00:00' AND  date_to >= '2014-04-10 08:00:00')
            OR (date_from >= '2014-04-10 08:00:00')
        )
    GROUP BY teacher_id
)

ORDER BY date_from ASC;

Question

Existe-t-il un moyen d'optimiser l'UNION, afin que je puisse obtenir une réponse raisonnable d'un maximum de ~ 20 ms ou même une plage de retour basée sur + toutes les heures en une seule requête (avec un IF, etc.)?

SQL Fiddle: http://www.sqlfiddle.com/#!2/59420/1/0

ÉDITER:

J'ai essayé une dénormalisation en créant un champ "only_date_from" où je stockais seulement la date, donc je pouvais changer cela ...

DATE(MIN(date_from)) as closestDay / DATE(teacher_slots.date_from) = closestDay

... pour ça

MIN(only_date_from) as closestDay / teacher_slots.only_date_from = closestDay

Cela m'a déjà fait gagner 100 ms! Toujours 200ms en moyenne.

AlfredBaudisch
la source

Réponses:

1

Premièrement, je pense que votre requête d'origine peut ne pas être "correcte"; En ce qui concerne votre SQLFiddle, il me semble que si vous devriez retournerez lignes avec ID= 2, 3et 4(en plus de la ligne avec ID= 1vous avez trouvé de cette moitié), parce que votre logique existante apparaît comme si vous l' intention de ces autres lignes à inclure, car ils répondent explicitement à la OR (date_from >= '2014-04-10 08:00:00')partie de votre deuxième WHEREclause.

La GROUP BY teacher_idclause de votre deuxième partie vous UNIONfait perdre ces lignes. En effet, vous n'agrégez en fait aucune colonne de votre liste de sélection, et dans ce cas, le GROUP BYcomportement sera «difficile à définir».

De plus, bien que je ne puisse pas expliquer les mauvaises performances de votre UNION, je peux contourner cela pour vous en le supprimant carrément de votre requête:

Plutôt que d'utiliser deux ensembles de logique séparés (et en partie, répétitifs) pour obtenir des lignes de la même table, j'ai consolidé votre logique en une seule requête avec les différences de votre logique ORéditées ensemble - c'est-à-dire si une ligne rencontre l'un ou l'autre de vos WHEREclauses d' origine , il est inclus. Ceci est possible car j'ai remplacé le que (INNER) JOINvous utilisiez pour trouver le closestDateavec un LEFT JOIN.

Cela LEFT JOINsignifie que nous pouvons maintenant également distinguer quel ensemble de logique doit être appliqué à une ligne; Si la jointure fonctionne (la date la plus proche N'EST PAS NULL), nous appliquons votre logique à partir de la première moitié, mais si la jointure échoue (la date la plus proche EST NUL), alors nous appliquons la logique à partir de votre seconde moitié.

Ainsi, cela retournera toutes les lignes que votre requête a renvoyées (dans le violon), et elle récupère également celles supplémentaires.

  SELECT
    *

  FROM 
    teacher_slots ts

    LEFT JOIN 
    (
      SELECT 
        teacher_id,
        DATE(MIN(date_from)) as closestDay

      FROM 
        teacher_slots

      WHERE   
        date_from >= '2014-04-10 08:00:00' 
        AND order_of_arrival = 0
        AND status = 0 
        AND city_id = 6015 
        AND subject_id = 1

      GROUP BY 
        teacher_id

    ) a
    ON a.teacher_id = ts.teacher_id
    AND a.closestDay = DATE(ts.date_from)

  WHERE 
    /* conditions that were common to both halves of the union */
    ts.status = 0
    AND ts.city_id = 6015
    AND ts.subject_id = 1

    AND
    (
      (
        /* conditions that were from above the union 
           (ie when we joined to get closest future date) */
        a.teacher_id IS NOT NULL
        AND ts.date_from >= '2014-04-10 08:00:00'
        AND ts.order_of_arrival = 0
      ) 
      OR
      (
        /* conditions that were below the union 
          (ie when we didn't join) */
        a.teacher_id IS NULL       
        AND ts.order_of_arrival = 1 
        AND 
        (
          (
            date_from <= '2014-04-10 08:00:00' 
            AND  
            date_to >= '2014-04-10 08:00:00'
          )

          /* rows that met this condition were being discarded 
             as a result of 'difficult to define' GROUP BY behaviour. */
          OR date_from >= '2014-04-10 08:00:00' 
        )
      )
    )

  ORDER BY 
   ts.date_from ASC;

De plus, vous pouvez « ranger » votre requête plus loin pour que vous ne avez pas besoin de « brancher » votre status, city_idet les subject_idparamètres plus d'une fois.

Pour ce faire, modifiez la sous-requête apour sélectionner également ces colonnes et pour les regrouper également sur ces colonnes. Ensuite, la JOINde » ONclause aurait besoin de cartographier ces colonnes à leurs ts.xxxéquivalents.

Je ne pense pas que cela affectera négativement les performances, mais je ne pourrais pas être sûr sans tester sur un grand ensemble de données.

Ainsi, votre jointure ressemblera davantage à:

LEFT JOIN 
(
  SELECT 
    teacher_id,
    status,
    city_id,
    subject_id,
    DATE(MIN(date_from)) as closestDay

  FROM 
    teacher_slots

  WHERE   
    date_from >= '2014-04-10 08:00:00' 
    AND order_of_arrival = 0
  /* These no longer required here...
    AND status = 0 
    AND city_id = 6015 
    AND subject_id = 1
  */

  GROUP BY 
    teacher_id,
    status,
    city_id,
    subject_id

) a
ON a.teacher_id = ts.teacher_id
AND a.status = ts.status 
AND a.city_id = ts.city_id 
AND a.subject_id = ts.city_id
AND a.closestDay = DATE(ts.date_from)
Sepster
la source
2

Essayez cette requête:

(
select * from (SELECT id, teacher_slots.teacher_id, date_from, date_to,  order_of_arrival
FROM teacher_slots  WHERE teacher_slots.date_from >= '2014-04-10 08:00:00'
    AND teacher_slots.order_of_arrival = 0
    AND teacher_slots.status = 0
    AND teacher_slots.city_id = 6015
    AND teacher_slots.subject_id = 1) 
 teacher_slots
JOIN (
    SELECT DATE(MIN(date_from)) as closestDay, teacher_id
    FROM teacher_slots
    WHERE   date_from >= '2014-04-10 08:00:00' AND order_of_arrival = 0
            AND status = 0 AND city_id = 6015 AND subject_id = 1
    GROUP BY teacher_id
) a ON a.teacher_id = teacher_slots.teacher_id
AND DATE(teacher_slots.date_from) = closestDay

)

UNION ALL

(
SELECT id, teacher_id, date_from, date_to, order_of_arrival
FROM teacher_slots
WHERE order_of_arrival = 1 AND status = 0 AND city_id = 6015 AND subject_id = 1
    AND (
        (date_from <= '2014-04-10 08:00:00' AND  date_to >= '2014-04-10 08:00:00')
        OR (date_from >= '2014-04-10 08:00:00')
    )
GROUP BY teacher_id
)

ORDER BY date_from ASC;
Hackerman
la source