7.2.7 【MySQL】用于分组

2023-09-19 09:38:25

有时候我们为了方便统计表中的一些信息,会把表中的记录按照某些列进行分组。比如下边这个分组查询:

SELECT name, birthday, phone_number, COUNT(*) FROM person_info GROUP BY name, birthday

这个查询语句相当于做了3次分组操作:

1. 先把记录按照 name 值进行分组,所有 name 值相同的记录划分为一组。

2. 将每个 name 值相同的分组里的记录再按照 birthday 的值进行分组,将 birthday 值相同的记录放到一个小分组里,所以看起来就像在一个大分组里又化分了好多小分组。

3. 再将上一步中产生的小分组按照 phone_number 的值分成更小的分组,所以整体上看起来就像是先把记录分成一个大分组,然后把 大分组 分成若干个 小分组 ,然后把若干个 小分组 再细分成更多的 小小分组 。

然后针对那些 小小分组 进行统计,比如在我们这个查询语句中就是统计每个 小小分组 包含的记录条数。如果没有索引的话,这个分组过程全部需要在内存里实现,而如果有了索引的话,恰巧这个分组顺序又和我们的 B+ 树中的索引列的顺序是一致的,而我们的 B+ 树索引又是按照索引列排好序的,这不正好么,所以可以直接使用B+ 树索引进行分组。

和使用 B+ 树索引进行排序是一个道理,分组列的顺序也需要和索引列的顺序一致,也可以只使用索引列中左边的列进行分组。

7.3 回表的代价

上边的讨论对 回表 这个词儿多是一带而过,可能大家没啥深刻的体会,下边我们详细唠叨下。还是用idx_name_birthday_phone_number 索引为例,看下边这个查询:

SELECT * FROM person_info WHERE name > 'Asa' AND name < 'Barlow';

在使用 idx_name_birthday_phone_number 索引进行查询时大致可以分为这两个步骤:

1. 从索引 idx_name_birthday_phone_number 对应的 B+ 树中取出 name 值在 Asa ~ Barlow 之间的用户记录。

2. 由于索引 idx_name_birthday_phone_number 对应的 B+ 树用户记录中只包含 name 、 birthday 、phone_number 、 id 这4个字段,而查询列表是 * ,意味着要查询表中所有字段,也就是还要包括 country字段。这时需要把从上一步中获取到的每一条记录的 id 字段都到聚簇索引对应的 B+ 树中找到完整的用户记录,也就是我们通常所说的 回表 ,然后把完整的用户记录返回给查询用户。

由于索引 idx_name_birthday_phone_number 对应的 B+ 树中的记录首先会按照 name 列的值进行排序,所以值在 Asa ~ Barlow 之间的记录在磁盘中的存储是相连的,集中分布在一个或几个数据页中,我们可以很快的把这些连着的记录从磁盘中读出来,这种读取方式我们也可以称为 顺序I/O 。根据第1步中获取到的记录的 id 字段的值可能并不相连,而在聚簇索引中记录是根据 id (也就是主键)的顺序排列的,所以根据这些并不连续的 id值到聚簇索引中访问完整的用户记录可能分布在不同的数据页中,这样读取完整的用户记录可能要访问更多的数据页,这种读取方式我们也可以称为 随机I/O 。一般情况下,顺序I/O比随机I/O的性能高很多,所以步骤1的执行可能很快,而步骤2就慢一些。所以这个使用索引 idx_name_birthday_phone_number 的查询有这么两个特点:

  • 会使用到两个 B+ 树索引,一个二级索引,一个聚簇索引。
  • 访问二级索引使用 顺序I/O ,访问聚簇索引使用 随机I/O 。

需要回表的记录越多,使用二级索引的性能就越低,甚至让某些查询宁愿使用全表扫描也不使用 二级索引 。比方说 name 值在 Asa ~ Barlow 之间的用户记录数量占全部记录数量90%以上,那么如果使用idx_name_birthday_phone_number 索引的话,有90%多的 id 值需要回表,这不是吃力不讨好么,还不如直接去扫描聚簇索引(也就是全表扫描)。

那什么时候采用全表扫描的方式,什么时候使用采用 二级索引 + 回表 的方式去执行查询呢?这个就是传说中的查询优化器做的工作,查询优化器会事先对表中的记录计算一些统计数据,然后再利用这些统计数据根据查询的条件来计算一下需要回表的记录数,需要回表的记录数越多,就越倾向于使用全表扫描,反之倾向于使用 二级索引 + 回表 的方式。当然优化器做的分析工作不仅仅是这么简单,但是大致上是个这个过程。一般情况下,限制查询获取较少的记录数会让优化器更倾向于选择使用 二级索引 + 回表 的方式进行查询,因为回表的记录越少,性能提升就越高,比方说上边的查询可以改写成这样:

SELECT * FROM person_info WHERE name > 'Asa' AND name < 'Barlow' LIMIT 10;

