我们用 explain 分析包含 group by 的 select 语句时,从输出结果的 Extra 列经常可以看到 Using temporary; Using filesort
。看到这个,我们就知道 MySQL 使用了临时表来实现 group by。
使用临时表实现 group by,成本高,执行慢。如果能够利用索引中记录已经排好序的特性,使用索引来实现 group by,那就是鸟枪换炮了。
本文我们一起来探寻 MySQL 使用索引实现 group by 的过程,使用临时表实现 group by 会单独用一篇文章来介绍。
本文内容基于 MySQL 5.7.35 源码。
内容目录
使用索引实现 group by,最简单的方式,大概就是这样了:
这种实现方式被称为紧凑索引扫描
。
紧凑索引扫描会对满足 where 条件的所有记录进行聚合函数处理,而对于 min()、max() 来说,实际需要的只有每个分组中聚合函数字段值最小或最大的那条记录。
如果 server 层能直接从存储引擎读取到每个分组中聚合函数需要的那条记录,而不必读取每个分组中的所有记录进行聚合函数处理,是不是就可以节省很多时间了?
是的,这种只读取分组中部分记录实现 group by 的方式,被称为松散索引扫描
。
为了方便描述,本文在需要的时候会以具体 SQL 作为示例说明,示例 SQL 的表结构如下:
CREATE TABLE `t_group_by` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`i1` int(10) unsigned DEFAULT '0',
`c1` char(11) DEFAULT '',
`e1` enum('北京','上海','广州','深圳','天津','杭州','成都','重庆','苏州','南京','洽尔滨','沈阳','长春','厦门','福州','南昌','泉州','德清','长沙','武汉') DEFAULT '北京',
`d1` decimal(10,2) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_e1_i1` (`e1`,`i1`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
group by 字段包含在索引中,并且满足索引最左匹配原则,server 层就可以顺序读取索引中的记录实现 group by,而不需要借助临时表。
紧凑索引扫描中的紧凑
,表示 server 层从存储引擎读取记录时,以索引范围扫描或全索引扫描方式,按顺序一条一条读取记录,不会跳过中间的某条记录,示意图如下:
紧凑索引扫描
接下来,我们以 avg()
为例介绍紧凑索引扫描
的执行过程,示例 SQL 如下:
select
e1, avg(i1) as t
from t_group_by
where d1 > 5452415
group by e1
词法分析 & 语法分析阶段,avg(i1) 被解析为 Item_sum_avg 类,以下是该类的实例属性的其中 3 个:
sum
,保存分组求和结果。count
,保存分组计数。args
,avg() 函数的参数,avg() 只能有一个参数。args[0] 为 i1 字段对应的 Item_field 类实例。Item_sum_avg
avg() 只有一个参数,为什么参数属性名是 args?
Item_sum_avg 类的实例属性
args
是从父类 Item_sum 继承得到的。Item_sum_count 类(
count() 对应的类
)的实例属性args
也是从父类 Item_sum 继承的,count() 可以有多个参数,所以,用 args 来表示聚合函数的参数。
查询准备阶段(prepare 阶段),i1 字段对应的 Item_field 类实例会关联到表 t_group_by 的 i1 字段。
Item_sum_avg
执行阶段,server 层从存储引擎读取到一条记录之后,判断记录是否符合 where 条件(d1 > 5452415
)。
记录不符合 where 条件,继续读取下一条记录。
记录符合 where 条件,进行聚合函数逻辑处理。
如果当前记录的分组前缀
(示例 SQL 中 group by 的 e1 字段值)和上一条记录的分组前缀不一样
,说明需要结束上一个分组,并开启新分组。
结束上一个分组
:通过 sum / count
计算得到分组平均值(即 avg(i1) 的结果),把分组前缀
及分组平均值
发送给客户端。开启新分组
:Item_sum_avg 类的实例属性 sum、count 清零
,当前记录的 e1 字段值作为新分组前缀
,然后,新分组进行分组求和(sum 加上 i1 字段值)、分组计数(count 加 1)。如果当前记录的分组前缀
和上一条记录的分组前缀一样
,说明还是同一个分组,只需要
进行分组求和、分组计数,不需要计算平均值。
分组求和、分组计数代码如下:
bool Item_sum_avg::add()
{
// 分组求和
if (Item_sum_sum::add())
return TRUE;
// 分组计数(字段值不为 NULL 才进行计数)
if (!aggr->arg_is_null(true))
count++;
return FALSE;
}
只有字段值不为 NULL,分组计数(count)才会加 1。
了解 avg() 之后,count()、sum() 也就明白了。count()、sum() 和 avg() 的执行过程基本一样,不同之处在于:
只需要进行分组计数
,不需要分组求和、计算平均值。只需要进行分组求和
,不需要分组计数、计算平均值。松散索引扫描,从存储引擎读取分组记录时,会跳着读,读取分组前缀之后,直接通过分组前缀
(group by 字段的值)定位到分组中符合 where 条件的第一条或最后一条记录,而不需要读取分组的所有记录,然后就接着读取下一个分组的分组前缀,这样可以减少 select 语句执行过程中需要读取的记录数,从而比紧凑索引扫描更快(有例外情况,后面会介绍)。
松散索引扫描
如果 select 语句执行过程中使用了松散索引扫描实现 group by,explain 输出结果的 Extra 列会显示 Using index for group-by
。
松散索引扫描用于 min()、max(),可以减少需要读取的记录数;用于 count(distinct)、sum(distinct)、avg(distinct) ,可以对记录去重
,避免使用临时表去重。
我们以 min()
为例介绍松散索引扫描
的执行过程,示例 SQL 如下:
select
e1, min(i1)
from t_group_by
group by e1
词法分析 & 语法分析阶段,min(i1) 被解析为 Item_sum_min 类,以下是该类的实例属性的其中 2 个:
value
,该属性类型为 Item_cache,Item_cache 子类的实例属性 value 保存分组最小值
(分组记录中 i1 字段的最小值)。args
,min() 函数的参数,args[0] 为 i1 字段对应的 Item_field 类实例。Item_sum_min
查询准备阶段,i1 字段对应的 Item_field 类实例会关联到表 t_group_by 的 i1 字段。
Item_sum_min
执行阶段,读取分组最小值的过程分为两步:
读取分组前缀
(示例 SQL 中 group by 的 e1 字段值),从存储引擎读取分组的第一条记录,得到分组前缀。根据分组前缀读取分组最小值
(分组记录中 i1 字段的最小值),用前面得到的分组前缀限定索引扫描范围,从存储引擎读取分组中 i1 字段的最小值,保存到 value 属性中。读取分组最小值
松散索引扫描虽然具备提升 select 语句执行效率的能力,但只有在适用的场景下才能发挥它的威力,因此,它的使用需要满足以下条件
:
条件 1,select 语句只能是单表查询,不能是连接查询。
条件 2,group by 字段必须满足索引的最左匹配原则。例如:表中有一个索引包含 c1, c2, c3 三个字段,group by c1, c2 满足最左匹配原则。
条件 3,如果 select 字段列表中包含聚合函数,聚合函数必须满足这些条件:
group by 字段 + 聚合函数字段
也必须满足索引最左匹配原则。松散索引扫描中,两类聚合函数不能同时存在,因为它们对于
分组前缀
处理逻辑不一样。在读取数据时,min()、max() 用group by 字段值
作为分组前缀;count(distinct)、sum(distinct)、avg(distinct) 用group by 字段值 + 聚合函数中的字段值
作为分组前缀。
条件 4,索引中所有字段必须是全字段索引,不能是前缀索引。
例如:有个字段 c1 varchar(20)
,索引中该字段为 index(c1(10))
,这样的索引就不能用于松散索引扫描。
满足以上条件,还只是站在了使用松散索引扫描的门外,想要登堂入室,还必须进行成本评估。
如果松散索引扫描的成本比紧凑索引扫描的成本低,自然就要用松散索引扫描
来提升 select 语句的执行效率了。
松散索引扫描成本比紧凑索引扫描成本更高时,如果 select 语句中的聚合函数是 min()、max() 中的 1 ~ 2 个,就会使用紧凑索引扫描
。
松散索引扫描自带去重功能
,不需要借助临时表,和包含 distinct 关键字的聚合函数天生更匹配。紧凑索引扫描则需要借助临时表
对记录进行去重。
如果聚合函数是 count(distinct)、sum(distinct)、avg(distinct) 中的 1 ~ 3 个,虽然紧凑索引扫描读取记录成本更低,但必须使用临时表
对记录去重,这样一来,紧凑索引扫描读取数据 + 临时表去重
的成本就比松散索引扫描成本更高了。
这就很尴尬了,两种方式各有优缺点,两难之下,MySQL 要怎么办?
两难之下,最好的选择就是找到第三个选项。为此,MySQL 祭出了一个大招,既要和紧凑索引扫描一样顺序读取数据,又要用松散索引扫描自带的去重能力。如果用了这个大招,在 explain 输出结果的 Extra 列可以看到 Using index for group-by (scanning)
。
MySQL 把紧凑索引扫描中使用的顺序读取记录
嵌入到松散索引扫描的逻辑里,当评估紧凑索引扫描成本比松散索引扫描低时,对于包含 distinct 关键字的聚合函数,就会用顺序读取记录代替跳着读取记录,并且在顺序读取记录的过程中完成记录去重。
对于松散索引扫描的这个变种,到写完本文为止,我还没有在哪里看到官方有正式的命名,为了方便记忆,估且把它命名为顺序松散索引扫描
吧。
顺序松散索引扫描
紧凑索引扫描
,存储引擎按顺序一条一条读取记录,返回给 server 层,server 层判断记录是否符合 where 条件,然后对符合条件的记录进行聚合函数逻辑处理。
松散索引扫描
,对于每个分组,都会从存储引擎读取两次数据,第一次是读取分组的第一条记录,得到分组前缀
;第二次是根据分组前缀
读取分组中索引扫描范围的第一条或最后一条记录。
如果分组中的记录数量多,第二次读取记录时,能跳过
的记录就多,节省的成本就多,松散索引扫描就会比紧凑索引扫描更快。
如果分组中的记录数量少,第二次读取记录时,能跳过的记录就少,每个分组读取两次记录增加的成本超过了
跳过记录节省的成本,松散索引扫描就会比紧凑索引扫描更慢。
引言
小节,介绍了 MySQL 实现 group by 的两种索引扫描方式:紧凑索引扫描、松散索引扫描。
紧凑索引扫描
小节,以 avg() 为例介绍了紧凑索引扫描的执行过程,avg() 在词法分析 & 语法分析阶段会被解析为 Item_sum_avg 类。该类的实例属性 sum、count、args 分别用于保存分组求和结果、分组计数、avg() 函数的参数。
在执行阶段,通过把 avg() 字段值累加到 sum 属性进行分组求和;对 count 属性进行自增实现分组计数;通过 sum / count 计算得到分组平均值。
Item_sum_count、Item_sum_sum、Item_sum_avg、Item_sum_min、Item_sum_max 类的实例属性 args 都继承自父类 Item_sum,用于保存聚合函数参数,count() 支持多个参数,所以,参数的属性名为 args 而不是 arg。
松散索引扫描
小节,以 min() 为例介绍了松散索引扫描的执行过程,执行阶段,分为两步读取分组最小值:读取分组前缀,根据分组前缀读取分组最小值。
两种索引扫描怎么选?
小节,介绍了使用松散索引扫描必须满足的一系列条件。
当松散索引扫描比紧凑索引扫描成本高时,min()、max() 会选择用紧凑索引扫描,MySQL 为 count(distinct)、sum(distinct)、avg(distinct) 引入松散索引扫描的变种,顺序读取索引记录(和紧凑索引扫描一样)+ 松散索引扫描自带的记录去重功能
,避免了使用临时表对记录去重。
还介绍了松散索引扫描比紧凑索引扫描的成本高,是因为分组中记录数量少时,两次读取存储引擎数据增加的成本超过了跳着读取索引记录节省的成本。
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/kl93d0S_e0pB2o2GsQ_iaA
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。