SQL优化之SELECT COUNT(*)
前言
SQL优化之SQL 进阶技巧(上) SQL优化之SQL 进阶技巧(下)中提到使用以下 sql 会导致慢查询
SELECT COUNT(*) FROM SomeTable
SELECT COUNT(1) FROM SomeTable
原因是会造成全表扫描,有位读者说这种说法是有问题的,实际上针对无 where_clause 的 COUNT(*),MySQL 是有优化的,优化器会选择成本最小的辅助索引查询计数,其实反而性能最高,这位读者的说法对不对呢
针对这个疑问,我首先去生产上找了一个千万级别的表使用 EXPLAIN 来查询了一下执行计划
EXPLAIN SELECT COUNT(*) FROM SomeTable
结果如下

如图所示: 发现确实此条语句在此例中用到的并不是主键索引,而是辅助索引,实际上在此例中我试验了,不管是 COUNT(1),还是 COUNT(*),MySQL 都会用成本最小的辅助索引查询方式来计数,也就是使用 COUNT(*) 由于 MySQL 的优化已经保证了它的查询性能是最好的!随带提一句,COUNT(*)是 SQL92 定义的标准统计行数的语法,并且效率高,所以请直接使用COUNT(*)查询表的行数!
所以这位读者的说法确实是对的。但有个前提,在 MySQL 5.6 之后的版本中才有这种优化。
那么这个成本最小该怎么定义呢,有时候在 WHERE 中指定了多个条件,为啥最终 MySQL 执行的时候却选择了另一个索引,甚至不选索引?
本文将会给你答案,本文将会从以下两方面来分析
SQL 选用索引的执行成本如何计算 实例说明
SQL 选用索引的执行成本如何计算
就如前文所述,在有多个索引的情况下, 在查询数据前,MySQL 会选择成本最小原则来选择使用对应的索引,这里的成本主要包含两个方面。
IO 成本: 即从磁盘把数据加载到内存的成本,默认情况下,读取数据页的 IO 成本是 1,MySQL 是以页的形式读取数据的,即当用到某个数据时,并不会只读取这个数据,而会把这个数据相邻的数据也一起读到内存中,这就是有名的程序局部性原理,所以 MySQL 每次会读取一整页,一页的成本就是 1。所以 IO 的成本主要和页的大小有关 CPU 成本:将数据读入内存后,还要检测数据是否满足条件和排序等 CPU 操作的成本,显然它与行数有关,默认情况下,检测记录的成本是 0.2。
实例说明
为了根据以上两个成本来算出使用索引的最终成本,我们先准备一个表(以下操作基于 MySQL 5.7.18)
CREATE TABLEperson(
idbigint(20) NOT NULL AUTO_INCREMENT,
namevarchar(255) NOT NULL,
scoreint(11) NOT NULL,
create_timetimestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEYname_score(name(191),score),
KEYcreate_time(create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
这个表除了主键索引之外,还有另外两个索引, name_score 及 create_time。然后我们在此表中插入 10 w 行数据,只要写一个存储过程调用即可,如下:
CREATE PROCEDURE insert_person()
begin
declare c_id integer default 1;
while c_id<=100000 do
insert into person values(c_id, concat('name',c_id), c_id+100, date_sub(NOW(), interval c_id second));
set c_id=c_id+1;
end while;
end
插入之后我们现在使用 EXPLAIN 来计算下统计总行数到底使用的是哪个索引
EXPLAIN SELECT COUNT(*) FROM person

从结果上看它选择了 create_time 辅助索引,显然 MySQL 认为使用此索引进行查询成本最小,这也是符合我们的预期,使用辅助索引来查询确实是性能最高的!
我们再来看以下 SQL 会使用哪个索引
SELECT * FROM person WHERE NAME >'name84059' AND create_time>'2020-05-23 14:39:18'

用了全表扫描!理论上应该用 name_score 或者 create_time 索引才对,从 WHERE 的查询条件来看确实都能命中索引,那是否是使用 SELECT * 造成的回表代价太大所致呢,我们改成覆盖索引的形式试一下
SELECT create_time FROM person WHERE NAME >'name84059' AND create_time > '2020-05-23 14:39:18'
结果 MySQL 依然选择了全表扫描!这就比较有意思了,理论上采用了覆盖索引的方式进行查找性能肯定是比全表扫描更好的,为啥 MySQL 选择了全表扫描呢,既然它认为全表扫描比使用覆盖索引的形式性能更好,那我们分别用这两者执行来比较下查询时间吧
-- 全表扫描执行时间: 4.0 ms
SELECT create_time FROM person WHERE NAME >'name84059' AND create_time>'2020-05-23 14:39:18'-- 使用覆盖索引执行时间: 2.0 ms
SELECT create_time FROM person force index(create_time) WHERE NAME >'name84059' AND create_time>'2020-05-23 14:39:18'
从实际执行的效果看使用覆盖索引查询比使用全表扫描执行的时间快了一倍!说明 MySQL 在查询前做的成本估算不准!我们先来看看 MySQL 做全表扫描的成本有多少。
前面我们说了成本主要 IO 成本和 CPU 成本有关,对于全表扫描来说也就是分别和聚簇索引占用的页面数和表中的记录数。执行以下命令
SHOW TABLE STATUS LIKE 'person'

可以发现
行数是 100264,我们不是插入了 10 w 行的数据了吗,怎么算出的数据反而多了,其实这里的计算是估算,也有可能这里的行数统计出来比 10 w 少了,估算方式有兴趣大家去网上查找,这里不是本文重点,就不展开了。得知行数,那我们知道 CPU 成本是 100264 * 0.2 = 20052.8。
数据长度是 5783552,InnoDB 每个页面的大小是 16 KB,可以算出页面数量是 353。
也就是说全表扫描的成本是 20052.8 + 353 = 20406。
这个结果对不对呢,我们可以用一个工具验证一下。在 MySQL 5.6 及之后的版本中,我们可以用 optimizer trace 功能来查看优化器生成计划的整个过程 ,它列出了选择每个索引的执行计划成本以及最终的选择结果,我们可以依赖这些信息来进一步优化我们的 SQL。
optimizer_trace 功能使用如下
SET optimizer_trace="enabled=on";
SELECT create_time FROM person WHERE NAME >'name84059' AND create_time > '2020-05-23 14:39:18';
SELECT * FROM information_schema.OPTIMIZER_TRACE;
SET optimizer_trace="enabled=off";
执行之后我们主要观察使用 name_score,create_time 索引及全表扫描的成本。
先来看下使用 name_score 索引执行的的预估执行成本:
{
"index": "name_score",
"ranges": [
"name84059 <= name"
],
"index_dives_for_eq_ranges": true,
"rows": 25372,
"cost": 30447
}
可以看到执行成本为 30447,高于我们之前算出来的全表扫描成本:20406。所以没选择此索引执行
注意:这里的 30447 是查询二级索引的 IO 成本和 CPU 成本之和,再加上回表查询聚簇索引的 IO 成本和 CPU 成本之和。
再来看下使用 create_time 索引执行的的预估执行成本:
{
"index": "create_time",
"ranges": [
"0x5ec8c516 < create_time"
],
"index_dives_for_eq_ranges": true,
"rows": 50132,
"cost": 60159,
"cause": "cost"
}
可以看到成本是 60159,远大于全表扫描成本 20406,自然也没选择此索引。
再来看计算出的全表扫描成本:
{
"considered_execution_plans": [
{
"plan_prefix": [
],
"table": "`person`",
"best_access_path": {
"considered_access_paths": [
{
"rows_to_scan": 100264,
"access_type": "scan",
"resulting_rows": 100264,
"cost": 20406,
"chosen": true
}
]
},
"condition_filtering_pct": 100,
"rows_for_plan": 100264,
"cost_for_plan": 20406,
"chosen": true
}
]
}
注意看 cost:20406,与我们之前算出来的完全一样!这个值在以上三者算出的执行成本中最小,所以最终 MySQL 选择了用全表扫描的方式来执行此 SQL。
实际上 optimizer trace 详细列出了覆盖索引,回表的成本统计情况,有兴趣的可以去研究一下。
从以上分析可以看出, MySQL 选择的执行计划未必是最佳的,原因有挺多,就比如上文说的行数统计信息不准,再比如 MySQL 认为的最优跟我们认为不一样,我们可以认为执行时间短的是最优的,但 MySQL 认为的成本小未必意味着执行时间短。
总结
本文通过一个例子深入剖析了 MySQL 的执行计划是如何选择的,以及为什么它的选择未必是我们认为的最优的,这也提醒我们,在生产中如果有多个索引的情况,使用 WHERE 进行过滤未必会选中你认为的索引,我们可以提前使用 EXPLAIN, optimizer trace 来优化我们的查询语句。
相关文章
SQL优化之SELECT COUNT(*)的更多相关文章
- SQL优化之SQL 进阶技巧(下)
上文( SQL优化之SQL 进阶技巧(上) )我们简述了 SQL 的一些进阶技巧,一些朋友觉得不过瘾,我们继续来下篇,再送你 10 个技巧 一. 使用延迟查询优化 limit [offset], [r ...
- SQL优化之SQL 进阶技巧(上)
由于工作需要,最近做了很多 BI 取数的工作,需要用到一些比较高级的 SQL 技巧,总结了一下工作中用到的一些比较骚的进阶技巧,特此记录一下,以方便自己查阅,主要目录如下: SQL 的书写规范 SQL ...
- 【数据库】SQL优化方法汇总
最近在研究SQL语句的优化问题. 下面是从网上搜集的,有的地方有点老了,可是还是有很多可以借鉴的地方的. 如何加快查询速度? 1.升级硬件. 2.根据查询条件,建立索引,优化索引.优化访问方式,限制结 ...
- mysql 开发进阶篇系列 4 SQL 优化(各种优化方法点)
1 通过handler_read 查看索引使用情况 如果索引经常被用到 那么handler_read_key的值将很高,这个值代表了一个行被索引值读的次数, 很低的值表明增加索引得到的性能改善不高,索 ...
- 基于Oracle的SQL优化(崔华著)-整理笔记-第2章“Oracle里的执行计划”
详细介绍了Oracle数据里与执行计划有关的各个方面的内容,包括执行计划的含义,加何查看执行计划,如何得到目标SQL真实的执行计划,如何查看执行计划的执行顺序,Oracle数据库里各种常见的执行计划的 ...
- SQL优化例子
如下SQL的优化: select count(*) from ( select id,name,col1,col2 from t1 where name='xxxx' union select id ...
- SQL优化的一些总结 SQL编写一般要求
SQL编写一般要求---SQL语句尽可能简单---分解联接保证高并发---同数据类型的列值比较---不在索引列做运算---禁止使用SELECT *---避免负向查询和%前缀模糊查询---保持事务(连接 ...
- 树形查询SQL优化一例
上周五一哥们发了条SQL,让我看看,代码如下: SELECT COUNT(1) FROM (select m.sheet_id from cpm_main_sheet_history m, cpm_s ...
- 数栈SQL优化案例:隐式转换
MySQL是当下最流行的关系型数据库之一,互联网高速发展的今天,MySQL数据库在电商.金融等诸多行业的生产系统中被广泛使用. 在实际的开发运维过程中,想必大家也常常会碰到慢SQL的困扰.一条性能不好 ...
随机推荐
- monkey及其的日志管理和分析
1. monkey 1.1. 介绍 通过monkey程序模拟用户触摸屏幕,滑动Trackball.按键等操作来对设备上的程序进行压力测试,检测程序多久的时间会发生异常,检查和评估被测程序的稳定性 ...
- 测试流程规范--测试准入、准出、停止标准、bug优先级定义
一.背景 最近在推进组内流程规范专项建设,从"研发测试流程"."提测规范"."测试准入标准"."bug优先级标准".&q ...
- How to using expression setup BackgroundColor AX2012 SSRS Report[AX2012]
tile label using [#99ccff] property BackgroundColor - > expression =Iif(Fields!Flag.Value = " ...
- 1.使用javax.mail, spring的JavaMailSender,springboot发送邮件
一.java发邮件 电子邮件服务器:这些邮件服务器就类似于邮局,它主要负责接收用户投递过来的邮件,并把邮件投递到邮件接收者的电子邮箱中,按功能划分有两种类型 SMTP邮件服务器:用户替用户发送邮件和接 ...
- (5)ASP.NET Core3.1 Ocelot服务质量
1.服务质量(Quality of Service) 对于微服务来说,熔断就是我们常说的"保险丝",意思是当服务出现某些状况时候,通过切断服务防止应用程序不断地执行可能会失败的操作 ...
- 预训练模型——开创NLP新纪元
预训练模型--开创NLP新纪元 论文地址 BERT相关论文列表 清华整理-预训练语言模型 awesome-bert-nlp BERT Lang Street huggingface models 论文 ...
- Socket accept 简要分析
accept 用于从指定套接字的连接队列中取出第一个连接,并返回一个新的套接字用于与客户端进行通信,示例代码如下 #include <sys/types.h> /* See NOTES * ...
- 自适应哈希索引(Adaptive Hash Index, AHI) 转
Adaptive Hash Index, AHI 场景 比如我们每次从辅助索引查询到对应记录的主键,然后还要用主键作为search key去搜索主键B+tree才能找到记录. 当这种搜索变多了,inn ...
- sklearn.tree.DecisionTreeClassifier 详细说明
sklearn.tree.DecisionTreeClassifier()函数用于构建决策树,默认使用CART算法,现对该函数参数进行说明,参考的是scikit-learn 0.20.3版本. ...
- UNP——原始套接字
1.原始套接字的用处 使用原始套接字可以构造或读取网际层及其以上报文. 具体来说,可以构造 ICMP, IGMP 协议报文,通过开启 IP_HDRINCL 套接字选项,进而自定义 IPv4首部. 2. ...