数据库核心操作解析:order by、group by、join、union 排序分组连接合并原理详析...

一、Order 排序实现方式
1. 常规排序

在数据库操作里,当使用ORDER BY对查询结果进行排序时,常规排序是一种常见的处理方式。下面以

cs 复制代码
select col1,col2,col3 from t1 where id = 'xx' order by col2;

为例,详细介绍其排序流程:

步骤 操作详情
1 从表 t1 中获取所有满足 WHERE 条件(id = 'xx')的记录。
2 对每条满足条件的记录,将记录的主键与排序键(id, col2)取出,并放入sort_buffer缓冲区
3 如果sort_buffer能够容纳所有满足条件的(id, col2)对,就直接进行内存排序;反之,当 sort_buffer 满了之后,需要采用快速排序算法对其中的数据进行排序,并将排序结果固化到磁盘临时文件中。
4 如果在排序过程中产生了磁盘临时文件,需要利用归并排序算法,确保临时文件中的记录是有序的。
5 循环执行上述步骤 2 - 4,将所有满足条件的记录排序。
6 扫描排好序的(id, col2)对,然后依据主键 id 回表,去表中捞取 SELECT 语句需要返回的字段(col1, col2, col3)。
7 返回结果集给客户端。

从上述流程可以看出,是否使用文件排序主要取决于缓冲区 sort_buffer能否容纳需要排序的(id,col2)对,而sort_buffer的大小由sort_buffer_size参数控制。并且,一次常规排序需要进行两次 I/O 操作:第一次是捞取(id,col2),第二次是回表捞取(col1,col2,col3)。由于返回的结果集是按照col2排序的,所以id是乱序的,通过乱序的id去捞取(col1,col2,col3)时会产生大量的随机 I/O,这会在一定程度上影响排序的性能。

所以,数据库对于第二次 I/O 操作有一个优化方法:在捞取数据之前,先将 id 进行排序,并把排序后的 id 放入缓冲区 read_rnd_buffer,然后按照有序的 id 去捞取记录,这样就可以将随机 I/O 转换为顺序 I/O。

read_rnd_buffer 主要用于在使用行指针排序之后,将随机读转换为顺序读。

2. sort_buffer 与排序优化

数据库在进行排序时,使用内存缓冲区sort_buffer ,数据库会把需要排序的数据先存放到这里,然后进行排序操作。如果排序操作能够仅在 sort_buffer 中完成,就可以避免使用临时表,从而提高排序的效率。

然而,如果需要排序的数据量过大,超出了 sort_buffer 的容量,数据库就会借助磁盘文件来进行归并排序。为了避免走磁盘排序,可以调大sort_buffer_size 参数来增大 sort_buffer 的容量,让它能够容纳更多的数据,避免使用磁盘临时文件。

排序优化方式

常规排序除了排序本身的开销外,还需要额外的两次 I/O 操作。为了减少这些开销,前面说过数据库提供了一种优化的排序方式。这种优化方式与常规排序的主要区别在于,放入sort_buffer的不是(id, col2),而是(col1, col2, col3),也就是 SELECT 语句需要返回的全量字段。这样做的好处是可以避免一次回表操作,从而减少 I/O 开销。

不过,这种优化方式有一个前提条件:MySQL 提供了参数 max_length_for_sort_data,只有当SELECT 语句需要返回的字段的长度总和小于 max_length_for_sort_data 时,才能使用这种优化排序方式;否则,就只能采用常规排序方式。

下面用一个表格来总结 sort_buffer 相关的参数和优化要点:

参数 / 要点 说明
sort_buffer_size 控制 sort_buffer 的大小,可根据数据量调整,以减少磁盘临时文件的使用。
max_length_for_sort_data 决定是否能采用优化排序方式的阈值,SELECT 语句需要返回字段长度总和小于该值时可优化。
优化排序思路 将全量返回字段放入 sort_buffer,避免一次回表,减少 I/O 开销。
3. 其他排序相关要点

避免排序操作

当我们使用 ORDER BY 进行排序时,数据库通常会使用 文件排序(Using filesort)机制。但在某些场景下,我们可以显式指定 ORDER BY NULL 来告诉数据库不需要进行排序,从而消除排序操作带来的额外开销。

索引与排序

ORDER BYGROUP BYDISTINCT 是按照索引进行归类,并且 SELECT 字段不需要回表(即可以直接从索引中获取所需数据),那么数据库就可以直接通过索引扫描来完成排序操作,这种方式的效率非常高。

例如,有一个表 employees,包含字段 idnamedepartment,并且在 department 字段上建立了索引。当我们执行

