Temos um aplicativo que armazena artigos de diferentes fontes em uma tabela MySQL e permite que os usuários recuperem os artigos pedidos por data. Os artigos são sempre filtrados por fonte, portanto, para os SELECTs do cliente, sempre temos
WHERE source_id IN (...,...) ORDER BY date DESC/ASC
Estamos usando IN, porque os usuários têm muitas assinaturas (alguns têm milhares).
Aqui está o esquema da tabela de artigos:
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='';
Precisamos do índice (date), porque às vezes estamos executando operações em segundo plano nesta tabela sem filtrar por origem. Os usuários, no entanto, não podem fazer isso.
A tabela possui cerca de 1 bilhão de registros (sim, estamos considerando sharding para o futuro ...). Uma consulta típica é assim:
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
Porquê FORCE INDEX? Como se descobriu, o MySQL às vezes opta por usar o índice (date) para essas consultas (talvez por causa de seu tamanho menor?) E isso resulta em varreduras de milhões de registros. Se removermos o FORCE INDEX em produção, nossos núcleos de CPU do servidor de banco de dados serão maximizados em segundos (é um aplicativo OLTP e consultas como a acima são executadas a taxas em torno de 2000 por segundo).
O problema dessa abordagem é que algumas consultas (suspeitamos que estejam relacionadas ao número de source_ids na cláusula IN) são realmente mais rápidas com o índice de datas. Quando executamos EXPLAIN naqueles, vemos que o índice source_id_date verifica dezenas de milhões de registros, enquanto o índice de data verifica apenas alguns milhares. Geralmente é o contrário, mas não conseguimos encontrar uma relação sólida.
Idealmente, queríamos descobrir por que o otimizador do MySQL escolhe o índice errado e remove a instrução FORCE INDEX, mas uma maneira de prever quando forçar o índice de datas também funcionará para nós.
Alguns esclarecimentos:
A consulta SELECT acima é muito simplificada para os fins desta pergunta. Ele possui vários JOINs em tabelas com cerca de 100 milhões de linhas cada, unidas ao PK (articles_user_flags.id = article.id), o que agrava o problema quando há milhões de linhas para classificar. Além disso, algumas consultas têm mais locais, por exemplo:
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 lista apenas artigos com estrela para o usuário específico (1).
O servidor está executando o MySQL versão 5.5.32 (Percona) com o XtraDB. O hardware é 2xE5-2620, 128GB RAM, 4HDDx1TB RAID10 com controlador suportado por bateria. Os SELECTs problemáticos são completamente vinculados à CPU.
my.cnf é o seguinte (foram removidas algumas diretivas não relacionadas, como identificação do servidor, porta, 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
Conforme solicitado, aqui estão alguns EXPLAINs das 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)
O SELECT real leva cerca de um minuto e é completamente vinculado à CPU. Quando altero o índice para (data), que neste caso o otimizador do MySQL também escolhe automaticamente:
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)
E o SELECT leva apenas 10ms.
Mas EXPLAINs podem ser muito quebrados aqui! Por exemplo, se EXPLICAR uma consulta com apenas um source_id na cláusula IN e forçar o índice em (date), ele informa que analisará apenas 20 linhas, mas isso não é possível, porque a tabela possui mais de 1 bilhão de linhas e apenas algumas corresponda a este source_id.
EXPLAIN
?ANALYZE
é algo diferente e provavelmente é algo a considerar, se você não tiver, pois uma explicação possível é que as estatísticas distorcidas do índice estão distraindo o otimizador de escolher sabiamente. Acho que não há necessidade do my.cnf na pergunta, e esse espaço pode ser melhor usado para postar algumaEXPLAIN
saída das variações de comportamento que você vê ... depois de investigarANALYZE [LOCAL] TABLE
...date
é umDOUBLE
...?Respostas:
Você pode verificar seu valor para o parâmetro innodb_stats_sample_pages . Ele controla quantos mergulhos de índice o MySQL executa em uma tabela ao atualizar as estatísticas do índice, que por sua vez são usadas para calcular o custo de um plano de ingresso de candidato. O valor padrão era 8 para a versão que estávamos usando. Alteramos para 128 e observamos menos planos de junção inesperados.
fonte