Tenemos una aplicación que almacena artículos de diferentes fuentes en una tabla MySQL y permite a los usuarios recuperar los artículos ordenados por fecha. Los artículos siempre se filtran por fuente, por lo que para los clientes SELECT siempre tenemos
WHERE source_id IN (...,...) ORDER BY date DESC/ASC
Estamos utilizando IN, porque los usuarios tienen muchas suscripciones (algunas tienen miles).
Aquí está el esquema de la tabla de artículos:
CREATE TABLE `articles` (
`id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
`source_id` INTEGER(11) UNSIGNED NOT NULL,
`date` DOUBLE(16,6) NOT NULL,
PRIMARY KEY (`id`),
KEY `source_id_date` (`source_id`, `date`),
KEY `date` (`date`)
)ENGINE=InnoDB
AUTO_INCREMENT=1
CHARACTER SET 'utf8' COLLATE 'utf8_general_ci'
COMMENT='';
Necesitamos el índice (fecha), porque a veces estamos ejecutando operaciones en segundo plano en esta tabla sin filtrar por fuente. Sin embargo, los usuarios no pueden hacer esto.
La tabla tiene alrededor de mil millones de registros (sí, estamos considerando fragmentar para el futuro ...). Una consulta típica se ve así:
SELECT a.id, a.date, s.name
FROM articles a FORCE INDEX (source_id_date)
JOIN sources s ON s.id = a.source_id
WHERE a.source_id IN (1,2,3,...)
ORDER BY a.date DESC
LIMIT 10
¿Por qué FORCE INDEX? Debido a que resultó que MySQL a veces elige usar el índice (fecha) para tales consultas (¿tal vez debido a su menor longitud?) Y esto resulta en escaneos de millones de registros. Si eliminamos el ÍNDICE DE FUERZA en producción, nuestros núcleos de CPU del servidor de base de datos se maximizan en segundos (es una aplicación OLTP y consultas como las anteriores se ejecutan a velocidades de alrededor de 2000 por segundo).
El problema con este enfoque es que algunas consultas (sospechamos que de alguna manera están relacionadas con el número de source_ids en la cláusula IN) realmente se ejecutan más rápido con el índice de fecha. Cuando ejecutamos EXPLAIN en esos, vemos que el índice source_id_date escanea decenas de millones de registros, mientras que el índice de fecha escanea solo algunos miles. Por lo general, es al revés, pero no podemos encontrar una relación sólida.
Idealmente, queríamos averiguar por qué el optimizador de MySQL elige el índice incorrecto y eliminar la instrucción FORCE INDEX, pero una forma de predecir cuándo forzar el índice de fecha también funcionará para nosotros.
Algunas aclaraciones:
La consulta SELECT anterior se simplifica mucho a los fines de esta pregunta. Tiene varias uniones a tablas con alrededor de 100 millones de filas cada una, unidas a la PK (articles_user_flags.id = article.id), lo que agrava el problema cuando hay millones de filas para ordenar. También algunas consultas tienen dónde adicionales, por ejemplo:
SELECT a.id, a.date, s.name
FROM articles a FORCE INDEX (source_id_date)
JOIN sources s ON s.id = a.source_id
LEFT JOIN articles_user_flags auf ON auf.article_id=a.id AND auf.user_id=1
WHERE a.source_id IN (1,2,3,...)
AND auf.starred=1
ORDER BY a.date DESC
LIMIT 10
Esta consulta enumera solo los artículos destacados para el usuario en particular (1).
El servidor ejecuta MySQL versión 5.5.32 (Percona) con XtraDB. El hardware es 2xE5-2620, 128 GB de RAM, 4HDDx1TB RAID10 con controlador respaldado por batería. Los SELECT problemáticos están completamente vinculados a la CPU.
my.cnf es el siguiente (eliminó algunas directivas no relacionadas, como server-id, puerto, etc.):
transaction-isolation = READ-COMMITTED
binlog_cache_size = 256K
max_connections = 2500
max_user_connections = 2000
back_log = 2048
thread_concurrency = 12
max_allowed_packet = 32M
sort_buffer_size = 256K
read_buffer_size = 128K
read_rnd_buffer_size = 256K
join_buffer_size = 8M
myisam_sort_buffer_size = 8M
query_cache_limit = 1M
query_cache_size = 0
query_cache_type = 0
key_buffer = 10M
table_cache = 10000
thread_stack = 256K
thread_cache_size = 100
tmp_table_size = 256M
max_heap_table_size = 4G
query_cache_min_res_unit = 1K
slow-query-log = 1
slow-query-log-file = /mysql_database/log/mysql-slow.log
long_query_time = 1
general_log = 0
general_log_file = /mysql_database/log/mysql-general.log
log_error = /mysql_database/log/mysql.log
character-set-server = utf8
innodb_flush_method = O_DIRECT
innodb_flush_log_at_trx_commit = 2
innodb_buffer_pool_size = 105G
innodb_buffer_pool_instances = 32
innodb_log_file_size = 1G
innodb_log_buffer_size = 16M
innodb_thread_concurrency = 25
innodb_file_per_table = 1
#percona specific
innodb_buffer_pool_restore_at_startup = 60
Según lo solicitado, aquí hay algunas EXPLICACIONES de las consultas problemáticas:
mysql> EXPLAIN SELECT a.id,a.date AS date_double
-> FROM articles a
-> FORCE INDEX (source_id_date)
-> JOIN sources s ON s.id = a.source_id WHERE
-> a.source_id IN (...) --Around 1000 IDs
-> ORDER BY a.date LIMIT 20;
+----+-------------+-------+--------+-----------------+----------------+---------+---------------------------+----------+------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+--------+-----------------+----------------+---------+---------------------------+----------+------------------------------------------+
| 1 | SIMPLE | a | range | source_id_date | source_id_date | 4 | NULL | 13744277 | Using where; Using index; Using filesort |
| 1 | SIMPLE | s | eq_ref | PRIMARY | PRIMARY | 4 | articles_db.a.source_id | 1 | Using where; Using index |
+----+-------------+-------+--------+-----------------+----------------+---------+---------------------------+----------+------------------------------------------+
2 rows in set (0.01 sec)
El SELECT real toma alrededor de un minuto y está completamente vinculado a la CPU. Cuando cambio el índice a (fecha) que en este caso el optimizador MySQL también elige automáticamente:
mysql> EXPLAIN SELECT a.id,a.date AS date_double
-> FROM articles a
-> FORCE INDEX (date)
-> JOIN sources s ON s.id = a.source_id WHERE
-> a.source_id IN (...) --Around 1000 IDs
-> ORDER BY a.date LIMIT 20;
+----+-------------+-------+--------+---------------+---------+---------+---------------------------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+--------+---------------+---------+---------+---------------------------+------+--------------------------+
| 1 | SIMPLE | a | index | NULL | date | 8 | NULL | 20 | Using where |
| 1 | SIMPLE | s | eq_ref | PRIMARY | PRIMARY | 4 | articles_db.a.source_id | 1 | Using where; Using index |
+----+-------------+-------+--------+---------------+---------+---------+---------------------------+------+--------------------------+
2 rows in set (0.01 sec)
Y el SELECT toma solo 10 ms.
¡Pero las EXPLICACIONES pueden estar muy rotas aquí! Por ejemplo, si EXPLICO una consulta con solo un source_id en la cláusula IN y el índice forzado el (fecha), me dice que escaneará solo 20 filas, pero eso no es posible, porque la tabla tiene más de 1 mil millones de filas y solo unas pocas coincide con este source_id.
date
es un DOUBLE
...?
EXPLAIN
?ANALYZE
es algo diferente y probablemente sea algo a considerar si no lo ha hecho, ya que una posible explicación es que las estadísticas de índice sesgadas están distrayendo al optimizador de elegir sabiamente. No creo que haya ninguna necesidad de my.cnf en la pregunta, y ese espacio podría usarse mejor para publicar algunaEXPLAIN
salida de las variaciones en el comportamiento que ves ... después de investigarANALYZE [LOCAL] TABLE
...