Otimizando a condição WHERE para o campo TIMESTAMP na instrução MySQL SELECT

8

Estou trabalhando em um esquema para um sistema de análise que rastreia os tempos de uso e é necessário ver o tempo total de uso em um determinado período.

Para dar um exemplo simples, esse tipo de consulta seria executado com frequência:

select sum(diff_ms) from writetest_table where time_on > ("2015-07-13 15:11:56");

Essa consulta normalmente leva cerca de 7 segundos em uma tabela que é muito preenchida. Possui ~ 35 milhões de linhas, o MyISAM no MySQL é executado no Amazon RDS (db.m3.xlarge).

A eliminação da cláusula WHERE faz com que a consulta demore apenas 4 segundos e a adição de uma segunda cláusula (time_off> XXX) adiciona mais 1,5 segundos, elevando o tempo da consulta para 8,5 segundos.

Como eu sei que esses tipos de consultas serão normalmente feitos, eu gostaria de otimizar as coisas para que sejam mais rápidas, de preferência abaixo de 5 segundos.

Comecei adicionando um índice em time_on e, apesar de acelerar drasticamente uma consulta WHERE "=", ela não teve efeito na consulta ">". Existe uma maneira de criar um índice que acelere as consultas WHERE ">" ou "<"?

Ou, se houver outras sugestões sobre o desempenho desse tipo de consulta, entre em contato.

Nota: Estou usando o campo "diff_ms" como uma etapa de desnormalização (igual a time_off - time_on) que melhora o desempenho da agregação em cerca de 30% a 40%.

Estou criando o índice com este comando:

ALTER TABLE writetest_table ADD INDEX time_on (time_on) USING BTREE;

A execução de "explicação" na consulta original (com "time_on>") indica que time_on é uma "chave possível" e o tipo de seleção é "SIMPLES". A coluna "extra" diz "Usando onde" e "tipo" é "TUDO". Depois que o índice foi adicionado, a tabela diz que "time_on" é o tipo de chave "MUL", que parece correto, pois o mesmo tempo pode estar presente duas vezes.

Aqui está o esquema da tabela:

CREATE TABLE `writetest_table` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `sessionID` int(11) DEFAULT NULL,
  `time_on` timestamp NULL DEFAULT NULL,
  `time_off` timestamp NULL DEFAULT NULL,
  `diff_ms` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `time_on` (`time_on`)
) ENGINE=MyISAM AUTO_INCREMENT=50410902 DEFAULT CHARSET=latin1;

UPDATE: Criei o seguinte índice com base na resposta do ypercube, mas isso aumenta o tempo de consulta da primeira consulta para cerca de 17 segundos!

ALTER TABLE writetest_table  ADD INDEX time_on__diff_ms__ix (time_on, diff_ms) ;

ATUALIZAÇÃO 2: saída EXPLAIN

mysql> explain select sum(diff_ms) from writetest_table where time_on > '2015-07-13 15:11:56';
+----+-------------+---------------------+-------+----------------------+----------------------+---------+------+----------+--------------------------+
| id | select_type | table               | type  | possible_keys        | key                  | key_len | ref  | rows     | Extra                    |
+----+-------------+---------------------+-------+----------------------+----------------------+---------+------+----------+--------------------------+
|  1 | SIMPLE      | writetest_table_old | index | time_on__diff_ms__ix | time_on__diff_ms__ix | 10      | NULL | 35831102 | Using where; Using index |
+----+-------------+---------------------+-------+----------------------+----------------------+---------+------+----------+--------------------------+
1 row in set (0.00 sec)

Atualização 3: resultado da consulta solicitada

mysql> SELECT time_on FROM writetest_table ORDER BY time_on LIMIT 1;
+---------------------+
| time_on             |
+---------------------+
| 2015-07-13 15:11:56 |
+---------------------+
1 row in set (0.01 sec)
Locksleyu
fonte
Você realmente tem nulos nessas 2 colunas ( time_one diff_ms)? O que acontece se você adicionar na consulta WHERE ... AND diff_ms IS NOT NULL?
ypercubeᵀᴹ
Você pode por favor nos mostrar a saída deSELECT COUNT(*), COUNT(diff_ms) FROM writetest_table;
ypercubec
Além disso, a explicação na sua "Atualização 2" mostra " tabela:writetest_table_old " enquanto a consulta o possui from writetest_table. Isso é um erro de digitação ou você executa a consulta em uma tabela diferente?
ypercubeᵀᴹ

Respostas:

3

Eu acho que estou começando a entender.

Quando eu pedi para você correr

SELECT time_on FROM writetest_table ORDER BY time_on LIMIT 1;

Você disse que era isso 2015-07-13 15:11:56que você tem na sua WHEREcláusula

Quando você fez a consulta

select sum(diff_ms) from writetest_table;

Ele executou uma verificação de tabela completa de 35,8 milhões de linhas.

Quando você fez a consulta

