Optimización de la condición WHERE para el campo TIMESTAMP en la instrucción MySQL SELECT

8

Estoy trabajando en un esquema para un sistema de análisis que rastrea los tiempos de uso, y es necesario ver el tiempo de uso total en un cierto rango de fechas.

Para dar un ejemplo simple, este tipo de consulta se ejecutará con frecuencia:

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

Esta consulta generalmente toma alrededor de 7 segundos en una tabla que está muy poblada. Tiene ~ 35 millones de filas, MyISAM en MySQL ejecutándose en Amazon RDS (db.m3.xlarge).

Deshacerse de la cláusula WHERE hace que la consulta tome solo 4 segundos, y agregar una segunda cláusula (time_off> XXX) agrega 1.5 segundos adicionales, lo que lleva el tiempo de consulta a 8.5 segundos.

Como sé que este tipo de consultas se realizarán comúnmente, me gustaría optimizar las cosas para que sean más rápidas, idealmente por debajo de 5 segundos.

Comencé agregando un índice en time_on, y aunque eso aceleró drásticamente una consulta WHERE "=", no tuvo ningún efecto en la consulta ">". ¿Hay alguna manera de crear un índice que acelere las consultas WHERE ">" o "<"?

O si hay alguna otra sugerencia sobre el rendimiento de este tipo de consulta, hágamelo saber.

Nota: Estoy usando el campo "diff_ms" como un paso de desnormalización (es igual a time_off - time_on) que mejora el rendimiento de la agregación en un 30% -40%.

Estoy creando el índice con este comando:

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

Ejecutar "explicar" en la consulta original (con "time_on>") dice que time_on es una "posible_clave" y select_type es "SIMPLE". La columna "extra" dice "Uso de where" y "type" es "ALL". Después de agregar el índice, la tabla dice que "time_on" es el tipo de clave "MUL", lo que parece correcto ya que el mismo tiempo puede estar presente dos veces.

Aquí está el esquema de la tabla:

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;

ACTUALIZACIÓN: ¡Creé el siguiente índice basado en la respuesta de ypercube, pero esto aumenta el tiempo de consulta para la primera consulta a alrededor de 17 segundos!

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

ACTUALIZACIÓN 2: EXPLICAR la salida

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)

Actualización 3: resultado de la 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
fuente
¿Realmente tienes nulos en estas 2 columnas ( time_ony diff_ms)? ¿Qué sucede si agrega la consulta WHERE ... AND diff_ms IS NOT NULL?
ypercubeᵀᴹ
¿Nos puede mostrar la salida deSELECT COUNT(*), COUNT(diff_ms) FROM writetest_table;
ypercubeᵀᴹ
También la explicación en su "Actualización 2" muestra " tabla:writetest_table_old " mientras que la consulta tiene from writetest_table. ¿Es un error tipográfico o ejecuta la consulta en una tabla diferente?
ypercubeᵀᴹ

Respuestas:

3

Creo que estoy empezando a entender.

Cuando te pedí que corrieras

SELECT time_on FROM writetest_table ORDER BY time_on LIMIT 1;

Dijiste que era 2015-07-13 15:11:56lo que tienes en tu WHEREcláusula

Cuando hiciste la consulta

select sum(diff_ms) from writetest_table;

Realizó una exploración de tabla completa de 35.8 millones de filas.

Cuando hiciste la consulta

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

Realizó un escaneo de índice completo de 35.8 millones de filas.

Tiene sentido que la consulta sin la cláusula WHERE sea más rápida. Por qué ?

El escaneo de la tabla leería 35.8 millones de filas en una pasada lineal.

El EXPLICAR en la consulta con WHERE también arrojó 35.8 millones de filas. Una exploración de índice se comportaría un poco diferente. Si bien el BTREE mantiene el orden de las teclas, es horrible hacer escaneos de rango. En su caso particular, está realizando el peor escaneo de rango posible, que tendría la misma cantidad de entradas BTREE que filas en la tabla. MySQL tiene que atravesar las páginas BTREE (al menos a través de los nodos hoja) para leer los valores. Además, la time_oncolumna debe compararse a lo largo del camino en el orden dictado por el índice. Por lo tanto, los nodos BTREE que no son hojas también deben atravesarse.

Por favor vea mis publicaciones en BTREEs

Si la consulta fue hoy a medianoche

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

o incluso al mediodía de hoy

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

Debería tomar menos tiempo.

MORAL DE LA HISTORIA: No use una cláusula WHERE que haga un escaneo de rango ordenado igual al número de filas en la tabla de destino.

RolandoMySQLDBA
fuente
Mi único problema es cómo ir desde aquí. Hice una consulta con una fecha que resultó en solo 1 millón de filas filtradas y la suma tomó solo 1 segundo. Pero ocasionalmente tengo que hacer sumas agregadas en la mayoría de los datos. ¿Alguna sugerencia sobre cómo manejar esto? Esperaba que MySQL fuera lo suficientemente inteligente como para saber cuándo usar el índice y cuándo no, pero supongo que en este caso no tiene suficiente información.
Locksleyu
Realmente desearía que hubiera algún tipo de índice organizado para hacer que las cláusulas WHERE que especifican rangos de fechas sean rápidas, parece que técnicamente sería posible implementarlo, pero supongo que no es compatible.
Locksleyu
Tienes demasiados datos en un rango tan corto. Ninguna cláusula WHERE puede ser compensada. Por qué ? No es el índice el problema. Es la opinión de MySQL Query Optimizer del índice. Cuando comience a acumular muchos más datos (digamos unas dos semanas), las estadísticas del índice deberían nivelarse y debería ver una mejora en el rendimiento. Simplemente no haga escaneos de índice completo.
RolandoMySQLDBA
4

Para la consulta específica:

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

un índice en (time_on, diff_ms)sería la mejor opción. Entonces, si la consulta se ejecuta con la frecuencia suficiente o su eficiencia es crucial para su aplicación, agregue este índice:

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

(No relacionado con la pregunta)
Y realmente, cambie el motor de la tabla a InnoDB. Es 2015 y el funeral de MyISAM fue hace algunos años.
(/despotricar)

ypercubeᵀᴹ
fuente
Creé el índice exacto que sugirió y luego ejecuté la consulta exacta que mencionó primero en su respuesta, pero ahora el tiempo es mucho peor, y toma alrededor de 17 segundos consistentemente (lo intenté varias veces).
Locksleyu
No tengo idea de qué lo está causando. En caso de que sea importante, solo hay 3671 valores distintos de time_on en la tabla (esto se debe a cómo mi script de prueba está completando datos).
Locksleyu
Debe hacer tres (3) cosas: 1. ejecutar ALTER TABLE writetest_table DROP INDEX time_on;, 2) ejecutar ANALYZE TABLE writetest_table;y 3) volver a ejecutar la consulta. ¿El tiempo vuelve a 7 segundos?
RolandoMySQLDBA
1
También deberías correr EXPLAIN select sum(diff_ms) from writetest_table where time_on > ("2015-07-13 15:11:56");. ¿Se está utilizando el nuevo índice? Si no se está utilizando, diría que es su población clave, especialmente si su tiempo más temprano es solo hace unos días. A medida que el número de filas aumenta con días más distintos, la distribución de claves debería nivelarse y la EXPLICACIÓN debería ser mejor .
RolandoMySQLDBA
RolandoMySQLDBA: probé tus tres pasos, y sí, el tiempo vuelve a 7 segundos. Hice la explicación y dice que se está utilizando el índice. Todavía no tengo idea de por qué agregar un índice como este podría hacer que un rendimiento superior a 2x sea tan malo.
Locksleyu