百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT技术 > 正文

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

wptr33 2025-03-01 15:45 10 浏览



以前的文章中提到使用以下 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()
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'

可以发现

  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?

可以看到成本是 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://mp.weixin.qq.com/s/iKifzfuKgBGBguCCdq-B3g

相关推荐

用Java实现RAG的3大核心模块与7个必知细节

一、真实场景驱动:某制造企业的知识管理之痛某汽车零部件企业有超过20万份技术文档(PDF/HTML/Word),工程师每天平均花费2小时查找资料。我们为其构建的Java版RAG系统,将查询耗时缩短至1...

在 C# .NET 中从 PDF 中提取表数据

概述:...

【分享】教你如何使用 Java 读取 Excel、docx、pdf 和 txt 文件

在Java开发中,我们经常需要读取不同类型的文件,包括Excel表格文件、"doc"和"docx"文档文件、PDF文件以及纯文本文件。其中最常用的是A...

Spring AI 模块架构与功能解析

SpringAI是Spring生态系统中的一个新兴模块,专注于简化人工智能和机器学习技术在Spring应用程序中的集成。本文将详细介绍SpringAI的核心组件、功能模块及其之间的关...

告别付费!一站式服务,PDF多功能工具!

大家好,我是Java陈序员。今天,给大家介绍一个PDF多功能在线操作工具,完全免费开源!...

本地PDF操作神器:永久告别盗版和破解,再也不用担心安全问题

前言PDF(便携式文档格式)目前已经成为了文档交换和存储的标准。然而,找到一个功能全面、安全可靠、且完全本地化的PDF处理工具并不容易。...

Python rembg 库去除图片背景

rembg是一个强大的Python库,用于自动去除图片背景。它基于深度学习模型(如U^2-Net),能够高效地将前景物体从背景中分离,生成透明背景的PNG图像。本教程将带你从安装到实际应用...

31个必备的python字符串方法,建议收藏

字符串是Python中基本的数据类型,几乎在每个Python程序中都会使用到它。...

python学习day1——输出格式化

print一般在控制台中我们用print进行输出,默认情况下,使用格式为:print(*objects,sep='',end='\n')第一个参数是我们要在控制台...

一张图认识Python(附基本语法总结)

一张图认识Python(附基本语法总结)一张图带你了解Python,更快入门,一张图认识Python(附基本语法总结)Python基础语法总结:1.Python标识符在Python里,标识符有字...

学习编程第188天 python编程 字典格式化

今天学习的是刘金玉老师零基础Python教程第84期,主要内容是python字典格式化。...

Python基础数据类型转换

Python中的基础数据类型转换可以分为隐式转换和显示转换。隐式转换是python解释器自动转换,显示转换是通过内置函数实现。无论哪种方式进行的转换,均为转换为对应类型的数据,而非改变原数据的类型。...

python之json基本操作

1.概述JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式,它具有简洁、清晰的层次结构,易于阅读和编写,还可以有效的提升网络传输效率。Python标准库的...

Python之迭代器及其用法

前面章节中,已经对列表(list)、元组(tuple)、字典(dict)、集合(set)这些序列式容器做了详细的介绍。值得一提的是,这些序列式容器有一个共同的特性,它们都支持使用for循环遍历存储...

从初始化一个现代 python项目学习到的东西

uv我准备用uv初始化一个python项目环境我用的是苹果笔记本MacBookPro,具体的操作系统及硬件参数如下:...