0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

SELECT COUNT(*) 会造成全表扫描?

Android编程精选 来源:程序员大彬 2023-03-25 09:53 次阅读

前言

SELECTCOUNT(*)会不会导致全表扫描引起慢查询呢?

SELECTCOUNT(*)FROMSomeTable

网上有一种说法,针对无where_clauseCOUNT(*),MySQL 是有优化的,优化器会选择成本最小的辅助索引查询计数,其实反而性能最高,这种说法对不对呢

针对这个疑问,我首先去生产上找了一个千万级别的表使用 EXPLAIN来查询了一下执行计划

EXPLAINSELECTCOUNT(*)FROMSomeTable

结果如下

f5a79de0-caa7-11ed-bfe3-dac502259ad0.jpg

如图所示: 发现确实此条语句在此例中用到的并不是主键索引,而是辅助索引,实际上在此例中我试验了,不管是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)

CREATETABLE`person`(
`id`bigint(20)NOTNULLAUTO_INCREMENT,
`name`varchar(255)NOTNULL,
`score`int(11)NOTNULL,
`create_time`timestampNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,
PRIMARYKEY(`id`),
KEY`name_score`(`name`(191),`score`),
KEY`create_time`(`create_time`)
)ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;

这个表除了主键索引之外,还有另外两个索引,name_scorecreate_time。然后我们在此表中插入 10 w 行数据,只要写一个存储过程调用即可,如下:

CREATEPROCEDUREinsert_person()
begin
declarec_idintegerdefault1;
whilec_id<=100000 do
insertintopersonvalues(c_id,concat('name',c_id),c_id+100,date_sub(NOW(),intervalc_idsecond));
setc_id=c_id+1;
endwhile;
end

插入之后我们现在使用 EXPLAIN 来计算下统计总行数到底使用的是哪个索引

EXPLAINSELECTCOUNT(*)FROMperson
f5bbf420-caa7-11ed-bfe3-dac502259ad0.png

从结果上看它选择了create_time辅助索引,显然 MySQL 认为使用此索引进行查询成本最小,这也是符合我们的预期,使用辅助索引来查询确实是性能最高的!

我们再来看以下 SQL 会使用哪个索引

SELECT*FROMpersonWHERENAME>'name84059'ANDcreate_time>'2020-05-231418'
f5d67b56-caa7-11ed-bfe3-dac502259ad0.png

用了全表扫描!理论上应该用name_score或者create_time索引才对,从 WHERE 的查询条件来看确实都能命中索引,那是否是使用SELECT *造成的回表代价太大所致呢,我们改成覆盖索引的形式试一下

SELECTcreate_timeFROMpersonWHERENAME>'name84059'ANDcreate_time>'2020-05-231418'

结果 MySQL 依然选择了全表扫描!这就比较有意思了,理论上采用了覆盖索引的方式进行查找性能肯定是比全表扫描更好的,为啥 MySQL 选择了全表扫描呢,既然它认为全表扫描比使用覆盖索引的形式性能更好,那我们分别用这两者执行来比较下查询时间吧

--全表扫描执行时间:4.0ms
SELECTcreate_timeFROMpersonWHERENAME>'name84059'ANDcreate_time>'2020-05-231418'

--使用覆盖索引执行时间:2.0ms
SELECTcreate_timeFROMpersonforceindex(create_time)WHERENAME>'name84059'ANDcreate_time>'2020-05-231418'

从实际执行的效果看使用覆盖索引查询比使用全表扫描执行的时间快了一倍!说明 MySQL 在查询前做的成本估算不准!我们先来看看 MySQL 做全表扫描的成本有多少。

前面我们说了成本主要 IO 成本和 CPU 成本有关,对于全表扫描来说也就是分别和聚簇索引占用的页面数和表中的记录数。执行以下命令

SHOWTABLESTATUSLIKE'person'
f5f0ceac-caa7-11ed-bfe3-dac502259ad0.png

可以发现

  1. 行数是 100264,我们不是插入了 10 w 行的数据了吗,怎么算出的数据反而多了,其实这里的计算是估算,也有可能这里的行数统计出来比 10 w 少了,估算方式有兴趣大家去网上查找,这里不是本文重点,就不展开了。得知行数,那我们知道 CPU 成本是100264 * 0.2 = 20052.8

  2. 数据长度是 5783552,InnoDB 每个页面的大小是 16 KB,可以算出页面数量是 353。

也就是说全表扫描的成本是20052.8 + 353 = 20406

这个结果对不对呢,我们可以用一个工具验证一下。在 MySQL 5.6 及之后的版本中,我们可以用optimizer trace功能来查看优化器生成计划的整个过程 ,它列出了选择每个索引的执行计划成本以及最终的选择结果,我们可以依赖这些信息来进一步优化我们的 SQL。

optimizer_trace功能使用如下

SEToptimizer_trace="enabled=on";
SELECTcreate_timeFROMpersonWHERENAME>'name84059'ANDcreate_time>'2020-05-231418';
SELECT*FROMinformation_schema.OPTIMIZER_TRACE;
SEToptimizer_trace="enabled=off";

