对数据库稍有使用经验的人,排序与分页是非常常见的需求。MySQL作为一款常用的关系型数据库,对于排序和分页也提供了相应的语法和底层实现,但是实际上我也见到过很多人抱怨:排序分页后性能大减。这是为什么,又该如何解决呢?今天我们就来细说Mysql数据库的排序与分页
📕作者简介:战斧,从事金融IT行业,有着多年一线开发、架构经验;爱好广泛,乐于分享,致力于创作更多高质量内容
📗本文收录于 mysql 专栏,有需要者,可直接订阅专栏实时获取更新
📘高质量专栏 云原生、RabbitMQ、Spring全家桶 等仍在更新,欢迎指导
📙Zookeeper Redis dubbo docker netty等诸多框架,以及架构与分布式专题即将上线,敬请期待
ORDER BY
语句用于对查询结果进行排序,常见的语法为:
SELECT * FROM table_name ORDER BY column_name [ASC|DESC];
其中的 [ASC|DESC] 代表是要升序还是降序,如果不填,则默认为ASC - 升序。
而如果需要按照多个列进行排序,可以在ORDER BY子句中指定多个列,例如:
SELECT * FROM table_name ORDER BY column1, column2 [ASC|DESC];
举个例子,如果我们想把用户查出来,我们需要按照姓名(name)首字母的字母顺序进行升序查出,如果姓名相同,则按照年龄(age)进行降序,则可以使用以下SQL:
SELECT * FROM user ORDER BY name ASC, age DESC;
当然,在排序过程中,NULL值的处理是一个重要的问题。MySQL默认情况下将NULL值认定为最小,所以如果是升序的话,其NULL值排在首位,而使用降序时,其排在最后。
如果需要将NULL值排在前面,可以使用IS NULL或IS NOT NULL进行判断。还是以上面为例:
这种在原排序字段前,使用 case when XXX is null then Y else Z end
的句式,本质上就是增加一个虚拟字段并先按虚拟字段排序。在我们的SQL中,判断如果 name 字段为 NULL, 则设定该虚拟字段为1,否则虚拟字段为0,然后因为MYSQL默认的升序,这样当 name 字段为 NULL 时,虚拟字段为1,就会被排到最后了。
如果你希望按照指定逻辑进行排序,而不是以数据库自己的升序或降序排序,你可以使用上面我们说的 CASE
表达式来实现
SELECT *
FROM your_table
ORDER BY
CASE
WHEN name IS NULL THEN 0 -- 姓名为NULL的记录排在最前面
ELSE 1
END,
name,
CASE
WHEN age IS NULL THEN 0 -- 在姓名相同的记录中,年龄为NULL的记录排在最前面
ELSE 1
END,
age DESC;
如果函数支持,你也可以使用对应的函数来进行处理,假如我们想以name字段的第二个字符进行升序排序,可以使用SUBSTRING函数来获取name字段的第二个字符,如下:
这里,我们使用SUBSTRING函数来截取name字段的第二个字符。第一个参数是字段名,第二个参数是起始位置(从1开始),第三个参数是截取的字符数。
对于分页的实现,MySQL提供了LIMIT
语句,可以限制查询结果的行数,例如:
SELECT * FROM table_name LIMIT offset, count;
其中 offset 为起始行的偏移量,你可以理解为需要跳过的行数,count表示要返回的行数。offset 如果不填则默认是0, 也即不跳过任何数据。
-- 返回表中的前5行记录:
SELECT *
FROM your_table
LIMIT 5;
-- 返回表中的第6到第10行记录
SELECT *
FROM your_table
LIMIT 5, 5;
请注意,LIMIT语句中的索引是从0开始的,而不是从1开始。因此,LIMIT 5, 5表示跳过前5行,即从第6行开始,返回5行记录。
Mysql会根绝字段以及表的情况,采用不同的排序手段,最常见的是下面这几种:
文件排序
MySQL会使用磁盘临时文件来存储查询结果,并通过排序算法对文件中的数据进行排序。其原理是将排序字段的数据写入临时文件中,并使用外部排序算法对文件中的数据进行排序。外部排序算法通常包括多路归并排序等,它们将文件分成多个块进行排序和合并,最终得到有序的结果。比如我们现在的name字段并不是索引,当我们使用这样一个排序语句时,其采用的就是 filesort ,即文件排序:
索引排序
:如果查询结果可以使用索引进行排序,则可以通过索引直接按照排序顺序返回结果,而不需要进行额外的排序操作。下面我们为name字段加上一个索引,并指定只查询name字段,我们就能看到使用了索引排序,如下:
这里指定只查询name字段,是使用了覆盖索引的特性,如果我们还是使用 * 来查所有字段,那么用的还是文件排序,
这是因为innoDB的非聚簇索引的特性,name并不是主键,所以如果你要查其他字段,使用完索引后,还得回到主键来进行查询。在数据量比较小的情况下,Mysql觉得不如直接通过主键全查,然后内存排序来的快
临时表排序
当MySQL无法直接利用索引的有序信息,MySQL会在内存中创建一个临时表,将查询结果放入临时表中,并通过排序算法对临时表进行排序。比如
一般来说,文件排序直接在磁盘上进行排序,而临时表排序在内存中进行排序,所以文件排序一般是最慢的 ,当然,最快的肯定还是索引排序,所以我们对于常用的排序字段最好是要有索引。
MySQL首先会执行查询语句,获取满足条件的全部结果集,为了实现分页,MySQL会对返回的结果集进行处理。它会根据LIMIT子句指定的每页行数,以及OFFSET子句指定的偏移量,确定要返回的起始行和结束行。MySQL会通过扫描结果集中的行,跳过OFFSET指定的偏移量行,然后返回LIMIT指定的行数。
需要注意的是,当MySQL执行分页查询时,如果结果集很大,可能会导致性能问题,因为MySQL需要扫描整个结果集并计算偏移量。假如我们有百万级别的数据,都是显示5条数据,以下两个语句却会有很大的时间差异
select * from user order by name limit 5
select * from user order by name limit 1000000 , 5
排序操作需要耗费大量的CPU和内存资源,如果排序的数据量较大,会导致性能下降。尤其是在排序的列上没有创建索引的情况下,排序操作会更加耗时。如果一定要执行排序,可以考虑以下解决方案:
组合索引
,尽量覆盖到这些字段所谓分页不稳定,在我们这里表现为即使没有改动数据,但在翻页的时候,不同的页仍然出现了重复数据或者遗漏数据的情况。而这主要是排序不稳定
引起的
通俗的说,当使用ORDER BY进行排序时,如果排序字段中有相同的值,会导致分页查询的结果不稳定。在相同值的情况下,MySQL的排序算法并没有固定的顺序,可能会导致不同的查询结果。产生这种现象的原因,主要还是我们上面提到了排序 + limit 的优化导致的,当同时执行排序与limit时,实际上mysql不会对所有数据排序后再分页,而是使用堆排序
进行TopK的查找。而堆排序就是不稳定的,那最终导致了分页不稳定。
解决方案其实很简单,就是让排序不会有相同的值,比如我们想以name排序,但name可能有重名的,我们想保持稳定,可以在后面加上主键的排序
分页操作同样会对性能产生一定的影响。我们在前面说过,MySQL包含了Server层、存储引擎层。而limit其实就是在Server层执行的分页,正因如此,MySQL就会读取并排序整个结果集,然后返回指定范围的数据,导致对于大型表或复杂查询可能会耗费较多的时间和资源。为了优化LIMIT分页查询的性能,可以考虑以下几个方面:
通过本文的介绍,我们了解了MySQL数据库的排序与分页的语法和底层实现方式,并探讨了在何种情况下对性能有较大影响。针对性能问题,我们也提出了相应的解决方案。在实际的开发中,合理使用排序和分页操作,可以提升数据库查询的性能,我们将在后续对Mysql做更细致的分析。