Отдельные запросы выполняются за 10 мс, с UNION ALL они занимают 290 мс + (7,7 млн ​​записей MySQL DB). Как оптимизировать?

9

У меня есть таблица, в которой хранятся доступные встречи для учителей, позволяющие вставлять два вида:

  1. На почасовой основе : с полной свободой добавлять неограниченное количество слотов в день на учителя (если слоты не перекрываются): 15 апреля преподаватель может иметь слоты в 10:00, 11:00, 12:00 и 16:00. , Человек обслуживается после выбора определенного времени учителя / слота.

  2. Период времени / диапазон : 15 апреля другой преподаватель может работать с 10:00 до 12:00, а затем с 14:00 до 18:00. Лицо обслуживается по порядку прибытия, поэтому, если учитель работает с 10:00 до 12:00, все лица, прибывающие в этот период, будут сопровождаться порядком прибытия (местная очередь).

Так как я должен вернуть всех доступных учителей в поиске, мне нужно, чтобы все слоты были сохранены в той же таблице, что и диапазоны порядка прибытия. Таким образом, я могу заказать по дате от ASC, показывая первые доступные слоты первыми в результатах поиска.

Текущая структура таблицы

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;

Поисковый запрос

Мне нужно отфильтровать по: фактическому datetime, city_id, subject_id и, если слот доступен (status = 0).

Для почасовой оплаты я должен показать все доступные слоты на первый ближайший доступный день для каждого учителя (показать все временные интервалы данного дня и не может показывать более одного дня для одного и того же учителя). (Я получил запрос с помощью mattedgod ).

Для основанного на диапазоне (order_of_arrival = 1) я должен показать самый близкий доступный диапазон, только один раз на учителя.

Первый запрос выполняется индивидуально примерно за 0,10 мс, второй запрос - 0,08 мс, а UNION ALL - в среднем за 300 мс.

(
    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;

Вопрос

Есть ли способ оптимизировать UNION, чтобы я мог получить разумный ответ максимум ~ 20 мс или даже диапазон возврата на основе + ежечасно на основе только одного запроса (с IF и т. Д.)?

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

РЕДАКТИРОВАТЬ:

Я попытался немного денормализовать, создав поле "only_date_from", где я сохранил только дату, чтобы я мог изменить это ...

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

... к этому

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

Это уже спасло меня 100 мс! Еще 200 мс в среднем.

AlfredBaudisch
источник

Ответы:

1

Во-первых, я думаю, что ваш исходный запрос не может быть «правильным»; Со ссылкой на ваш SQLFiddle, он смотрит на меня , как будто вы должны возвращаться строками с ID= 2, 3и 4(в дополнении к ряду с ID= 1вы будете получать от половины), потому что существующая логика выглядит как будто вы предназначены для этих других строк быть включенным, поскольку они явно соответствуют OR (date_from >= '2014-04-10 08:00:00')части вашего второго WHEREпредложения.

Предложение GROUP BY teacher_idв вашей второй части UNIONзаставляет вас терять эти строки. Это потому, что вы фактически не агрегируете какие-либо столбцы в списке выбора, и в этом случае GROUP BYповедение будет «трудно определять».

Кроме того, хотя я не могу объяснить низкую производительность вашей системы UNION, я могу обойти ее, просто убрав ее из вашего запроса:

Вместо того, чтобы использовать два отдельных (и по частям повторяющихся) набора логики для получения строк из одной и той же таблицы, я объединил вашу логику в один запрос с различиями в вашей логике, ORсобранной вместе - то есть, если строка встречает один или другой из ваших оригинальных WHEREстатей, это включено. Это возможно, потому что я заменил тот, который (INNER) JOINвы использовали, чтобы найти closestDateс LEFT JOIN.

Это LEFT JOINозначает, что теперь мы также можем различать, какой набор логики следует применять к строке; Если объединение работает (closestDate IS NOT NULL), мы применяем вашу логику из первой половины, но если соединение не удается (closestDate IS NULL), тогда мы применяем логику из вашей второй половины.

Таким образом, он вернет все строки, которые возвратил ваш запрос (в скрипке), и также соберет эти дополнительные.

  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;

Кроме того, вы можете «привести в порядок» запрос дальше , так что вам не нужно «вилки в» ваших status, city_idи subject_idпараметры более чем один раз.

Для этого измените подзапрос, aчтобы также выбрать эти столбцы и сгруппировать их по этим столбцам. Затем в предложении JOIN's' ONнеобходимо сопоставить эти столбцы с их ts.xxxэквивалентами.

Я не думаю, что это отрицательно скажется на производительности, но не было бы уверенности без тестирования большого набора данных.

Таким образом, ваше объединение будет выглядеть так:

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
источник
2

Попробуйте этот запрос:

(
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
источник