执行之后我们主要观察使用name_scorecreate_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来优化我们的查询语句。

审核编辑 :李倩


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • MySQL
    +关注

    关注

    1

    文章

    809

    浏览量

    26574
  • 索引
    +关注

    关注

    0

    文章

    59

    浏览量

    10469

原文标题:SELECT COUNT(*) 会造成全表扫描?回去等通知吧

文章出处:【微信号:AndroidPush,微信公众号:Android编程精选】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    实现延迟函数是ReadCoreTimer还是_CP0_GET_COUNT

    大家好,我正在尝试实现一个延迟函数,我的问题是关于我应该使用哪一个?代码显示了如何定义:#._CP0_GET_COUNT()_mfc0(_CP0_COUNT,_CP0_COUNT_SELECT)无符号int_.((nomips1
    发表于 10-10 14:16

    ucos节拍过快造成系统负荷加重怎么解决?

    书上说时钟节拍一般为每秒10-100次为好,节拍过快造成系统负荷加重。但是在许多仪表中,LED数码管需要动态扫描激励,简易的矩阵键盘也需要考动态扫描来识别按键是否被人按下。这样,10
    发表于 06-17 08:09

    基础SQL语句-使用SELECT索引数据

    SELECT 语句是最常用的SQL语句了,用来索引一个或者多个信息。关键字(keyword)作为SQL组成部分的字段,关键字不能作为或者列的名字。使用SELECT索引数据,必须至少
    发表于 11-03 14:34

    SuperK_SELECT数据手册

    The SuperK SELECT is a tunable wavelength filter based on Acousto-optic Tunable Filters (AOTF
    发表于 12-25 22:04 0次下载

    epoll和select的区别

     select,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select
    发表于 11-10 16:20 2.1w次阅读
    epoll和<b class='flag-5'>select</b>的区别

    20秒完成全身3D扫描的医学成像设备

    一款最新的医学成像设备只需20秒就能完成全身3D扫描,不久或将在研究和临床领域得到广泛应用。传统的正电子发射断层扫描仪(PET)一般需要20分钟的成像时间,而这款经过改良的PET扫描
    发表于 06-30 10:58 2943次阅读

    SQL告别count改用LIMIT 1

    根据某一条件从数据库中查询 『有』与『没有』,只有两种状态,那为什么在写SQL的时候,还要SELECT count(*) 呢?无论是刚入道的程序员新星,还是精湛沙场多年的程序员老白,都是一如既往
    的头像 发表于 07-26 10:57 2062次阅读

    select......for update还是锁行?

    验证 结合一下实例验证 结果   select查询语句是不会加锁的,但是select .......for update除了有查询的作用外,还会加锁呢,而且它是悲观锁。 那么它加的是行锁还是
    的头像 发表于 10-10 15:54 1520次阅读

    Select、Switch组件的使用

    Element UI 的 Select 直接使用 el-select / el-option 标签即可,属性 v-model 表示该下拉框绑定的对象,即最终选择的值赋给该对象,直接用于
    的头像 发表于 02-28 15:40 1133次阅读
    <b class='flag-5'>Select</b>、Switch组件的使用

    rt-smart select的实现

    select()是常用的多路IO复用的posix调用接口。select () 函数指示指定的文件描述符中的哪些已准备好读取、准备好写入或有待处理的错误条件。
    的头像 发表于 08-09 16:05 757次阅读

    基于select!宏的进阶用法

    Tokio 是一个基于 Rust 语言的异步编程框架,它提供了一组工具和库,使得异步编程变得更加容易和高效。其中最重要的组件之一就是 select!宏。 select!宏是 Tokio 中的一个核心
    的头像 发表于 09-19 15:35 680次阅读

    epoll和select使用区别

    epoll 和select 相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时
    的头像 发表于 11-09 14:14 1121次阅读
    epoll和<b class='flag-5'>select</b>使用区别

    数据库select语句的基本用法

    数据库中的SELECT语句是用于从数据库中检索数据的基本工具。它是数据库语言(如SQL)中最常用的命令之一,几乎在每个数据库管理系统中都有。 SELECT语句的基本语法如下: SELECT
    的头像 发表于 11-17 15:08 2016次阅读

    SELECT语句的基本格式

    列名 1 , 列名 2 , ..., 列名n FROM 名; 在这个格式中, SELECT 关键字用于指示我们正在执行一个查询操作。紧接着是我们要检索的列名,用逗号分隔。如果我们想检索所有列,可以使用星号(*)代替列名。接下来,是 FROM 关键字,用于指定我们要从哪
    的头像 发表于 11-17 15:10 2698次阅读

    select语句的基本语法

    、详实、细致地解释SELECT语句的基本语法以及关键部分。 SELECT语句的基本语法如下: SELECT 列名 1 , 列名 2 , ... FROM 名 WHERE 条件 上述语
    的头像 发表于 11-17 16:23 2073次阅读