数据库核心操作解析: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 语句,提升数据库的性能。

相关推荐
lhdz_bj32 分钟前
数据库开发常识(10.6)——SQL性能判断标准及索引误区(1)
sql·oracle·数据库开发·索引·性能·标准·误区
smart_ljh1 小时前
k8s二进制集群之ETCD集群证书生成
数据库·k8s·etcd
look_outs3 小时前
PyQt4学习笔记1】使用QWidget创建窗口
数据库·笔记·python·学习·pyqt
漫步者TZ4 小时前
Starrocks 对比 Clickhouse
数据库·starrocks·clickhouse
s_little_monster4 小时前
【Linux】进程状态和优先级
linux·服务器·数据库·经验分享·笔记·学习·学习方法
呼啦啦啦啦啦啦啦啦4 小时前
【Redis】主从模式,哨兵,集群
数据库·redis·缓存
~时泪~5 小时前
sql主从同步
数据库·sql
予早5 小时前
SQLModel入门
数据库·orm·sqlmodel
ThinkStu5 小时前
2025年时序数据库发展方向和前景分析
数据库·时序数据库