一、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 BY
、GROUP BY
或 DISTINCT
是按照索引进行归类,并且 SELECT
字段不需要回表(即可以直接从索引中获取所需数据),那么数据库就可以直接通过索引扫描来完成排序操作,这种方式的效率非常高。
例如,有一个表 employees
,包含字段 id
、name
、department
,并且在 department
字段上建立了索引。当我们执行
sql
SELECT department, COUNT(*) FROM employees GROUP BY department ORDER BY department;
时,department
索引能满足查询需求,数据库就可以直接利用索引进行排序和分组操作,而无需额外的排序过程。
临时表与排序
临时表在数据库操作中也有重要作用,UNION
和 GROUP 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;
执行流程如下:
-
创建内存临时表,临时表包含两个字段
country
和num
。 -
全表扫描表t
的记录,依次取出``country = 'X'
的记录。 -
判断临时表中是否有
country = 'X'
的行,没有就插入一个记录(X
, 1)。 -
如果临时表中有
country = 'X'
的行,就将这一行的num
值加 1。 -
遍历完成后,再根据字段
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;
执行流程如下:
-
创建内存临时表,临时表包含两个字段
country
和num
。 -
扫描索引
age
,找到年龄大于 18 的主键ID
。 -
根据主键
ID
,回表找到country = 'X'
。 -
判断临时表中是否有
country = 'X'
的行,没有就插入一个记录(X
, 1)。 -
如果临时表中有
country = 'X'
的行,就将这一行的num
值加 1。 -
最后根据字段
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
字段加上索引,然后再将临时表和t1
做join
操作:
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
进行快速查找:
- select * from t1 where ...; --获取驱动表表
t1``的满足条件数据,存入``hash map``。
2. select * from t2 where b >= 1 and b <= 2000; --获取表 t2
中满足条件的 X 行数据,返回给业务层。
- 对这 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 by
、group by
、join
和 union
是数据库中非常重要的操作,理解它们的原理和优化方法,能够帮助我们写出更高效的 SQL 语句,提升数据库的性能。