Las consultas individuales se ejecutan en 10 ms, con UNION ALL están tomando 290 ms + (7.7M registros MySQL DB). ¿Cómo optimizar?


9

Tengo una tabla que almacena citas disponibles para maestros, permitiendo dos tipos de inserciones:

  1. Basado en la hora : con total libertad para agregar espacios ilimitados por día por maestro (siempre y cuando los espacios no se superpongan): el 15 / Abr un maestro puede tener espacios a las 10:00, 11:00, 12:00 y 16:00 . Una persona es atendida después de elegir un horario / horario de maestro específico.

  2. Periodo / rango de tiempo : el 15 / Abr otro maestro puede trabajar de 10:00 a 12:00 y luego de 14:00 a 18:00. Una persona es atendida por orden de llegada, por lo que si un maestro trabaja de 10:00 a 12:00, todas las personas que lleguen en este período serán atendidas por orden de llegada (cola local).

Como tengo que devolver a todos los maestros disponibles en una búsqueda, necesito que todos los espacios se guarden en la misma tabla que el rango de orden de llegada. De esta manera puedo ordenar por fecha_desde ASC, mostrando primero los primeros espacios disponibles en los resultados de búsqueda.

Estructura de la tabla actual

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 busqueda

Necesito filtrar por: fecha y hora real, city_id, subject_id y si hay un espacio disponible (estado = 0).

Para cada hora , tengo que mostrar todos los espacios disponibles para el primer día disponible más cercano para cada maestro (mostrar todos los espacios de tiempo de un día determinado y no puedo mostrar más de un día para el mismo maestro). (Recibí la consulta con la ayuda de mattedgod ).

Para el rango basado (order_of_arrival = 1), tengo que mostrar el rango disponible más cercano, solo una vez por maestro.

La primera consulta se ejecuta individualmente en alrededor de 0,10 ms, la segunda consulta 0,08 ms y la UNION ALL un promedio de 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;

Pregunta

¿Hay alguna manera de optimizar UNION, para que pueda obtener una respuesta razonable de un máximo de ~ 20 ms o incluso un rango de retorno basado en + por hora en una sola consulta (con un IF, etc.)?

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

EDITAR:

Intenté alguna desnormalización creando un campo "only_date_from" donde almacené solo la fecha, para poder cambiar esto ...

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

... a esto

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

¡Ya me salvó 100ms! Todavía 200ms en promedio.

Respuestas:


1

En primer lugar, creo que su consulta original puede no ser "correcta"; Con referencia a su SQLFiddle, me parece que debería devolver filas con ID= 2, 3y 4(además de la fila con ID= 1que está obteniendo de esta mitad), porque su lógica existente parece como si estuviera destinada a estas otras filas para ser incluidos, ya que cumplen explícitamente la OR (date_from >= '2014-04-10 08:00:00')parte de su segunda WHEREcláusula.

La GROUP BY teacher_idcláusula en su segunda parte de su UNIONcausa le está haciendo perder esas filas. Esto se debe a que en realidad no está agregando ninguna columna en su lista de selección, y en este caso GROUP BYcausará un comportamiento 'difícil de definir'.

Además, aunque no puedo explicar el bajo rendimiento de su UNION, puedo solucionarlo eliminando por completo su consulta:

En lugar de usar dos conjuntos de lógica separados (y en partes, que se repiten) para obtener filas de la misma tabla, he consolidado su lógica en una consulta con las diferencias en su lógica ORjuntas, es decir, si una fila se encuentra con una u otra de sus WHEREcláusulas originales , está incluido. Esto es posible porque he reemplazado el (INNER) JOINque estaba usando para encontrar el closestDatecon a LEFT JOIN.

Esto LEFT JOINsignifica que ahora también podemos distinguir qué conjunto de lógica debería aplicarse a una fila; Si la unión funciona (la fecha más cercana NO ES NULA) aplicamos su lógica de la primera mitad, pero si la unión falla (la fecha más cercana es NULA) entonces aplicamos la lógica de su segunda mitad.

Entonces, esto devolverá todas las filas que devolvió su consulta (en el violín), y también recogerá aquellas adicionales.

  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;

Además, se puede "poner en orden" la consulta aún más por lo que no es necesario "tapón" en sus status, city_idy subject_idparámetros más de una vez.

Para hacer esto, cambie la subconsulta apara seleccionar también esas columnas y para agruparlas también. Entonces, la cláusula JOIN's' ONnecesitaría asignar esas columnas a sus ts.xxxequivalentes.

No creo que esto afecte negativamente el rendimiento, pero no podría estar seguro sin probar en un gran conjunto de datos.

Entonces su unión se verá más como:

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

Prueba 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;
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.