sql >> Base de Datos >  >> RDS >> Mysql

¿Cómo puedo optimizar aún más una consulta de tabla derivada que funciona mejor que el equivalente JOINed?

Bueno, encontré una solución. Tomó mucha experimentación, y creo que un poco de suerte ciega, pero aquí está:

CREATE TABLE magic ENGINE=MEMORY
SELECT
  s.shop_id AS shop_id,
  s.id AS shift_id,
  st.dow AS dow,
  st.start AS start,
  st.end AS end,
  su.user_id AS manager_id
FROM shifts s
JOIN shift_times st ON s.id = st.shift_id
JOIN shifts_users su ON s.id = su.shift_id
JOIN shift_positions sp ON su.shift_position_id = sp.id AND sp.level = 1

ALTER TABLE magic ADD INDEX (shop_id, dow);

CREATE TABLE tickets_extra ENGINE=MyISAM
SELECT 
  t.id AS ticket_id,
  (
    SELECT m.manager_id
    FROM magic m
    WHERE DAYOFWEEK(t.created) = m.dow
    AND TIME(t.created) BETWEEN m.start AND m.end
    AND m.shop_id = t.shop_id
  ) AS manager_created,
  (
    SELECT m.manager_id
    FROM magic m
    WHERE DAYOFWEEK(t.resolved) = m.dow
    AND TIME(t.resolved) BETWEEN m.start AND m.end
    AND m.shop_id = t.shop_id
  ) AS manager_resolved
FROM tickets t;
DROP TABLE magic;

Explicación extensa

Ahora, explicaré por qué esto funciona, y mi proceso relativo y los pasos para llegar aquí.

Primero, sabía que la consulta que estaba intentando estaba sufriendo debido a la enorme tabla derivada y las UNIONES subsiguientes en esto. Estaba tomando mi tabla de tickets bien indexada y uniendo todos los datos de shift_times en ella, luego dejé que MySQL masticara eso mientras intentaba unirse a la tabla shifts y shift_positions. Este gigante derivado sería un desastre sin indexar de hasta 2 millones de filas.

Ahora, yo sabía que esto estaba sucediendo. Sin embargo, la razón por la que estaba siguiendo este camino era porque la forma "correcta" de hacerlo, usando estrictamente JOINs, estaba tomando una cantidad de tiempo aún mayor. Esto se debe al desagradable caos que se requiere para determinar quién es el gerente de un turno determinado. Tengo que unirme a shift_times para averiguar cuál es el turno correcto, mientras que simultáneamente me uno a shift_positions para averiguar el nivel del usuario. No creo que el optimizador de MySQL maneje esto muy bien, y termina creando una ENORME monstruosidad de una tabla temporal de uniones, y luego filtrando lo que no se aplica.

Entonces, como la tabla derivada parecía ser el "camino a seguir", persistí obstinadamente en esto por un tiempo. Intenté convertirlo en una cláusula JOIN, sin mejoras. Intenté crear una tabla temporal con la tabla derivada, pero de nuevo fue demasiado lento porque la tabla temporal no estaba indexada.

Me di cuenta de que tenía que manejar este cálculo de turnos, tiempos, posiciones con sensatez. Pensé, tal vez una VISTA sería el camino a seguir. ¿Qué pasa si creé una VISTA que contenía esta información:(shop_id, shift_id, dow, start, end, manager_id). Entonces, simplemente tendría que unirme a la tabla de tickets por shop_id y todo el cálculo de DAYOFWEEK/TIME, y estaría en el negocio. Por supuesto, no recordé que MySQL maneja VIEW con bastante facilidad. No los materializa en absoluto, simplemente ejecuta la consulta que habría utilizado para obtener la vista por usted. Entonces, al unir tickets en esto, básicamente estaba ejecutando mi consulta original, sin mejoras.

Entonces, en lugar de una VISTA, decidí usar una TABLA TEMPORAL. Esto funcionó bien si solo buscaba uno de los administradores (creados o resueltos) a la vez, pero aun así era bastante lento. Además, descubrí que con MySQL no se puede hacer referencia a la misma tabla dos veces en la misma consulta (tendría que unirme a mi tabla temporal dos veces para poder diferenciar entre manager_created y manager_resolved). Este es un gran WTF, ya que puedo hacerlo siempre que no especifique "TEMPORAL":aquí es donde entró en juego la magia CREAR TABLA MOTOR =MEMORIA.

Con esta tabla pseudo temporal en la mano, probé mi JOIN solo para manager_created nuevamente. Funcionó bien, pero todavía bastante lento. Sin embargo, cuando me uní nuevamente para obtener manager_resolved en la misma consulta, el tiempo de consulta volvió a la estratosfera. Mirando EXPLAIN mostró el escaneo completo de la tabla de tickets (filas ~2mln), como se esperaba, y los JOIN en la mesa mágica en ~2,087 cada uno. Una vez más, parecía que estaba fallando.

Ahora comencé a pensar en cómo evitar los JOIN por completo y fue entonces cuando encontré una oscura publicación antigua en el tablero de mensajes donde alguien sugirió usar subselecciones (no puedo encontrar el enlace en mi historial). Esto es lo que condujo a la segunda consulta SELECT que se muestra arriba (la de creación de tickets_extra). En el caso de seleccionar solo un campo de administrador, funcionó bien, pero nuevamente con ambos fue una mierda. Miré EXPLAIN y vi esto:

*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: t
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 173825
        Extra: 
*************************** 2. row ***************************
           id: 3
  select_type: DEPENDENT SUBQUERY
        table: m
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 2037
        Extra: Using where
*************************** 3. row ***************************
           id: 2
  select_type: DEPENDENT SUBQUERY
        table: m
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 2037
        Extra: Using where
3 rows in set (0.00 sec)

Ack, la temida SUBCONSULTA DEPENDIENTE. A menudo se sugiere evitarlos, ya que MySQL generalmente los ejecutará de afuera hacia adentro, ejecutando la consulta interna para cada fila de la externa. Ignoré esto y me pregunté:"Bueno... ¿y si indexo esta estúpida tabla mágica?". Así nació el índice ADD (shop_id, dow).

Mira esto:

mysql> CREATE TABLE magic ENGINE=MEMORY
<snip>
Query OK, 3220 rows affected (0.40 sec)

mysql> ALTER TABLE magic ADD INDEX (shop_id, dow);
Query OK, 3220 rows affected (0.02 sec)

mysql> CREATE TABLE tickets_extra ENGINE=MyISAM
<snip>
Query OK, 1933769 rows affected (24.18 sec)

mysql> drop table magic;
Query OK, 0 rows affected (0.00 sec)

Ahora ESO ES de lo que estoy hablando!

Conclusión

Esta es definitivamente la primera vez que he creado una tabla NO TEMPORAL sobre la marcha, y la he INDEXADO sobre la marcha, simplemente para hacer una sola consulta de manera eficiente. Supongo que siempre asumí que agregar un índice sobre la marcha es una operación prohibitivamente costosa. (Agregar un índice en mi tabla de tickets de 2 millones de filas puede llevar más de una hora). Sin embargo, para tan solo 3000 filas, esto es pan comido.

No tenga miedo de las SUBCONSULTAS DEPENDIENTES, la creación de tablas TEMPORALES que realmente no lo son, la indexación sobre la marcha o los extraterrestres. Todos pueden ser cosas buenas en la situación correcta.

Gracias por toda la ayuda StackOverflow. :-D