介绍使用索引、临时表 + 文件排序实现 group by,以及单独介绍临时表的三篇文章中,多次以 count(distinct) 作为示例说明。
那还有必要单独为 count(distinct) 写一篇文章吗?
此刻,想到一句台词:别问,问就是有必要。
回到正题,MySQL 使用 MEMORY 引擎临时表实现 count(distinct) 的去重功能时,玩出了新花样,所以,还是值得写一下的。背景说明到此为止,我们快快开始。
本文内容基于 MySQL 5.7.35 源码。
如果 count(distinct) 不能使用索引去重,就需要使用临时表。临时表的存储引擎有三种选择:MEMORY、MyISAM、InnoDB。
和使用 MyISAM 或 InnoDB 作为临时表的存储引擎处理逻辑有些不一样,如果 MySQL 决定使用 MEMORY 作为临时表的存储引擎,临时表会被创建,但只是作为辅助,表里不会
写入任何数据。
要说清楚为什么会有这种花样操作,需要从 MEMORY 引擎支持的两种索引结构说起。
MEMORY 引擎支持两种索引结构:HASH 索引、B-TREE 索引。
HASH 索引,顾名思义,索引的数据结构是哈希表
。hash key 是索引字段内容计算得到的哈希值,hash value 是索引记录指向的数据行的地址。
HASH 索引中的记录不是
按照字段内容顺序存放的,而是乱序
的,其优点在于查找时间复杂度是 O(1),按单个值查找记录速度非常快,但不能用于范围查询。
HASH 索引结构示意图
MyISAM、InnoDB 引擎 B-TREE 索引的数据结构是 B+ 树,而 MEMORY 引擎 B-TREE 索引的数据结构是红黑树
。
B-TREE 索引结构示意图
MEMORY 引擎的 B-TREE 索引结点中保存着索引字段内容,以及对应数据行的地址。
红黑树是平衡二叉排序树,因此 B-TREE 索引中的结点是排好序的,支持范围查询,但是按单个值查找记录的时间复杂度是 O(logN),相比于 HASH 索引来说要低一些。
基于两种数据结构的特点,HASH 索引适用于单值查找场景,B-TREE 索引适用于范围查询和需要排好序的记录的场景。
说完了 MEMORY 引擎的两种索引结构,以及它们的适用场景,再来介绍 count(distinct) 去重方案就有基础了。
按照常规流程走,当 MySQL 选择使用 MEMORY 作为临时表的存储引擎,加上为 distinct 字段创建的 HASH 索引,这完全能实现去重操作。但是本着节约精神,MySQL 向来是能省则省,只要有优化方案,一定是要使用的,那还可以怎么优化呢?
要回答这个问题,我们需要先抓住去重功能的关键,那就是表中记录的唯一性
。
临时表中为 distinct 创建的 HASH 索引默认
就是唯一索引,既然 HASH 索引本身就保证了唯一性,是不是可以考虑只使用 HASH 索引
实现 count(distinct) 的去重功能呢?
这种思路是可行的,不过 MEMORY 引擎的 HASH 索引有一个不能满足要求的地方:HASH 索引中没有保存索引字段内容,只保存了字段内容的 hash 值。
只用索引的数据结构去重为什么需要保存字段内容,介绍去重过程的时候会说明,在那个场景下解释起来更好理解一点,这里先按下不表。
既然 HASH 索引不能满足要求,别忘了 MEMORY 引擎还支持另一种索引结构:B-TREE 索引
。B-TREE 索引也能实现去重功能,索引结点中还保存了字段内容,完美符合要求。
前面以流式的方式介绍了三种候选的去重方案,我们需要来个小结:
方案一,临时表 + HASH 索引,这种实现方案中规中矩,能够满足需求,缺点在于:只是为了实现去重功能,HASH 索引自己上阵就够了,可偏偏还要搭上更多内存往表里写一份数据,浪费宝贵的内存资源。
方案二,方案一
中说到,HASH 索引自己就能够实现去重了,这点毋庸置疑,只使用 HASH 索引
也是候选方案之一,但 HASH 索引中没有保存索引字段内容,只能无奈的出局了。
方案三,B-TREE 索引,既能实现去重功能,索引中还保存了字段内容,完美,就是它了。
不过,MySQL 没有
在 MEMORY 临时表上再创建一个 B-TREE 类型的唯一索引,而是用了 B-TREE 索引所使用的红黑树
,并且因为临时表中不会写入任何数据,红黑树结点中只需要保存字段内容,不需要保存指向表中数据行的地址。
再次说明:MEMORY 临时表还是会创建,但是不会写入任何数据,就是空表。红黑树实现去重功能的过程中,会用到 MEMORY 临时表的字段信息、记录缓冲区。
以后,用 explain 查看执行计划时,如果发现 count(distinct) 既没有使用索引,也没有使用临时表,那你可能就会想到:这家伙大概是悄无声息的使用了红黑树。
前面说了这么多,只是为了弄清楚一个问题:为什么选择红黑树实现去重功能。这很重要,我们要知其然,更要知其所以然,这样我们理解起来也会更容易些,你说是吗?
接下来我们就要说说红黑树实现 count(distinct) 去重功能的点点滴滴了。
红黑树是平衡二叉排序树,既然是二叉树结构,就会有指向左子树、右子树的指针。
红黑树的结点分为红色和黑色,自然要有个属性来标记结点颜色。
MySQL 实现的红黑树,还支持插入重复结点
,这是通过在结点中增加一个记录结点内容重复次数
的属性实现的。
以上信息都属于结点元数据
,元数据占用 24 字节内存空间。每一个结点中,除了保存着结点元数据,还要保存结点数据
,就是字段内容。
结点元数据
使用红黑树去重虽然不用往 MEMORY 临时表写入数据,但是红黑树也不能无限制占用内存。
它能够占用的最大内存和 MEMORY 引擎临时表一样,也是由 tmp_table_size、max_heap_table_size 两个系统变量中较小的的那个决定的。默认配置下,红黑树能够占用的最大内存为 16M。
既然内存大小有限制,那就可能会出现红黑树中没有空间容纳新结点的情况,此时,磁盘文件
就要粉墨登场了。
如果红黑树占用内存达到最大值,所有结点数据
(不包含元数据)会被写入磁盘文件,然后删除红黑树所有结点,保留内存以便重复使用
。
这些一起写入磁盘文件的数据会组成一个数据块,数据块的相关信息(在磁盘文件中的位置、记录数量)保存在对应的 Merge_chunk 中。
磁盘文件中可能会有多个数据块。
数据块相关信息数据块中的数据,因为来源于红黑树,有两个特点:
顺序
存放的。唯一
的,不存在重复。数据块中的数据
通过上一小节,我们知道红黑树占用内存达到最大值之后,会生成一个数据块写入到磁盘文件。
所谓天下大势,合久必分,分久必合。
磁盘文件中的数据块,虽然是分开写入的,但终究要合并去重,并进行分组计数。
磁盘文件中的每个数据块内部,记录的字段内容是不存在重复的。但是,多个数据块之间的字段内容可能存在重复,合并过程中,需要对多个数据块之间的字段内容去重。
合并去重,有两种可选的实现方案:
方案一,分为三步:
① 从磁盘文件的每个数据块
中读取剩余记录里最小的一条
记录到内存中,最小的记录其实就是剩余记录里的第一条
记录。
② 找出第 ① 步读取的那些记录中最小的记录。
③ 判断当前的最小记录,是否和上一次最小的记录相同,如果相同
,说明重复,不处理;如果不同
,进行计数。
循环执行第 ① ~ ③ 步,直到读完当前分组所有数据块中的记录,合并完成。
从以上描述中,想必大家已经发现了这种方案存在的问题:需要频繁的从磁盘文件中读取数据,每次还只读取一条记录,频繁磁盘 IO 必然会影响 SQL 语句执行效率,为此,就有了方案二
。
方案二,既然不能频繁从磁盘中读取数据,那就换个方式,每次读取一批
记录,减少读取次数。
但是,一批记录和一条记录不一样,需要找个大点的地方临时存放,于是就有了合并缓冲区
。
合并缓冲区的大小和红黑树占用内存最大值一样,也是由 tmp_table_size、max_heap_table_size 两个系统变量中较小的那个控制的,默认大小为 16M。
合并缓冲区会分成 N 份(N = 磁盘文件中数据块的数量),每一份对应一个数据块,用于存放从数据块中读取的一批记录。
合并缓冲区
介绍完了前置知识点,重头戏来了,该说说红黑树去重和分组计数的过程了。
为了方便描述,我们还是结合一个具体 SQL 来介绍,示例表及 SQL 如下:
CREATE TABLE `t_group_by` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`e1` enum('北京','上海','广州','深圳','天津','杭州','成都','重庆','苏州','南京','哈尔滨','沈阳','长春','厦门','福州','南昌','泉州','德清','长沙','武汉') DEFAULT '北京',
`i1` int(10) unsigned DEFAULT '0',
`c1` char(11) DEFAULT '',
`d1` decimal(10,2) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_e1` (`e1`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
select
e1, count(distinct i1)
from t_group_by
group by e1
在调试过程中,我给 t_group_by 表的 e1 字段建了索引,所以 SQL 执行时就不需要先对表中记录进行排序了。
先来看一下去重及分组计数过程的示意图。
去重及分组计数主流程
看完上面的示意图,想必大家对整个过程有个大致的印象了,我们再进一步看看过程中的每一步都会做哪些事情。
第 1 步,读取记录。
从 from 子句的表中读取一条记录,示例 SQL 中为 t_group_by 表。
第 2 步,判断红黑树是否写满。
前面介绍过,红黑树的一个结点中包含两类信息:
结点元数据
,占用 24 字节。结点数据
,示例 SQL 中结点数据就是 i1 字段内容,长度为 4 字节。示例 SQL 中,一个红黑树结点占用 24 + 4 = 28
字节。
知道红黑树最大能占用的内存,每个结点占用的内存,就能够算出红黑树最多可以插入多少个结点了,也就能够很方便的判断出红黑树是不是满了。
如果红黑树已满
,进入第 3 步
,把红黑树中所有结点数据
写入磁盘文件。
如果红黑树没满
,进入第 4 步
,插入新结点。
第 3 步,把红黑树所有结点数据写入磁盘文件。
按照中序遍历,把红黑树中所有结点数据
按顺序写入磁盘文件。结点元数据此时就不需要了,不会
写入磁盘文件。
前面介绍过这些数据会组成一个数组块,每个数据块的相关信息保存在对应的 Merge_chunk 类实例中。
数据写入磁盘后,红黑树会删除所有结点,但是内存空间会保留复用。此时,红黑树就是空的了,进入第 4 步
,把刚刚因为红黑树已满没有插入的节点插入到空的红黑树中。
第 4 步,插入新结点。
从 t_group_by 表读取一条记录之后,i1 字段值作为新结点的数据插入到红黑树中,然后回到第 1 步
继续执行。
第 1 ~ 4 步是循环执行的,直到一个分组的所有数据都插了入红黑树,循环过程结束,进入第 5 步
。
第 5 步,处理 count(distinct) 聚合逻辑。
处理聚合逻辑分两种情况:
没有使用磁盘文件
,分组记录少,红黑树一次都没有写满过,所有数据都在内存中。使用了磁盘文件
,分组记录多,红黑树写满过,前面 N - 1 次写满之后,数据写入磁盘文件,最后一次数据留在内存中。如果没有
使用磁盘文件,进入第 6 步
。
如果使用了
磁盘文件,进入第 7 步
。
第 6 步,分组计数。
红黑树所有结点都在内存中,红黑树中的结点数量就是 count(distinct) 函数的结果。这个步骤处理完,流程结束。
第 7 步,多个数据块合并去重,然后分组计数。
红黑树写满过,部分数据在磁盘文件中,部分数据在内存中。需要先把内存中红黑树所有结点数据写入到磁盘文件中,组成最后一个数据块。
所有数据都写入磁盘文件之后,就可以开始进行合并去重和分组计数了。
首先,分配一块内存作为合并缓冲区
。
然后,把缓冲区平均分成 N 份,为了描述方便,我们把缓冲区的 N 分之一叫作子缓冲区
。假设示例 SQL 在磁盘文件中有 4 个数据块,就会对应 4 个子缓冲区。
每一个数据块对应的 Merge_chunk 中保存着子缓冲区的开始和结束位置、能够存放的记录数量、指向子缓冲区中下一条要处理的记录的位置。
合并缓冲区
每个数据块内部的记录都是按照字段内容从小到大排好序
的,多个数据块合并去重的过程不算复杂,步骤如下:
合并去重及分组计数流程
① 读取磁盘文件中的数据块到子缓冲区。
从每个数据块读取一部分记录到子缓冲区
,所有数据块对应的 Merge_chunk 组成一个优先队列。
此时,每个 Merge_chunk 的 m_current_key
都指向数据块的第一条记录,也是该数据块中最小的记录,这条记录的内容就代表 Merge_chunk 的值。
Merge_chunk 的 m_mem_count
表示已读取到子缓冲区中尚未处理
的记录数量。
② 获取优先队列中最小的 Merge_chunk,用top
表示。
优先队列中第一个 Merge_chunk 就是所有 Merge_chunk 中最小的。
③ 读取 top Merge_chunk 的 m_current_key 指向的记录的内容到 old_key。
m_current_key 指向的记录就是 top Merge_chunk 中的最小记录,记为 old_key
。
然后,m_current_key 指向数据块中下一条
记录,m_mem_count 减 1,表示已读取到子缓冲区中的尚未处理的记录减少 1 条。
④ 如果 m_mem_count 等于
0,说明该数据块对应子缓冲区中的记录已处理完,需要再从磁盘文件中读取该数据块的一部分记录到子缓冲区。
如果数据块中的数据都已处理完,把数据块对应的 Merge_chunk 从优先队列中删除,对应子缓冲区的内存空间全部并入相邻的子缓冲区。
⑤ 更新优先队列中的 top Merge_chunk。
③、④ 两步执行之后,最小的 Merge_chunk 可能发生了变化,所以需要更新优先队列,保证优先队列中的第一个 Merge_chunk 是最小的。
⑥ 真正的去重操作。
比较新的 top Merge_chunk 中最小记录的内容和 old_key
的值,如果一样
,说明字段内容重复,不需要进行分组计数,回到 ③ ,继续进行下一轮循环。
如果不一样
,说明字段内容不重复,对 top Merge_chunk 中的最小记录进行分组计数,然后回到 ③ ,继续进行下一轮循环。
③ ~ ⑥ 是循环执行的,直到优先队列中 Merge_chunk 的数量小于等于
1 个,循环结束。
⑦ 处理最后一个数据块中剩余的数据。
经过 ③ ~ ⑥ 循环执行过程,优先队列中还会剩下 1 个
Merge_chunk,需要对 Merge_chunk 对应数据块中剩下的记录进行分组计数,因为是一个数据块内部的记录,就不需要去重了。
前面那个按下不表的问题也该有下文了:
因为对磁盘文件多个数据块中的记录合并去重时,需要使用字段内容做比较,而 MEMORY 引擎的 HASH 索引中没有保存字段内容,只保存了表中数据行的首地址,这就是 MySQL 选择使用红黑树去重,而没有选择 HASH 的原因。
sum(distinct)、avg(distinct) 也需要去重,但是和 count(distinct) 不一样的地方在于:sum(distinct)、avg(distinct) 只会对整数、浮点数求和或求平均数,并且只能有一个参数,需要的内存空间比较小,这意味着 sum(distinct)、avg(distinct) 去重时不需要用磁盘临时表。
因此,对于 sum(distinct)、avg(distinct) 来说,只会选择
使用红黑树去重,并且也不会创建一个空的 MEMORY 临时表,这两点和 count(distinct) 不一样。
如果 sum()、avg() 函数参数中的字段不是
整数或浮点数类型的字段,不会报错,字段值都会被转换
为浮点数,然后对浮点数求和或求平均数。
非整数、浮点数类型字段转换为浮点数,和开发语言中的转换逻辑基本相同,对于字符串内容,就是把字符串前面的数字
作为字段的数字值,例如:91 测试
转换为浮点数是 91.0,测试
转换为浮点数是 0.0。
第 2 小节
,介绍了 MEMORY 引擎支持的两种索引结构:HASH 索引、B-TREE 索引。
HASH 索引适用于单值查找多的场景;B-TREE 索引适用于范围查询、需要排好序的记录的场景。
第 3 小节
,以循序渐进的方式介绍了 MySQL 为什么选择使用红黑树实现 count(distinct) 的去重功能。
第 4 小节
,介绍了红黑树单个结点的结构,每个结点包含结点元数据
、结点数据
两类信息。
第 5 小节
,介绍了红黑树占用内存超过最大值之后,会把所有结点数据
写入磁盘文件,然后删除所有结点,保留内存重复使用。
第 6 小节
,以循序渐进的方式介绍了为什么需要合并缓冲区
,以及缓冲区的大小由 tmp_table_size、max_heap_table_size 两个系统变量控制。
第 7 小节
,介绍了磁盘文件中所有数据块合并去重、分组计数的详细过程。合并去重及分组计数分为红黑树写满过
、没写满过
两种情况,处理逻辑不一样。
第 8 小节
,介绍了 sum(distinct)、avg(distinct) 只能用于整数、浮点数求和、求平均数,它们和 count(distinct) 不一样的地方在于:只会选择使用红黑树去重,不需要创建 MEMORY 临时表,更不需要磁盘临时表。
以上就是本文的全部内容了,如果本文对你有所帮助
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/jmxQVH9xKurBBY0bpqQlXQ
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为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 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。