sql 复制代码
SELECT department, COUNT(*) FROM employees GROUP BY department ORDER BY department;

时,department 索引能满足查询需求,数据库就可以直接利用索引进行排序和分组操作,而无需额外的排序过程。

临时表与排序

临时表在数据库操作中也有重要作用,UNIONGROUP BY 操作是临时表的典型使用场景。在使用 GROUP BY 进行分组统计时,数据库可能会创建临时表来存储中间结果。

为了消除临时表带来的额外开销,我们可以对 GROUP BY 列添加索引。这样数据库在进行分组操作时,就可以利用索引来快速定位和分组数据,减少临时表的使用。对于大结果集,可以调大 SQL_BIG_RESULT 来优化临时表的使用。

内存临时表有一个默认的大小限制,通常是 16M。如果临时表的数据量超过了 tmp_table_size 参数设置的值,内存临时表就会转成磁盘临时表。磁盘临时表的读写速度相对较慢,可能会影响查询性能,所以在处理大数据量时,可以调整 tmp_table_size 参数。

4. 优先队列排序

在处理 Order by limit M, N 这类语句时,可以使用优先队列排序,它采用堆排序算法实现。堆排序的特性正好适合解决 limit M, N 排序问题,虽然仍然需要所有元素参与排序,但只需要N个元组的 sort_buffer 空间,基本不会出现因为 sort_buffer 空间不足而需要使用临时文件进行归并排序的情况。

在升序排序时,使用大顶堆,最终堆中的元素组成了最小的 N 个元素;在降序排序时,使用小顶堆,最终堆中的元素组成了最大的 N 个元素。

需要注意的是,堆排序是非稳定排序(对于相同的 key 值,无法保证排序后与排序前的位置一致),这可能会导致分页重复的现象。为了避免这个问题,可以在排序条件中加上唯一值,比如主键id,确保参与排序的 key 值唯一。例如,可将 SQL 写成:

sql 复制代码
select * from t order by time,id asc limit 0,3;
二、Group by

Group by一般会用临时表进行统计。

cs 复制代码
select country, count(*) as num from t group by country;

执行流程如下:

  1. 创建内存临时表,临时表包含两个字段 countrynum

  2. 全表扫描表t 的记录,依次取出``country = 'X' 的记录。

  3. 判断临时表中是否有 country = 'X' 的行,没有就插入一个记录(X, 1)。

  4. 如果临时表中有 country = 'X' 的行,就将这一行的 num 值加 1。

  5. 遍历完成后,再根据字段 country 做排序,得到结果集返回给客户端。如果不需要排序,可以使用 order by null

1. Group by + where/having 的区别

group by + where:

cs 复制代码
select country, count(*) as num from t where age > 18 group by country;

执行流程如下:

  1. 创建内存临时表,临时表包含两个字段 countrynum

  2. 扫描索引 age,找到年龄大于 18 的主键 ID

  3. 根据主键 ID,回表找到 country = 'X'

  4. 判断临时表中是否有 country = 'X' 的行,没有就插入一个记录(X, 1)。

  5. 如果临时表中有 country = 'X' 的行,就将这一行的 num 值加 1。

  6. 最后根据字段 country 做排序,得到结果集返回给客户端。

group by + having

cs 复制代码
select country, count(*) as num from t group by country having num >= 2;

having 称为分组过滤条件,它可以对返回的结果集操作。

同时有 where 和 having

cs 复制代码
select country, count(*) as num from t where age > 18 group by country having num >= 2;

执行流程如下:

1.执行 where 子句查找符合年龄大于 19 的员工数据;

2.group by 子句对员工数据,根据国家分组;

3.对 group by 子句形成的国家组,运行聚集函数在临时表计算每一组的员工数量值;

4.最后用 having 子句选出员工数量大于等于 3 的国家组;

因为排序会用到临时表,如果数据量比较大时会用到磁盘临时表,这种情况可以调大 tmp_table_size

三、Join

对被驱动表的关联字段建立索引,这样每次搜索只需要在辅助索引树上扫描一行就行了,性能比较高。

扫描次数 = 外层表记录数 * 内层表索引深度。

先使用子查询把驱动表执行排序 order、筛选 limit,去掉多余数据,然后再与被驱动表做 join,可以减少对比的数据。

1. Index Nested-Loop Join(NLJ)

如果被驱动表上有索引,可以利用索引类似嵌套查询。

cs 复制代码
select * from t1 join t2 on (t1.a = t2.a);

