Mysql
 sql >> Base de Dados >  >> RDS >> Mysql

Como posso otimizar ainda mais uma consulta de tabela derivada que tem um desempenho melhor do que o equivalente JOINed?


Bem, eu encontrei uma solução. Levou muita experimentação, e acho que um pouco de sorte cega, mas aqui 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;

Explicação extensa


Agora, vou explicar por que isso funciona, e meu processo de pensamento relativo e etapas para chegar aqui.

Primeiro, eu sabia que a consulta que estava tentando estava sofrendo por causa da enorme tabela derivada e dos JOINs subsequentes para isso. Eu estava pegando minha tabela de tickets bem indexada e juntando todos os dados shift_times nela, então deixando o MySQL mastigar isso enquanto tentava juntar as tabelas shifts e shift_positions. Esse gigante derivado seria uma bagunça não indexada de até 2 milhões de linhas.

Agora, eu sabia que isso estava acontecendo. A razão pela qual eu estava indo por esse caminho era porque a maneira "correta" de fazer isso, usando estritamente JOINs, estava demorando ainda mais. Isso se deve ao caos desagradável necessário para determinar quem é o gerente de um determinado turno. Eu tenho que me juntar a shift_times para descobrir qual é o turno correto, enquanto simultaneamente me junta a shift_positions para descobrir o nível do usuário. Eu não acho que o otimizador do MySQL lida com isso muito bem e acaba criando uma enorme monstruosidade de uma tabela temporária de junções, depois filtrando o que não se aplica.

Então, como a tabela derivada parecia ser o "caminho a seguir", eu obstinadamente persisti nisso por um tempo. Tentei colocá-lo em uma cláusula JOIN, sem melhora. Tentei criar uma tabela temporária com a tabela derivada, mas novamente estava muito lento, pois a tabela temporária não estava indexada.

Percebi que tinha que lidar com esse cálculo de turno, tempos, posições de forma sensata. Eu pensei, talvez um VIEW seria o caminho a percorrer. E se eu criasse uma VIEW que contivesse essas informações:(shop_id, shift_id, dow, start, end, manager_id). Então, eu simplesmente teria que juntar a tabela de tickets por shop_id e todo o cálculo DAYOFWEEK/TIME, e eu estaria no negócio. Claro, eu falhei em lembrar que o MySQL lida com VIEWs com bastante facilidade. Ele não os materializa, simplesmente executa a consulta que você teria usado para obter a visualização para você. Então, juntando tickets para isso, eu estava essencialmente executando minha consulta original - sem melhorias.

Então, ao invés de uma VIEW eu decidi usar uma TABELA TEMPORÁRIA. Isso funcionou bem se eu apenas buscasse um dos gerenciadores (criados ou resolvidos) por vez, mas ainda era muito lento. Além disso, descobri que com o MySQL você não pode se referir à mesma tabela duas vezes na mesma consulta (eu teria que juntar minha tabela temporária duas vezes para poder diferenciar entre manager_created e manager_resolved). Este é um grande WTF, pois posso fazê-lo desde que não especifique "TEMPORARY" - é aqui que a mágica CREATE TABLE ENGINE=MEMORY entrou em ação.

Com essa pseudo tabela temporária em mãos, tentei meu JOIN apenas para manager_created novamente. Ele teve um bom desempenho, mas ainda bastante lento. No entanto, quando entrei novamente para obter manager_resolved na mesma consulta, o tempo de consulta voltou para a estratosfera. Observar o EXPLAIN mostrou a verificação completa da tabela de tickets (linhas ~ 2 milhões), conforme esperado, e os JOINs na tabela mágica em ~ 2.087 cada. Mais uma vez, eu parecia estar correndo para falhar.

Agora comecei a pensar em como evitar os JOINs completamente e foi quando encontrei um post obscuro no fórum de mensagens onde alguém sugeriu o uso de subseleções (não consigo encontrar o link no meu histórico). Isso é o que levou à segunda consulta SELECT mostrada acima (a criação de tickets_extra). No caso de selecionar apenas um único campo de gerente, ele teve um bom desempenho, mas novamente com ambos foi uma porcaria. Eu olhei para o EXPLAIN e vi isso:
*************************** 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, a temida SUBCONSULTA DEPENDENTE. Muitas vezes é sugerido evitá-los, pois o MySQL geralmente os executará de fora para dentro, executando a consulta interna para cada linha do externo. Ignorei isso e me perguntei:"Bem... e se eu indexasse essa estúpida tabela mágica?". Assim nasceu o índice ADD (shop_id, dow).

Veja isso:
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)

Agora ISSO É do que estou falando!

Conclusão


Esta é definitivamente a primeira vez que eu criei uma tabela não-TEMPORARY em tempo real, e a INDEXei em tempo real, simplesmente para fazer uma única consulta com eficiência. Acho que sempre assumi que adicionar um índice em tempo real é uma operação proibitivamente cara. (Adicionar um índice na minha tabela de tickets de 2 milhões de linhas pode levar mais de uma hora). No entanto, para apenas 3.000 linhas, isso é uma moleza.

Não tenha medo de SUBCONSULTAS DEPENDENTES, criando tabelas TEMPORÁRIAS que realmente não são, indexação em tempo real ou alienígenas. Todos eles podem ser coisas boas na situação certa.

Obrigado por toda a ajuda StackOverflow. :-D