O MySQL requer o FORCE INDEX em uma mesa enorme e em SELECTs simples

8

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.

Jaqueta
fonte
"Quando executamos a análise sobre ..." Você quer dizer 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 alguma EXPLAINsaída das variações de comportamento que você vê ... depois de investigar ANALYZE [LOCAL] TABLE...
Michael - sqlbot
Sim, foi um erro de digitação, obrigado pela correção. Eu consertei isso. É claro que fizemos ANALYZE, mas isso não ajudou em nada. Tentarei capturar alguns EXPLAINs mais tarde.
Jacket
E dateé um DOUBLE...?
ypercubeᵀᴹ
Sim, porque precisamos de precisão de microssegundos aqui. A taxa de inserção nesta tabela é de cerca de 400.000 entradas por hora e precisamos que as datas sejam o mais únicas possível.
Jacket
@Jacket Você pode postar um EXPLAIN fora da consulta incorreta? Acho que é porque ele é vinculado à CPU o servidor está quicksorting ( "usando filesort em explicar)" conjunto de resultados ..
Raymond Nijland

Respostas:

4

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.

Eric Rath
fonte