Poszczególne zapytania działają przez 10 ms, a UNION ALL zajmuje 290 ms + (7,7 mln rekordów MySQL DB). Jak zoptymalizować?

9

Mam tabelę, która przechowuje dostępne terminy dla nauczycieli, pozwalając na dwa rodzaje wstawek:

  1. Oparte na godzinach: z całkowitą swobodą dodawania nieograniczonej liczby miejsc dziennie na nauczyciela (o ile miejsca nie pokrywają się): w dniu 15 kwietnia kwietnia nauczyciel może mieć miejsca o 10:00, 11:00, 12:00 i 16:00 . Osoba jest obsługiwana po wybraniu określonego czasu nauczyciela / przedziału czasowego.

  2. Przedział czasu / zakres : 15 kwietnia kwietnia inny nauczyciel może pracować od 10:00 do 12:00, a następnie od 14:00 do 18:00. Osoba jest obsługiwana według kolejności przyjazdu, więc jeśli nauczyciel pracuje od 10:00 do 12:00, wszystkie osoby, które przybędą w tym okresie, będą uczęszczane według kolejności przyjazdu (kolejka lokalna).

Ponieważ muszę zwrócić wszystkich dostępnych nauczycieli w poszukiwaniu, potrzebuję zapisać wszystkie miejsca w tej samej tabeli co zakresy kolejności przybycia. W ten sposób mogę zamówić według daty_z ASC, pokazując pierwsze dostępne automaty jako pierwsze w wynikach wyszukiwania.

Obecna struktura tabeli

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;

Wyszukiwana fraza

Muszę filtrować według: aktualnej daty / godziny, city_id, subject_id i czy dostępny jest slot (status = 0).

Na podstawie godzinowej muszę pokazać wszystkie dostępne przedziały dla pierwszego najbliższego dostępnego dnia dla każdego nauczyciela (pokaż wszystkie przedziały czasowe danego dnia i nie mogę pokazać więcej niż jednego dnia dla tego samego nauczyciela). (Otrzymałem zapytanie z pomocą Mattedgod ).

W przypadku zakresu (order_of_arrival = 1) muszę pokazać najbliższy dostępny zakres, tylko jeden raz na nauczyciela.

Pierwsze zapytanie uruchamia się indywidualnie w około 0,10 ms, drugie zapytanie 0,08 ms, a UNION ALL średnio 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;

Pytanie

Czy istnieje sposób na zoptymalizowanie UNION, aby uzyskać rozsądną odpowiedź wynoszącą maksymalnie ~ 20 ms lub nawet zwrot oparty na podstawie + co godzinę na podstawie tylko jednego zapytania (z IF, itp.)?

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

EDYTOWAĆ:

Próbowałem trochę denormalizacji, tworząc pole „only_date_from”, w którym zapisałem tylko datę, więc mogłem to zmienić ...

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

... do tego

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

To już zaoszczędziło mi 100ms! Średnio nadal 200 ms.

AlfredBaudisch
źródło

Odpowiedzi:

1

Po pierwsze, myślę, że twoje pierwotne zapytanie może nie być „poprawne”; Nawiązując do swojej SQLFiddle, to wygląda mi się, że powinno być powrocie wiersze z ID= 2, 3i 4(oprócz rzędu z ID= 1wy coraz z tego połowę), ponieważ istniejąca logika wydaje się tak, jakby przeznaczone dla tych innych wierszy do uwzględnienia, ponieważ wyraźnie spełniają OR (date_from >= '2014-04-10 08:00:00')część drugiej WHEREklauzuli.

GROUP BY teacher_idKlauzula w drugiej części swojego UNIONpowoduje utratę tych wierszy. Wynika to z faktu, że tak naprawdę nie agregujesz żadnych kolumn na liście wyboru, aw tym przypadku GROUP BYspowoduje to zachowanie „trudne do zdefiniowania”.

Ponadto, chociaż nie potrafię wyjaśnić słabej wydajności UNION, mogę go obejść, całkowicie usuwając go z zapytania:

Zamiast używać dwóch oddzielnych (i częściowo powtarzających się) zestawów logiki w celu uzyskania wierszy z tej samej tabeli, skonsolidowałem logikę w jedno zapytanie z różnicami w logice ORzmontowanymi razem - tj. Jeśli wiersz spełnia jeden lub drugi z oryginalnych WHEREklauzul, to jest uwzględnione. Jest to możliwe dlatego, że otrzymuje (INNER) JOINuzywasz wybrać closestDatez LEFT JOIN.

Oznacza LEFT JOINto, że jesteśmy teraz w stanie odróżnić, który zestaw logiki powinien zostać zastosowany do wiersza; Jeśli złączenie działa (data najbliższa NIE JEST NULL), zastosujemy logikę z pierwszej połowy, ale jeśli połączenie się nie powiedzie (data najbliższa NULL), wówczas zastosujemy logikę z drugiej połowy.

To zwróci wszystkie wiersze, które zwróciło zapytanie (w skrzypcach), a także zbierze te dodatkowe.

  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;

Ponadto, można „posprzątać” zapytanie dalej, tak że nie trzeba do „plug in” Twoje status, city_idi subject_idparametrów więcej niż jeden raz.

Aby to zrobić, zmień podkwerendę, aaby również wybrać te kolumny, a także pogrupować te kolumny. Następnie JOIN„s ONklauzuli musiałby map tych kolumn do swoich ts.xxxodpowiedników.

Nie sądzę, aby wpłynęło to negatywnie na wydajność, ale nie byłem pewien bez testowania dużego zestawu danych.

Twoje dołączenie będzie więc wyglądać bardziej jak:

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
źródło
2

Spróbuj tego zapytania:

(
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
źródło