У нас есть приложение, которое хранит статьи из разных источников в таблице 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.
date
это DOUBLE
...?
EXPLAIN
?ANALYZE
это что-то другое, и, вероятно, стоит подумать, если вы этого не сделали, поскольку одно из возможных объяснений состоит в том, что искаженная статистика индекса отвлекает оптимизатор от разумного выбора. Я не думаю, что в этом вопросе есть необходимость в my.cnf, и это пространство можно было бы лучше использовать, чтобы опубликовать некоторыеEXPLAIN
результаты изменений поведения, которые вы видите ... после того, как вы исследуетеANALYZE [LOCAL] TABLE
...