MySQL требует FORCE INDEX для огромной таблицы и простых SELECT

8

У нас есть приложение, которое хранит статьи из разных источников в таблице MySQL и позволяет пользователям получать эти статьи, упорядоченные по дате. Статьи всегда фильтруются по источнику, поэтому для клиентских SELECT у нас всегда есть

WHERE source_id IN (...,...) ORDER BY date DESC/ASC

Мы используем IN, потому что у пользователей много подписок (у некоторых тысячи).

Вот схема таблицы статей:

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='';

Нам нужен индекс (date), потому что иногда мы выполняем фоновые операции над этой таблицей без фильтрации по источнику. Однако пользователи не могут этого сделать.

Таблица содержит около 1 миллиарда записей (да, мы рассматриваем шардинг на будущее ...). Типичный запрос выглядит так:

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

Почему FORCE INDEX? Потому что оказалось, что MySQL иногда предпочитает использовать индекс (date) для таких запросов (возможно, из-за его меньшей длины?), И это приводит к сканированию миллионов записей. Если мы уберем FORCE INDEX в производственной среде, процессорные ядра нашего сервера базы данных будут максимально загружены за считанные секунды (это OLTP-приложения, и запросы, подобные приведенным выше, выполняются со скоростью около 2000 в секунду).

Проблема этого подхода заключается в том, что некоторые запросы (мы подозреваем, что они каким-то образом связаны с числом source_ids в предложении IN) действительно выполняются быстрее с индексом даты. Когда мы запускаем EXPLAIN для них, мы видим, что индекс source_id_date сканирует десятки миллионов записей, в то время как индекс даты сканирует только несколько тысяч. Обычно все наоборот, но мы не можем найти прочных отношений.

В идеале мы хотели выяснить, почему оптимизатор MySQL выбирает неправильный индекс, и удалить оператор FORCE INDEX, но способ предсказать, когда форсировать индекс даты, также будет работать для нас.

Некоторые уточнения:

Вышеуказанный запрос SELECT значительно упрощен для целей этого вопроса. Он имеет несколько соединений с таблицами, насчитывающими около 100 миллионов строк в каждой, которые присоединяются к PK (article_user_flags.id = article.id), что усугубляет проблему, когда есть миллионы строк для сортировки. Также некоторые запросы имеют дополнительные где, например:

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

В этом запросе перечислены только избранные статьи для конкретного пользователя (1).

Сервер работает под управлением MySQL версии 5.5.32 (Percona) с XtraDB. Аппаратное обеспечение - 2xE5-2620, 128 ГБ ОЗУ, 4HDDx1TB RAID10 с контроллером с батарейным питанием. Проблемные SELECT полностью связаны с процессором.

my.cnf выглядит следующим образом (удалены некоторые несвязанные директивы, такие как идентификатор сервера, порт и т. д.):

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

В соответствии с просьбой, вот некоторые объяснения проблемных запросов:

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)

Фактический SELECT занимает около одной минуты и полностью связан с процессором. Когда я изменяю индекс на (дата), который в этом случае оптимизатор MySQL также выбирает автоматически:

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)

И SELECT занимает всего 10 мс.

Но ОБЪЯСНЕНИЯ могут быть сильно нарушены! Например, если я ОБЪЯСНИВАЮ запрос с одним исходным идентификатором в предложении IN и принудительным индексом on (date), он сообщает, что будет сканировать только 20 строк, но это невозможно, поскольку в таблице более 1 миллиарда строк и всего несколько сопоставьте этот source_id.

Куртка
источник
«Когда мы разберемся с этими ...» Вы имеете в виду EXPLAIN? ANALYZEэто что-то другое, и, вероятно, стоит подумать, если вы этого не сделали, поскольку одно из возможных объяснений состоит в том, что искаженная статистика индекса отвлекает оптимизатор от разумного выбора. Я не думаю, что в этом вопросе есть необходимость в my.cnf, и это пространство можно было бы лучше использовать, чтобы опубликовать некоторые EXPLAINрезультаты изменений поведения, которые вы видите ... после того, как вы исследуете ANALYZE [LOCAL] TABLE...
Майкл - sqlbot
Да, это была опечатка, спасибо за исправление. Я исправил это. Конечно, мы сделали АНАЛИЗ, но это совсем не помогло. Я постараюсь захватить некоторые ОБЪЯСНЕНИЯ позже.
Куртка
И dateэто DOUBLE...?
ypercubeᵀᴹ
Да, потому что здесь нужна микросекундная точность. Скорость вставки в эту таблицу составляет около 400 000 записей в час, и нам нужно, чтобы даты были максимально уникальными.
Куртка
@Jacket Можете ли вы опубликовать EXPLAIN от оскорбительного запроса? я думаю, потому что это связано с ЦП, ваш сервер выполняет быструю сортировку («использует файловую сортировку в объяснении)».
Рэймонд Нейланд

Ответы:

4

Вы можете проверить свое значение для параметра innodb_stats_sample_pages . Он контролирует, сколько индексных погружений MySQL выполняет для таблицы при обновлении статистики индекса, которая, в свою очередь, используется для расчета стоимости плана соединения-кандидата. Значение по умолчанию было 8 для версии, которую мы использовали. Мы изменили его на 128 и заметили меньше неожиданных планов присоединения.

Эрик Рат
источник