简单的COUNT(*)语句很慢

背景

xxl-job 当中有一张表,数据量大概240W。 语句非常简单

SELECT count(*) FROM xxl_job_info

执行时间却要20S左右

相关表

在xxl-job 当中有一张表,如下:

CREATE TABLE `xxl_job_info` (
  `id` int NOT NULL AUTO_INCREMENT,
  `job_group` int NOT NULL COMMENT '执行器主键ID',
  `job_desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `add_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  `author` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '作者',
  `alarm_email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '报警邮件',
  `schedule_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'NONE' COMMENT '调度类型',
  `schedule_conf` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '调度配置,值含义取决于调度类型',
  `misfire_strategy` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'DO_NOTHING' COMMENT '调度过期策略',
  `executor_route_strategy` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '执行器路由策略',
  `executor_handler` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '执行器任务handler',
  `executor_param` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '执行器任务参数',
  `executor_block_strategy` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '阻塞处理策略',
  `executor_timeout` int NOT NULL DEFAULT '0' COMMENT '任务执行超时时间,单位秒',
  `executor_fail_retry_count` int NOT NULL DEFAULT '0' COMMENT '失败重试次数',
  `glue_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'GLUE类型',
  `glue_source` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'GLUE源代码',
  `glue_remark` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'GLUE备注',
  `glue_updatetime` datetime DEFAULT NULL COMMENT 'GLUE更新时间',
  `child_jobid` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '子任务ID,多个逗号分隔',
  `trigger_status` tinyint NOT NULL DEFAULT '0' COMMENT '调度状态:0-停止,1-运行',
  `trigger_last_time` bigint NOT NULL DEFAULT '0' COMMENT '上次调度时间',
  `trigger_next_time` bigint NOT NULL DEFAULT '0' COMMENT '下次调度时间',
  `trigger_no` bigint unsigned NOT NULL DEFAULT '0' COMMENT '当前任务最后的调度批次-初始化0',
  `biz_type` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'default' COMMENT '业务类型',
    KEY `tgnxtm_idx` (`trigger_status`,`trigger_next_time`,`trigger_last_time`)     USING BTREE,
    KEY `jg_idx` (`job_group`),
  KEY `add_tm_idx` (`add_time`) ,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB  DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

MySQL版本

MySQL 8.0

数据量

240W 左右

问题

如下sql 执行非常的慢,耗时22秒

SELECT count(*) FROM xxl_job_info

分析问题

首先,这个count(*) 语句非常的简单,没有任何条件,只是简单的进行一个全表统计。这样的语句,效率低,是反直觉的,为了搞清楚这个问题,于是进行了一些探索和尝试。

优化思路1:尝试 count(*) 改写 count(1) [失败]

首先能想到的就是这个古老的传说,先跑一次看看效果:

SELECT count(1) FROM xxl_job_info

执行结果如下:

可见改写成count(1)没有效果,依然很慢。 其实MySQL个官方针对这个问题进行了说明: https://dev.mysql.com/doc/refman/8.0/en/aggregate-functions.html#function_count

InnoDB handles SELECT COUNT(*) and SELECT COUNT(1) operations in the same way. There is no performance difference.

大意是:

InnoDB以相同的方式处理SELECT COUNT(*)和SELECT COUNT(1)操作。没有性能差异。

执行 EXPLAIN 查看执行计划

执行:

EXPLAIN SELECT count(*) FROM xxl_job_info

结果:

在执行计划中,这里使用了jg_idx这个索引。同时,在MySQL官方文档中,也能查找到相应的文档:
https://dev.mysql.com/doc/refman/8.0/en/aggregate-functions.html#function_count

InnoDB processes SELECT COUNT() statements by traversing the smallest available secondary index unless an index or optimizer hint directs the optimizer to use a different index. If a secondary index is not present, InnoDB processes SELECT COUNT() statements by scanning the clustered index.

大意是:

InnoDB通过遍历最小的可用辅助索引(二级索引)来处理SELECT COUNT()语句,除非索引或优化器提示指示优化器使用其他索引。如果不存在辅助索引(二级索引),InnoDB将通过扫描聚集索引来处理SELECT COUNT()语句。

然而,即使使用了二级索引,在200W左右的数据量下,耗时20S也是不应该的。 于是,想起MsSQL精创选错索引,我们会用 一些WHERE条件语句进行引导。虽然这里执行计划显示是使用了jd_idx这个索引,但是感觉实际上内部执行有一些问题。

优化思路2:加引导条件

于是进行了SQL的改写:

SELECT count(*) FROM xxl_job_info where job_group >= 0 

改写后的语句,只是在 WHERE条件后面加上了 job_group >= 0 这个条件,实际上 job_group 只有0和1这两个值,所以其实按理说加与不加,结果是相同的。这里的作用只是引导MySQL走这个 jd_idx索引。 执行结果如下:

可以看到,这里的执行效率高了几十倍,最终只用了0.6秒就完成了全表的统计。

为什么会导致这样的结果

查询了很多资料,包括MySQL官方资料,以及网络上的已有资源。都没有找到相关的解释。这里只是猜想:

  • 是否是MySQL内部执行逻辑的问题,导致与执行计划不匹配。
  • 是否是实际统计走了 聚簇索引,导致统计效率低下。
  • 是否是 job_goup >0 这个条件,让MySQL在缓存的读策略上有所不同,导致执行效率大大提高。
    这里只做分享,如果遇到类似问题,能通过这种手段进行优化。如果有大神了解其内部原理,望赐教!

暂无评论

发送评论 编辑评论


|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