从驱动表 t1 取数据时,可以不一行行取,批量取,放入内存 join_buffer,然后排序,然后再跟被驱动表t2 的索引进行比较。这里a字段上需要有索引。

2. Block Nested-Loop Join(BNL)

当被驱动表用于判断的列没有索引时,就是 BNL(Block Nested-Loop Join),会用到 join_buffer。可以增大 join_buffer_size 调节 buffer 大小,减少扫描次数。

如果表t2的比较字段无索引,那么会使用BNL取数,流程为:把表t1(小表,驱动表)的数据分段读入内存缓冲区join_buffer中,扫描表t2,把表t2中的每一行取出来,跟join_buffer中的数据做对比,满足join条件的,作为结果集的一部分返回。循环将表t1的数据放入缓冲区,直至对比完成。
cs 复制代码
select * from t1 straight_join t2 on (t1.a = t2.b);
这里a 字段有索引,b 字段无索引。
3. 驱动表的选择

如果要使用 join,使用小表作为驱动表。小表是指在过滤完成之后,计算参与过滤 join 条件的各个字段的总数据量,数据量小的那个表。

4. 优化方法

NLJ

可以使用缓冲区 join_buffer,根据索引 a 一次性捞取足够数量的主键 id,然后将主键 id 回主表查询。

BNL

在原表上对查找字段加索引;如果被驱动表特别大加索引不划算,可以建立临时表,将where满足条件的数据单独捞取出来,比如如下sql:

cs 复制代码
select * from t1 join t2 on (t1.b = t2.b) where t2.b >= 1 and t2.b <= 2000;

如果表t2数据量很大,而where条件过滤后满足条件的数据量不大,可以将过滤后的数据放入临时表tmp_t中,给临时表的b字段加上索引,然后再将临时表和t1join 操作:

cs 复制代码
create temporary table tmp_t(id int primary key, a int, b int, index(b)); --b字段建索引insert into tmp_t select * from t2 where b >= 1 and b <= 2000;select * from t1 join tmp_t on (t1.b = tmp_t.b);

其次,对于大表还可以在业务层使用``hash进行快速查找:

  1. select * from t1 where ...; --获取驱动表表t1``的满足条件数据,存入``hash map``。

2. select * from t2 where b >= 1 and b <= 2000; --获取表 t2 中满足条件的 X 行数据,返回给业务层。

  1. 对这 X 行数据,从 hash map 中查找,返回符合条件的结果集。

四、Union

union:对两个结果集进行并集操作,不包括重复行,相当于 distinct,同时进行默认规则的排序。

union all:对两个结果集进行并集操作,包括重复行,即所有的结果全部显示,不管是不是重复。

union 会自动将完全重复的数据去除掉,也就是两行完全相同会去掉;union all 会保留那些完全重复的数据。union all只是合并查询结果,并不会进行去重和排序操作,因此在没有去重的需求下,使用 union all 的执行效率要比 union 高。

通过 union 连接的 SQL 它们分别单独取出的列数必须相同。被 union 连接的 sql 子句,单个子句中不用写 order by,因为不会有排序的效果。但可以对最终的结果集进行排序,例如:

SQL 语句 排序效果
(select id,name from A order by id) union all (select id,name from B order by id); 没有排序效果
(select id,name from A ) union all (select id,name from B ) order by id; 有排序效果

综上所述,order bygroup byjoinunion 是数据库中非常重要的操作,理解它们的原理和优化方法,能够帮助我们写出更高效的 SQL 语句,提升数据库的性能。

相关推荐
一 乐12 分钟前
民宿|基于java的民宿推荐系统(源码+数据库+文档)
java·前端·数据库·vue.js·论文·源码
美林数据Tempodata2 小时前
大模型驱动数据分析革新:美林数据智能问数解决方案破局传统 BI 痛点
数据库·人工智能·数据分析·大模型·智能问数
野槐2 小时前
node.js连接mysql写接口(一)
数据库·mysql
Zzzone6832 小时前
PostgreSQL日常维护
数据库·postgresql
chxii2 小时前
1.13使用 Node.js 操作 SQLite
数据库·sqlite·node.js
冰刀画的圈2 小时前
修改Oracle编码
数据库·oracle
这个胖子不太裤3 小时前
Django(自用)
数据库·django·sqlite
麻辣清汤3 小时前
MySQL 索引类型及其必要性与优点
数据库·mysql
2501_915374354 小时前
Neo4j 图数据库安装教程(2024最新版)—— Windows / Linux / macOS 全平台指南
数据库·windows·neo4j
it-搬运工4 小时前
3.图数据Neo4j - CQL的使用
数据库·neo4j