MySQL带排序的分页查询优化

Posted loveletters

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MySQL带排序的分页查询优化相关的知识,希望对你有一定的参考价值。

MySQL带排序的分页查询优化

需求

在日常开发中,经常会遇到这么一种情况,一张表的数据量较大(500万左右)的时候,对其进行分页查询的时候,在分页比较深的情况下,查询效率会急剧下降。对于这种情况,我们需要做一些分页查询的优化。

准备

创建脚本

CREATE TABLE student (
    id INT NOT NULL AUTO_INCREMENT COMMENT \'ID\',
    student_number VARCHAR(10) NOT NULL COMMENT \'学号\',
    name VARCHAR(50) NOT NULL COMMENT \'姓名\',
    score DECIMAL(10,2) NOT NULL COMMENT \'分数\',
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT \'创建时间\',
    update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT \'更新时间\',
    remark TEXT COMMENT \'备注\',
    PRIMARY KEY (id)
) COMMENT=\'学生信息表\';

执行存储过程

我们通过存储过程的方式往student表中插入500万条数据

-- 创建存储过程
DELIMITER $$
CREATE PROCEDURE insert_student_data()
BEGIN
  DECLARE i INT DEFAULT 1;
  WHILE (i <= 5000000) DO
    INSERT INTO student (student_number, name, score)
    VALUES (CONCAT(\'S\', LPAD(i, 7, \'0\')), CONCAT(\'张三\', i), ROUND(RAND() * 100, 2));
    SET i = i + 1;
  END WHILE;
END$$
DELIMITER ;

-- 执行存储过程
CALL insert_student_data();

SQL优化

假设我们需要查询 学号,姓名,分数并且按照分数倒序分页查询查询,每页20条记录。

# 查询时间1.6s
SELECT  student_number,name,score from student ORDER BY score desc  limit 0,20;

# 查询时间2.3s
SELECT  student_number,name,score from student ORDER BY score desc  limit 10000,20

# 查询时间2.5s
SELECT  student_number,name,score from student ORDER BY score desc  limit 100000,20

# 查询时间3s
SELECT  student_number,name,score from student ORDER BY score desc  limit 4000000,20;

我们发现查询时间比较慢,并且随着分页的深入查询时候会更加的慢

查询一下执行计划

EXPLAIN SELECT  student_number,name,score from student ORDER BY score desc  limit 0,20;

发现type是ALL,发现并未走索引,随即我们给score字段加上索引再测试一次

ALTER TABLE student add index index_score(score)
# 查询时间0.005
SELECT  student_number,name,score from student ORDER BY score desc  limit 0,20;

# 查询时间0.005
SELECT  student_number,name,score from student ORDER BY score desc  limit 10000,20

# 查询时间2.5s
SELECT  student_number,name,score from student ORDER BY score desc  limit 100000,20

# 查询时间3s
SELECT  student_number,name,score from student ORDER BY score desc  limit 4000000,20;

可以看到在分页较潜的情况下执行的效率很高,但是在分页较深的情况下执行的效率还是一样的

查看执行计划

EXPLAIN SELECT  student_number,name,score from student ORDER BY score desc  limit 0,20;

可以看到在浅分页的情况下是走了索引并且索引的key就是我们刚添加的那个,额外只执行了 Backward-index-scan(8.0新增的,使用倒序索引扫描)

EXPLAIN
SELECT  student_number,name,score from student ORDER BY score desc  limit 4000000,20

然后在深分页的情况下type是ALL 全表扫描,并且额外执行的Using filesort(排序操作)

如果我们强制的让他使用索引查询

# 查询结果27秒
EXPLAIN
SELECT  student_number,name,score from student FORCE INDEX (index_score) ORDER BY score desc  limit 4000000,20

我们会发现强制使用索引的查询时间比全表扫描还要长,这是因为我们使用非覆盖索引进行查询的时候会有一次回表查询。

覆盖索引优化

既然是因为有回表操作使得查询效率变低,那么我们可以使用覆盖索引,让他查询的时候通过辅助索引就可以查询到所有信息,就不用进行回表操作。

添加索引


ALTER TABLE student add index index_score_name_student_number(score,student_number,name)

我们为student表添加一个联合索引,然后再执行深分页查询

# 查询时间0.9s
SELECT  student_number,name,score from student ORDER BY score desc  limit 4000000,20

查询时间显著的缩短了

可以看到因为新添加的索引覆盖了我们需要查询的列,所以不需要进行回表查询,直接走索引即可。

但是 如果我们的查询语句中再新增了一列

SELECT student_number,name,score,create_time from student ORDER BY score desc  limit 4000000,20

因为 create_time字段不再我们的联合索引里面,所以它又将进行全表扫描

延迟关联优化

先通过 where 条件提取出主键,在将该表与原数据表关联,通过主键 id 提取数据行,而不是通过原来的二级索引提取数据行

# 查询时间0.8s
SELECT  a.student_number,a.name,a.score ,a.create_time from student as a ,
(SELECT id from student ORDER BY score desc limit 4000000,20) as b WHERE a.id=b.id

分析下执行计划

首先执行的是id=2 的这条计划,走的是我们之前定义的index_score这个索引,然后执行的是第二条也就是别名为a的表可以看到它的类型为主键索引,最后执行的是第一条,临时结果表,虽然是全表扫描,但是我们可以得知的是这张表的结果只有20条,因为我们limit的就是20条记录,所以查询也很快。

从执行计划中可以看到我们的临时表走的是全表扫描,所以如果我们子查询的临时表中的结果比较多的话,这种方式就不推荐使用了。

书签方式

根据id,score 大于最小值或者小于最大值进行遍历。


# 查询时间 0.005s
SELECT id,student_number,name,score from student
WHERE id < 90000000 and score <=100.00
 ORDER BY score DESC LIMIT 20

查看执行计划

可以看到type为range效率很高

在进行上一页/下一页查询的时候需要前端传递给我们两个参数(第一行/最后一行)的id跟score,然后根据这两个参数再进行条件查询。

缺点是只能进行上一页下一页的查询,不能进行跳页查询。

MySQL百万级数据量分页查询方法及其优化建议

参考技术A offset+limit方式的分页查询,当数据表超过100w条记录,性能会很差。
主要原因是offset limit的分页方式是从头开始查询,然后舍弃前offset个记录,所以offset偏移量越大,查询速度越慢。

比如: 读第10000到10019行元素(pk是主键/唯一键).

使用order by id可以在查询时使用主键索引。
但是这种方式在id为uuid的时候就会出现问题。可以使用where in的方式解决:

带条件的查询:
如果在分页查询中添加了where条件例如 type = 'a’这样的条件,sql变成 :

这种情况因为type没有使用索引也会导致查询速度变慢。但是只添加type为索引查询速度还是很慢,是因为查询的数据量太多了。这个时候考虑添加组合索引,组合索引的顺序要where条件字段在前,id在后,如 (type,id),因为组合索引查询时用到了type索引,而type跟id是组合索引的关系,如果只select id ,那么直接就可以按组合索引返回id,而不需要再进行一次查询去返回id

使用uuid作为主键不仅会带来性能上的问题,在查询时也会遇到问题。

因为在使用select id from table limit 10000,10 查询id数据时,默认是对id进行排序,返回的是排序后的id结果,如果我们想按插入顺序查询结果,这样查询出来的结果就与我们的需求不相符。

聚集索引跟非聚集索引:聚集索引类似与新华字典的拼音,根据拼音搜索到的信息都是连续的,可以很快获取到它前后的信息。非聚集索引类似于部首查询,信息存放的位置可能不在一个区域。对经常使用范围查询的字段考虑使用聚集索引。

InnoDB中索引分为聚簇索引(主键索引)和非聚簇索引(非主键索引),聚簇索引的叶子节点中保存的是整行记录,而非聚簇索引的叶子节点中保存的是该行记录的主键的值。

如果您的表上定义有主键,该主键索引是聚集索引。
如果你不定义为您的表的主键时,MySQL取第一个唯一索引(unique)而且只含非空列(NOT NULL)作为主键,InnoDB使用它作为聚集索引。
如果没有这样的列,InnoDB就自己产生一个这样的ID值,
优先选index key_len小的索引进行count(*),尽量不使用聚簇索引

在没有where条件的情况下,count(*)和count(常量),如果有非聚簇索引,mysql会自动选择非聚簇索引,因为非聚簇索引所占的空间小,如果没有非聚簇索引会使用聚集索引。count(primary key)主键id为聚集索引,使用聚集索引。有where条件的情况下,是否使用索引会根据where条件判断。

以上是关于MySQL带排序的分页查询优化的主要内容,如果未能解决你的问题,请参考以下文章

PHP+MySQL高效的分页方法,如何优化LIMIT,OFFSET进行的分页?

Mysql的分页查询优化

MySQL的分页优化

在mysql 数据库下,基于sql 语言的分页语句

Oracle 分页查询在子查询中使用了 排序和like 会影响效率吗? 怎样优化呢

mysql的分页使用子查询?