select sum(diff_ms) from writetest_table where time_on > ("2015-07-13 15:11:56");

Ele executou uma verificação de índice completa de 35,8 milhões de linhas.

Faz totalmente sentido que a consulta sem a cláusula WHERE seja mais rápida. Por quê ?

A varredura da tabela exibirá 35,8 milhões de linhas em uma passagem linear.

O EXPLAIN na consulta com o WHERE também aumentou 35,8 milhões de linhas. Uma varredura de índice se comportaria um pouco diferente. Enquanto o BTREE mantém a ordem das teclas, é horrível fazer varreduras de alcance. No seu caso particular, você está executando a pior varredura de intervalo possível, com o mesmo número de entradas BTREE que existem linhas na tabela. O MySQL precisa percorrer as páginas do BTREE (pelo menos entre os nós da folha) para ler os valores. Além disso, a time_oncoluna deve ser comparada ao longo do caminho na ordem ditada pelo índice. Portanto, os nós BTREE que não são folhas também devem ser atravessados.

Por favor, veja minhas postagens no BTREEs

Se a consulta fosse hoje à meia-noite de hoje

select sum(diff_ms) from writetest_table where time_on >= ("2015-07-14 00:00:00");

ou mesmo meio dia hoje

select sum(diff_ms) from writetest_table where time_on >= ("2015-07-14 12:00:00");

isso deve levar menos tempo.

MORAL DA HISTÓRIA: Não use uma cláusula WHERE que faça uma varredura de intervalo ordenada igual ao número de linhas na tabela de destino.

RolandoMySQLDBA
fonte
Meu único problema é como sair daqui. Fiz uma consulta com uma data que resultou em apenas 1 milhão de linhas filtradas e a soma levou apenas 1 segundo. Mas, ocasionalmente, talvez eu precise fazer somas agregadas na maioria dos dados. Alguma sugestão de como lidar com isso? Eu esperava que o MySQL fosse inteligente o suficiente para saber quando usar o índice e quando não também, mas acho que não há informações suficientes nesse caso.
Locksleyu
Eu realmente gostaria que houvesse algum tipo de índice organizado para tornar as cláusulas WHERE especificando intervalos de datas rapidamente, que parece ser tecnicamente possível de implementar, mas acho que não é suportado.
Locksleyu
Você tem muitos dados em um intervalo tão curto. Nenhuma cláusula WHERE pode ser compensada. Por quê ? Não é o índice que é o problema. É a opinião do MySQL Query Optimizer sobre o índice. Quando você começa a acumular muito mais dados (digamos duas semanas), as estatísticas do índice devem se estabilizar e você deve ver uma melhoria no desempenho. Apenas não faça verificações de índice completas.
RolandoMySQLDBA 14/07
4

Para a consulta específica:

select sum(diff_ms) 
from writetest_table 
where time_on > '2015-07-13 15:11:56' ;     -- use single quotes, not double

um índice em (time_on, diff_ms)seria a melhor opção. Portanto, se a consulta for executada com bastante frequência ou se sua eficiência for crucial para o seu aplicativo, adicione este índice:

ALTER TABLE writetest_table 
  ADD INDEX time_on__diff_ms__ix      -- pick a name for the index
    (time_on, diff_ms) ;

(Não está relacionado à pergunta)
E, realmente, mude o mecanismo da tabela para InnoDB. É 2015 e o funeral do MyISAM foi há alguns anos atrás.
(/ rant)

ypercubeᵀᴹ
fonte
Criei o índice exato que você sugeriu e, em seguida, executei a consulta exata que você mencionou primeiro em sua resposta, mas o tempo agora é muito pior, levando cerca de 17 segundos de forma consistente (tentei várias vezes).
Locksleyu
Não tenho ideia do que está causando isso. Caso isso aconteça, existem apenas 3671 valores distintos de time_on na tabela (isso se deve à maneira como meu script de teste está preenchendo dados).
Locksleyu
Você deve fazer três (3) coisas: 1. executar ALTER TABLE writetest_table DROP INDEX time_on;, 2) executar ANALYZE TABLE writetest_table;e 3) executar novamente a consulta. O tempo volta para 7 segundos?
RolandoMySQLDBA
11
Você também deve executar EXPLAIN select sum(diff_ms) from writetest_table where time_on > ("2015-07-13 15:11:56");. O novo índice está sendo usado? Se não estiver sendo usado, eu diria que é a sua população-chave, principalmente se o seu tempo inicial for apenas alguns dias atrás. Como o número de linhas aumenta com dias mais distintos, a distribuição de chaves deve se estabilizar e o EXPLAIN deve ser melhor .
RolandoMySQLDBA
RolandoMySQLDBA - Eu tentei suas três etapas e sim o tempo remonta a 7 segundos. Eu expliquei e ele diz que o índice está sendo usado. Ainda não entendi porque a adição de um índice como esse poderia tornar o desempenho duas vezes pior.
Locksleyu