Jac*_*ket 8 mysql innodb index percona
我们有一个应用程序,它将来自不同来源的文章存储在 MySQL 表中,并允许用户检索按日期排序的文章。文章总是按来源过滤,所以对于客户选择我们总是有
WHERE source_id IN (...,...) ORDER BY date DESC/ASC
Run Code Online (Sandbox Code Playgroud)
我们使用 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='';
Run Code Online (Sandbox Code Playgroud)
我们需要(日期)索引,因为有时我们在这个表上运行后台操作而不按源过滤。但是,用户不能这样做。
该表有大约 10 亿条记录(是的,我们正在考虑对未来进行分片......)。一个典型的查询如下所示:
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
Run Code Online (Sandbox Code Playgroud)
为什么要强制索引?因为事实证明,MySQL 有时会选择对此类查询使用(日期)索引(可能是因为它的长度较小?),这会导致扫描数百万条记录。如果我们在生产环境中删除 FORCE INDEX,我们的数据库服务器 CPU 内核会在几秒钟内达到最大值(这是一个 OLTP 应用程序,上面的查询以每秒 2000 次左右的速度执行)。
这种方法的问题是某些查询(我们怀疑它与 IN 子句中的 source_ids 的数量有某种关系)确实使用日期索引运行得更快。当我们对那些运行 EXPLAIN 时,我们看到 source_id_date 索引扫描了数千万条记录,而 date 索引仅扫描了数千条记录。通常情况相反,但我们找不到牢固的关系。
理想情况下,我们想找出为什么 MySQL 优化器选择错误的索引并删除 FORCE INDEX 语句,但预测何时强制使用日期索引的方法也适用于我们。
一些澄清:
出于这个问题的目的,上面的 SELECT 查询被简化了很多。它有多个 JOIN 到每个大约 1 亿行的表,加入了 PK (articles_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
Run Code Online (Sandbox Code Playgroud)
此查询仅列出特定用户 (1) 的已加星标的文章。
服务器运行 MySQL 版本 5.5.32 (Percona) 和 XtraDB。硬件为 2xE5-2620、128GB RAM、4HDDx1TB RAID10,带电池支持的控制器。有问题的 SELECT 完全受 CPU 限制。
my.cnf 如下(去掉了一些不相关的指令,如 server-id、port 等...):
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
Run Code Online (Sandbox Code Playgroud)
根据要求,以下是有问题的查询的一些解释:
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)
Run Code Online (Sandbox Code Playgroud)
实际的 SELECT 大约需要一分钟,并且完全受 CPU 限制。当我将索引更改为 (date) 时,在这种情况下,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)
Run Code Online (Sandbox Code Playgroud)
而 SELECT 只需要 10 毫秒。
但是这里的解释可能会很糟糕!例如,如果我在 IN 子句中解释一个只有一个 source_id 并在(日期)上强制索引的查询,它告诉我它将只扫描 20 行,但这是不可能的,因为该表有超过 10 亿行,而只有少数匹配此 source_id。
您可以检查innodb_stats_sample_pages参数的值。它控制 MySQL 在更新索引统计信息时对表执行的索引潜水次数,这些统计信息又用于计算候选连接计划的成本。我们使用的版本的默认值为 8。我们将其更改为 128,并且观察到意外的加入计划减少了。
| 归档时间: |
|
| 查看次数: |
27732 次 |
| 最近记录: |