添加了 LIMIT 10 的查询更容易让优化器采用 二级索引 + 回表 的方式进行查询。

对于有排序需求的查询,上边讨论的采用 全表扫描 还是 二级索引 + 回表 的方式进行查询的条件也是成立的,比方说下边这个查询:

SELECT * FROM person_info ORDER BY name, birthday, phone_number;

由于查询列表是 * ,所以如果使用二级索引进行排序的话,需要把排序完的二级索引记录全部进行回表操作,这样操作的成本还不如直接遍历聚簇索引然后再进行文件排序( filesort )低,所以优化器会倾向于使用 全表扫描 的方式执行查询。如果我们加了 LIMIT 子句,比如这样:

SELECT * FROM person_info ORDER BY name, birthday, phone_number LIMIT 10;

这样需要回表的记录特别少,优化器就会倾向于使用 二级索引 + 回表 的方式执行查询。

7.3.1 覆盖索引

为了彻底告别 回表 操作带来的性能损耗,我们建议:最好在查询列表里只包含索引列,比如这样:

SELECT name, birthday, phone_number FROM person_info WHERE name > 'Asa' AND name < 'B

因为我们只查询 name , birthday , phone_number 这三个索引列的值,所以在通过idx_name_birthday_phone_number 索引得到结果后就不必到 聚簇索引 中再查找记录的剩余列,也就是country 列的值了,这样就省去了 回表 操作带来的性能损耗。我们把这种只需要用到索引的查询方式称为 索引覆盖 。排序操作也优先使用 覆盖索引 的方式进行查询,比方说这个查询:

SELECT name, birthday, phone_number FROM person_info ORDER BY name, birthday, phone_nu

虽然这个查询中没有 LIMIT 子句,但是采用了 覆盖索引 ,所以查询优化器就会直接使用idx_name_birthday_phone_number 索引进行排序而不需要回表操作了。

当然,如果业务需要查询出索引以外的列,那还是以保证业务需求为重。但是我们很不鼓励用 * 号作为查询列表,最好把我们需要查询的列依次标明。

7.4 如何挑选索引

上边我们以 idx_name_birthday_phone_number 索引为例对索引的适用条件进行了详细的唠叨,下边看一下我们在建立索引时或者编写查询语句时就应该注意的一些事项。

7.4.1 只为用于搜索、排序或分组的列创建索引

也就是说,只为出现在 WHERE 子句中的列、连接子句中的连接列,或者出现在 ORDER BY 或 GROUP BY 子句中的列创建索引。而出现在查询列表中的列就没必要建立索引了:

SELECT birthday, country FROM person name WHERE name = 'Ashburn';

像查询列表中的 birthday 、 country 这两个列就不需要建立索引,我们只需要为出现在 WHERE 子句中的 name列创建索引就可以了。

7.4.2 考虑列的基数

列的基数 指的是某一列中不重复数据的个数,比方说某个列包含值 2, 5, 8, 2, 5, 8, 2, 5, 8 ,虽然有 9 条记录,但该列的基数却是 3 。也就是说,在记录行数一定的情况下,列的基数越大,该列中的值越分散,列的基数越小,该列中的值越集中。这个 列的基数 指标非常重要,直接影响我们是否能有效的利用索引。假设某个列的基数为 1 ,也就是所有记录在该列中的值都一样,那为该列建立索引是没有用的,因为所有值都一样就无法排序,无法进行快速查找了~ 而且如果某个建立了二级索引的列的重复值特别多,那么使用这个二级索引查出的记录还可能要做回表操作,这样性能损耗就更大了。所以结论就是:最好为那些列的基数大的列建立索引,为基数太小列的建立索引效果可能不好。

7.4.3 索引列的类型尽量小

我们在定义表结构的时候要显式的指定列的类型,以整数类型为例,有 TINYINT 、 MEDIUMINT 、 INT 、 BIGINT这么几种,它们占用的存储空间依次递增,我们这里所说的 类型大小 指的就是该类型表示的数据范围的大小。能表示的整数范围当然也是依次递增,如果我们想要对某个整数列建立索引的话,在表示的整数范围允许的情况下,尽量让索引列使用较小的类型,比如我们能使用 INT 就不要使用 BIGINT ,能使用 MEDIUMINT 就不要使用INT ~ 这是因为:

  • 数据类型越小,在查询时进行的比较操作越快
  • 数据类型越小,索引占用的存储空间就越少,在一个数据页内就可以放下更多的记录,从而减少磁盘 I/O 带来的性能损耗,也就意味着可以把更多的数据页缓存在内存中,从而加快读写效率。

这个建议对于表的主键来说更加适用,因为不仅是聚簇索引中会存储主键值,其他所有的二级索引的节点处都会存储一份记录的主键值,如果主键适用更小的数据类型,也就意味着节省更多的存储空间和更高效的 I/O 。

更多推荐

Java下部笔记

