来自:码海

前言

上篇 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 TABLE `person` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`name` varchar(255) NOT NULL,`score` int(11) NOT NULL,`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,PRIMARY KEY (`id`),KEY `name_score` (`name`(191),`score`),KEY `create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

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

CREATE PROCEDURE insert_person()
begindeclare c_id integer default 1;while c_id<=100000 doinsert 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'

可以发现

  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 功能使用如下

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 来优化我们的查询语句。

巨人的肩膀

  • https://time.geekbang.org/column/article/213342

特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下:长按订阅更多精彩▼如有收获,点个在看,诚挚感谢

我说 SELECT COUNT(*) 会造成全表扫描,面试官让我回去等通知相关推荐

  1. 我说MySQL里每张表不要超过100w数据,面试官让我回去等通知?

    V-xin:ruyuanhadeng获得600+页原创精品文章汇总PDF 目录 1.面试题 2.面试官心理分析 3.面试题剖析 1.面试题 事务的几个特点是什么? 数据库事务有哪些隔离级别? MySQ ...

  2. oracle sql 分区查询语句_oracle11g 表分区后的查询语句如何知道是否进行了全表扫描...

    2019-05-10 回答 1. 对返回的行无任何限定条件,即没有where 子句 2. 未对数据表与任何索引主列相对应的行限定条件 例如:在city-state-zip列创建了三列复合索引,那么仅对 ...

  3. 哪些SQL语句会引起全表扫描

    大家都知道,用SQL语句对数据库进行操作时,如果引起全表扫描会对数据库的性能形成影响,下面简单介绍下SQL中哪些情况会引起全表扫描. 1.模糊查询效率很低: 原因:like本身效率就比较低,应该尽量避 ...

  4. oracle select max min 优化,select max(),min()为什么要全表扫描?

    本帖最后由 wendy 于 2013-7-19 15:27 编辑 版本:oracle 10.2.0.4(64bit). SQL> set autotrace on; SQL> select ...

  5. 记录一次没有收集直方图优化器选择全表扫描导致CPU耗尽

    场景:数据库升级第二天,操作系统CPU使用率接近100%. 查看ash报告: 再看TOP SQL 具体SQL: select count(1) as chipinCount, sum(bets) as ...

  6. mysql什么情况会扫描所有_造成MySQL全表扫描的原因

    全表扫描是数据库搜寻表的每一条记录的过程,直到所有符合给定条件的记录返回为止.通常在数据库中,对无索引的表进行查询一般称为全表扫描:然而有时候我们即便添加了索引,但当我们的SQL语句写的不合理的时候也 ...

  7. 优化一个奇葩表设计上的全表扫描SQL

    之前在一个比较繁忙的系统抓到的耗时长.消耗CPU多的一条SQL,如下: SELECT * FROM Z_VISU_DATA_ALARM_LOG T WHERE TO_DATE(T.T_TIMESTR, ...

  8. mysql in 子查询无法使用索引全表扫描 慎用in

    背景 最近慢sql 性能优化 发现一个调用频率高的sql 使用 in 子查询,导致外层全表扫描的问题? 为什么会产生这样的问题?特别强调在优化的使用发现 使用in 和优化后的效果差距 300倍,可见全 ...

  9. 数据库CPU内存高以及全表扫描的原因

    哪些情况数据库全表扫描 1. 使用null做为判断条件 2. 左模糊查询Like %XXX% 3. 使用or做为连接条件 4. 使用in时(not in) 5.使用not in时 6.使用!=或< ...

最新文章

  1. Linux创建两个相同的用户,在Linux系统中一次创建多个用户
  2. Struts2如何实现MVC,与Spring MVC有什么不同?
  3. PMP-【第1章 引论】-2020-12-02(1页-18页)
  4. java泛型程序设计——Varargs 警告+不能实例化类型变量
  5. python 类和对象 有必要学吗_类与对象-python学习19
  6. R语言第八讲 评估模型之交叉验证法分析案例
  7. ​图片流量节省60%:基于CDN的sharpP自适应图片技术实践
  8. Newtonsoft.Json.dll 的使用
  9. Android入门教程 (零)付费专栏课程规划
  10. MATLAB笔记7:三维曲线plot3和fplot3;三维曲面mesh和surf
  11. 支付宝小程序获取手机号授权
  12. 如何在阿里云服务器部署程序并用域名直接访问
  13. 马化腾的互联网之路:别人不是打不赢你,掌声越热烈就越危险
  14. android 分享到qq黑屏,Android 第三方登录 QQ登录Android 10系统 出现黑屏问题
  15. 打鼾,是一种全身性疾病
  16. C#_CRC-16/CCITT-FALSE计算加判断
  17. Scrum立会报告+燃尽图(Beta阶段第二周第三次)
  18. ubuntu 系统狠慢 或者很卡的原因
  19. 第四章 MPT 现代组合理论
  20. android中适配器的作用,适配器模式 在Android中的简单理解

热门文章

  1. 网络数据采集与python爬虫_高校邦网络数据采集与Python爬虫答案
  2. poj2594(最小可相交覆盖路径问题)
  3. poj1734(最小环+路径)
  4. PTA基础编程题目集-6-9 统计个位数字
  5. HDU1285拓扑排序模版题
  6. python怎么控制while循环_Python流程控制之while循环怎么学呢?老男孩Python
  7. vscode pylint 错误_VScode中报Unable to import #x27;xxx#x27; pylint的解决方案
  8. hangfire 过期记录_时隔数月后我又有减肥的想法(饮食日记录)
  9. J - Fire! UVA - 11624
  10. HTML在表格右边增加一个表格,如何在表格右侧增加一列