Consultas individuais são executadas em 10ms, com UNION ALL que estão levando 290ms + (7,7 milhões de registros no MySQL DB). Como otimizar?


9

Eu tenho uma tabela que armazena compromissos disponíveis para professores, permitindo dois tipos de inserções:

  1. Horário : com total liberdade para adicionar vagas ilimitadas por dia por professor (contanto que as vagas não se sobreponham): em 15 de abril, o professor pode ter vagas às 10:00, 11:00, 12:00 e 16:00 . Uma pessoa é atendida após escolher um horário / horário específico para o professor.

  2. Período / intervalo de tempo : em 15 / abr, outro professor pode trabalhar das 10:00 às 12:00 e, em seguida, das 14:00 às 18:00. Uma pessoa é atendida por ordem de chegada; portanto, se um professor trabalha das 10:00 às 12:00, todas as pessoas que chegarem nesse período serão atendidas por ordem de chegada (fila local).

Como tenho que retornar todos os professores disponíveis em uma pesquisa, preciso que todos os slots sejam salvos na mesma tabela que a ordem de chegada. Dessa forma, posso encomendar por date_from ASC, mostrando os primeiros slots disponíveis primeiro nos resultados da pesquisa.

Estrutura atual da tabela

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;

Consulta de pesquisa

Preciso filtrar por: data / hora real, city_id, subject_id e se um slot está disponível (status = 0).

Para horários, eu tenho que mostrar todos os horários disponíveis para o primeiro dia mais próximo disponível para cada professor (mostrar todos os horários de um determinado dia e não posso mostrar mais de um dia para o mesmo professor). (Eu recebi a consulta com a ajuda de mattedgod ).

Para o intervalo baseado (order_of_arrival = 1), preciso mostrar o intervalo disponível mais próximo, apenas uma vez por professor.

A primeira consulta é executada individualmente em cerca de 0,10 ms, a segunda consulta em 0,08 ms e o UNION ALL em uma média de 300ms.

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

Questão

Existe uma maneira de otimizar o UNION, para que eu possa obter uma resposta razoável de no máximo ~ 20ms ou até mesmo retornar intervalo com base + horária com base em apenas uma consulta (com um IF, etc)?

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

EDITAR:

Tentei alguma desnormalização criando um campo "only_date_from" onde armazenava apenas a data, para que eu pudesse alterar isso ...

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

... para isso

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

Já me salvou 100ms! Ainda 200ms em média.

Respostas:


1

Em primeiro lugar, acho que sua consulta original pode não estar "correta"; Com referência à sua SQLFiddle, parece-me que você deve estar retornando linhas com ID= 2, 3e 4(além da linha com ID= 1você está recebendo a partir dessa metade), porque a sua lógica existente aparece como se você destinado para essas outras linhas para ser incluído, pois eles cumprem explicitamente a OR (date_from >= '2014-04-10 08:00:00')parte da sua segunda WHEREcláusula.

A GROUP BY teacher_idcláusula na sua segunda parte UNIONestá causando a perda dessas linhas. Isso ocorre porque você não está realmente agregando colunas na sua lista de seleção e, nesse caso GROUP BY, causará um comportamento 'difícil de definir'.

Além disso, embora eu não possa explicar o desempenho ruim da sua UNION, eu posso contorná-la removendo-a da sua consulta:

Em vez de usar dois conjuntos de lógica separados (e em partes, repetidos) para obter linhas da mesma tabela, eu consolidei sua lógica em uma consulta com as diferenças em sua lógica ORjuntas - ou seja, se uma linha encontrar uma ou outra das suas WHEREcláusulas originais , está incluído. Isso é possível porque substituí o que (INNER) JOINvocê estava usando para encontrar o closestDatecom a LEFT JOIN.

Isso LEFT JOINsignifica que agora também podemos distinguir qual conjunto de lógica deve ser aplicado a uma linha; Se a associação funcionar (data mais próxima NÃO É NULL), aplicaremos sua lógica a partir da primeira metade, mas se a associação falhar (data mais próxima É NULL), aplicaremos a lógica a partir da segunda metade.

Portanto, isso retornará todas as linhas que sua consulta retornou (no violino) e também estará captando as linhas adicionais.

  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;

Além disso, você pode "arrumar" sua consulta ainda mais para que você não precisa "plug-in" seu status, city_ide subject_idos parâmetros mais de uma vez.

Para fazer isso, altere a subconsulta apara selecionar também essas colunas e também agrupá-las. Em seguida, o JOIN's ONcláusula seria necessário para mapear as colunas aos seus ts.xxxequivalentes.

Eu não acho que isso afetará negativamente o desempenho, mas não poderia ter certeza sem testar em um grande conjunto de dados.

Portanto, sua associação será mais parecida com:

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)

2

Tente esta consulta:

(
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;
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.