目录一.双列集合1.Map2.Map的遍历方式3.可变参数4.Collection中的默认方法5.不可变集合(map不会)二.Stream流1.获取stream流2.中间方法3.stream流的收集操作4.方法引用1.引用静态方法2.引用成员方法3.引用构造方法4.使用类名引用成员方法5.引用数组的构造方法三.异常四.

【Java 基础篇】优雅处理文本数据:Java字符流详解

当涉及字符流时,Java提供了一组类来处理字符数据的输入和输出。字符流比字节流更适合处理文本文件,因为它们可以正确处理字符编码,而不仅仅是字节。在本篇博客中,我们将详细介绍Java字符流的各个方面,包括基本的字符输入输出,字符编码,字符流的使用注意事项以及一些高级话题。1.什么是字符流?字符流是用于处理字符数据的Jav

Java中的异常基础知识

目录什么是异常?1.算术异常2.数组越界异常3.空指针异常4.输入不匹配异常Java异常体系异常的处理防御式编程:事后认错异常处理流程自定义异常什么是异常?在Java中,将程序执行过程中发生的不正常行为称为异常1.算术异常publicstaticvoidmain(String[]args){System.out.pri

数据结构Map-Set和哈希表

目录概念模型MapMap的常用方法对于Map的总结SetSet的常见方法关于Set的总结哈希表概念冲突概念哈希函数设计原则常见的哈希函数1.直接定制法(常用)2.除留余数法(常用)3.平方取中法4.折叠法5.随机数法6.数学分析法冲突避免-负载因子调节冲突-解决闭散列线性探测二次探测开散列/哈希桶三个例题理解Map和S

如何在JavaScript中实现链式调用(chaining)?

聚沙成塔·每天进步一点点⭐专栏简介⭐JavaScript中的链式调用⭐示例⭐写在最后⭐专栏简介前端入门之旅:探索Web开发的奇妙世界记得点击上方或者右侧链接订阅本专栏哦几何带你启航前端之旅欢迎来到前端入门之旅!这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发

Linux入门教程||Linux 文件与目录管理

我们知道Linux的目录结构为树状结构,最顶级的目录为根目录/。其他目录通过挂载可以将它们添加到树中,通过解除挂载可以移除它们。在开始本教程前我们需要先知道什么是绝对路径与相对路径。绝对路径:路径的写法,由根目录/写起,例如:/usr/share/doc这个目录。相对路径:路径的写法,不是由/写起,例如由/usr/sh

华为云云耀云服务器L实例评测|服务器反挖矿防护指南

前言本文为华为云云耀云服务器L实例测评文章,测评内容是云耀云服务器L实例反挖矿防护指南系统配置:2核2G3MCentOS7.9之前的文章中『一文教你如何防御数据库恶意攻击』,我们讲到黑客如何通过攻击数据库来获取权限,以及我们需要如何处理防护云耀云服务器L实例接下来我们将要讲述另外一种黑客攻击的手段——挖矿,本文将从黑客

Acwing算法心得——猜测短跑队员的速度(重写比较器)

大家好,我是晴天学长,今天的算法题用到了比较器的知识,是经常会用到的一个知识点,常见与同种数据的排序,需要的小伙伴请自取哦!如果觉得写的不错的话,可以点个关注哦,后续会继续更新的。💪💪💪1)猜测短跑队员的速度一个短跑运动员在一个数轴上跑步。他的奔跑速度是恒定的,但是奔跑方向可能会不断发生改变,有时朝数轴正方向,有

学习潘海东博士的《潮汐调和分析原理和应用》和调和分析软件S_Tide

潘海东博士在B站(用户名:ocean_tide)分享了他的电子书《潮汐调和分析原理和应用》,以及他开发的潮汐调和分析工具包S_Tide,非常厉害。水文同事在进行潮汐预报的时候,会经常说到调和分析和调和常数,博主一听到这些名词就懵圈,不明所以。而《潮汐调和分析原理和应用》开篇就讲潮汐调和分析求解分潮振幅和迟角的过程本质就

【每日随笔】关于 “ 终身学习 “ ① ( 各阶段学习过程 | 扫盲教育与选拔教育阶段 | 研究生阶段 | 终身学习阶段 )

文章目录一、学习的各个阶段1、扫盲教育与选拔教育阶段2、研究生阶段3、终身学习阶段4、终身学习内容推荐一、学习的各个阶段1、扫盲教育与选拔教育阶段小学六年和初中三年是扫盲教育,也就是九年义务教育,这是为了扫盲用的,初中毕业,就可以成为一个合格的劳动力;高中三年和大学四年是选拔教育,是用来选拔人才的,在之前知识的基础上,

解锁 zkSync Era:开创全新的 Layer 2 扩展时代

作者:stella@footprint.network数据来源:zkSyncDashboard在解决以太坊扩展性问题方面,Layer2解决方案备受关注。这些解决方案旨在通过引入Rollups,StateChannels或NestedBlockchains等技术来克服Layer1的局限性。在Layer2扩展领域,围绕Op

热文推荐