LIMIT 1非常缓慢,针对特定记录,使用不同的键

19

我正在诊断一个间歇性的慢查询,并发现MySQL中有一种奇怪的行为,我无法解释。只有在执行LIMIT 1时,它才会选择不同的、非最优的键策略,而且只针对一个特定的情况。

表格(为了简洁起见,删除了一些未引用的数据列)

CREATE TABLE `ch_log` (
    `cl_id` BIGINT(20) NOT NULL AUTO_INCREMENT,
    `cl_unit_id` INT(11) NOT NULL DEFAULT '0',
    `cl_date` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
    `cl_type` CHAR(1) NOT NULL DEFAULT '',
    `cl_data` TEXT NOT NULL,
    `cl_event` VARCHAR(255) NULL DEFAULT NULL,
    `cl_timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    `cl_record_status` CHAR(1) NOT NULL DEFAULT 'a',
    PRIMARY KEY (`cl_id`),
    INDEX `cl_type` (`cl_type`),
    INDEX `cl_date` (`cl_date`),
    INDEX `cl_event` (`cl_event`),
    INDEX `cl_unit_id` (`cl_unit_id`),
    INDEX `log_type_unit_id` (`cl_unit_id`, `cl_type`),
    INDEX `unique_user` (`cl_user_number`, `cl_unit_id`)
)
ENGINE=InnoDB
AUTO_INCREMENT=419582094;

这是查询语句,只有一个特定的 cl_unit_id 会运行缓慢:

EXPLAIN
SELECT *
FROM `ch_log`
WHERE `ch_log_type` ='I' and ch_log_event = 'G'  
AND cl_unit_id=1234
ORDER BY cl_date DESC 
LIMIT 1;
id|select_type|table |type |possible_keys                               |key    |key_len|ref|rows|Extra
1 |SIMPLE     |ch_log|index|cl_type,cl_event,cl_unit_id,log_type_unit_id|cl_date|8      |\N |5295|Using where

对于cl_unit_id的所有其他值,它使用log_type_unit_id键,这样速度更快。

id|select_type|table |type|possible_keys                                           |key             |key_len|ref        |rows|Extra
1 |SIMPLE     |ch_log|ref |ch_log_type,ch_log_event,ch_log_unit_id,log_type_unit_id|log_type_unit_id|5      |const,const|3804|Using where; Using filesort
  • 所有查询都只需要约0.01秒。
  • “慢单元”查询需要10-15分钟!

我看不出这个“单元”的数据有什么奇怪的地方:

  • 1234单元只有6条类型为I和事件G的记录。
  • 其他单元有更多。
  • 1234单元总共只有32,000条日志,这是典型的。
  • 数据本身是正常的,没有更大或更旧的。
  • 数据库中大约有3,000个“单元”,它们代表记录东西的设备。cl_unit_id是它们的唯一PK(虽然没有约束)。

一般信息

  • 总计有30m条记录,大约12GB
  • mysql 5.1.69-log
  • Centos 64bit
  • 数据逐渐变化(30m = 3个月的日志),但我不知道这以前是否发生过

我尝试过的解决方法:

  1. 去掉LIMIT 1 - 查询在毫秒级别运行并返回数据。

  2. 改成LIMIT 2或其他组合,如2,3 - 在毫秒内运行。

  3. 添加索引提示-解决了问题:

    FROM `ch_log` USE INDEX (log_type_unit_id)
    

    但是...我不想把这个硬编码到应用程序中。

  4. 在主键上添加第二个排序也可以“解决”它:

  5. ORDER BY cl_id, cl_date DESC 
    

    解释如下:

    id|select_type|table |type|possible_keys                                           |key             |key_len|ref        |rows|Extra
    1 |SIMPLE     |ch_log|ref |ch_log_type,ch_log_event,ch_log_unit_id,log_type_unit_id|log_type_unit_id|5      |const,const|6870|Using where
    

    这个类型提示略有不同,检查了更多记录(6,000),但仍在10毫秒内运行。

虽然我可以这样做,但我不喜欢使用我不理解的副作用。

所以我认为我的主要问题是:

a)为什么只发生在LIMIT 1

b)数据本身如何如此影响键策略?而数据的哪个方面,考虑到索引的数量和分布似乎很典型。


这是一个很好的问题。你可以在http://dba.stackexchange.com/上发布吗? - zedfoxus
为什么你同时拥有这两个:INDEX cl_unit_id (cl_unit_id),INDEX log_type_unit_id (cl_unit_id, cl_type).... 为什么不只保留第二个? - Drew
1
所以我会考虑这个(不知道你的查询路径)。放弃一个真正没有用的索引,并在(uid,date)上创建一个组合索引。对我来说很容易。也很容易分析表并在该新组合上强制使用索引。无论限制1如何,order by都会使其掉落一个蛋。 - Drew
1
感谢@DrewPierce,是的,我确实计划有一天进行关键字清理工作,但在生产数据库上这样做非常困难,而且还会涉及到交换从库。而且要扫描数十万行遗留代码以查找所有键和查询的组合也很困难。我上次进行的关键字更改需要停机8个小时! - scipilot
这不是你所问的,所以我只会评论一下:这可能会避免所有情况下的缓慢:INDEX(cl_unit_id, ch_log_type, ch_log_event, cl_date)。(cl_date必须放在最后;其他可以任意排序。) - Rick James
显示剩余7条评论
1个回答

10
Mysql会选择一个最佳的解释计划并使用不同的索引,具体取决于它认为哪个是统计上最好的选择。对于你的第一个问题,答案如下:
  1. 去掉LIMIT 1 - 查询在毫秒级别运行并返回数据。并且 -> 是的,请检查一下,解释计划很好
  2. 更改为LIMIT 2或其他组合,例如2,3 - 在毫秒级别运行。 -> 同样适用。优化器选择了不同的索引,因为突然之间,预期的块读取量比 LIMIT 1 大了一倍(这只是其中一种可能性)
  3. 添加索引提示解决了这个问题 -> 当然,你强制使用了一个好的解释计划
  4. 在主键上添加第二个排序也“解决”了它 -> 是的,因为碰巧结果是一个更好的解释计划

现在,这只回答了问题的一半。

a) 为什么只对LIMIT 1有效?

实际上,这不仅仅是因为LIMIT 1,而是因为

  • 您的数据统计重分配(指导优化器的决策)
  • 您的ORDER BY DESC子句。尝试使用ORDER BY ... ASC,您可能也会看到改进。

这种现象是完全被认可的。请阅读

其中一种被接受的解决方案(文章底部)是强制索引,就像您所做的那样。是的,有时候是有道理的。否则,这个提示的东西早就被彻底清除了。机器人并不总是完美的 :-)

b)数据本身如何如此影响关键策略?以及数据的哪个方面,考虑到索引中的数量和分布似乎很典型。

你说的没错,数据扩散通常是问题所在。优化器不仅可能根据准确的统计数据做出错误的决策,而且还可能完全偏离正轨,因为表上的增量 小于总行数的1/16...

2
请运行ANALYZE语句(这是由show table语句触发的)。但您应该调查此时统计信息是否过时。看起来你一路上都很不走运,因为在日志内容更改了1/16后,它应该自动发生。 - Sebas
@scipilot - 5.7版本(以及后续的5.6版本?)重新设计了执行“ANALYZE”操作时的情况。结果几乎总是兼顾两者的优点——正确地记住统计信息,同时又不必执行“ANALYZE”以期修复错误的统计信息。 - Rick James
谢谢@RickJames,这很有趣。我倾向于坚持使用操作系统仓库版本,但它们总是落后于最新版本!所以我会查看最新可用的版本。 - scipilot
顺便提一下,自发布此问题以来,该问题已在生产环境中再次发生两次。这个“修复”方法可行,但对我们而言并不罕见。 我们每天获得约30万条新日志,总共有3000万条,因此1/16大约是5-6天,这正是我们看到的。服务器每个周末都会死机!(让我跳起来应急) - scipilot
@scipilot 然后尽情地使用cron分析作业 :D - Sebas
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接