MySQL查询百万级数据分页查询优化实验记录

1.业务场景

在项目开发中,经常需要进行统计数据的上报和分析。通常,经过分析后,这些数据将在后台呈现给运营和产品人员,以便他们进行分页查看和分析。其中,一种常见的需求是按日期进行筛选和汇总数据。随着时间的推移,统计数据的数量将逐渐增加,最终可能达到数百万甚至数千万条数据。这种情况下,数据的处理和展示需要一些优化和考虑,以确保系统性能和用户体验。

2.实验数据插入

1.创建 user

sql 复制代码
CREATE TABLE `user`  (
  `id` int(0) UNSIGNED NOT NULL AUTO_INCREMENT,
  `user_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `password` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `age` int(0) UNSIGNED NULL DEFAULT NULL,
  `phone` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `create_time` datetime(0) NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

2.插入一百万条数据

开放了一个插入接口,通过 Apache Bench 发送 100 万次请求来向数据库插入 100 万条数据。命令如下:

shell 复制代码
# -c 表示并发请求数;-n 表示总请求数
ab -c 20 -n 1000000 http://127.0.0.1:8080/api/mysql/insert

大约需要 362 秒。

代码及测试 SQL 链接

3.无索引分页查询耗时

sql 复制代码
-- SQL_NO_CACHE 防止 SQL 查询走缓存
SELECT SQL_NO_CACHE * from `user` WHERE create_time BETWEEN '2000-01-01' AND '2023-12-31' LIMIT 1,10;

不同分页条件下的耗时情况:

LIMIT Time (ms)
1,10 0
100,10 1
1000,10 2
10000,10 9
100000,10 62
200000,10 122
400000,10 418
600000,10 466
800000,10 520
1000000,10 757

上述实验结果是单次实验所得。从表中分析可知,随着数据量的增大,往后页查询所耗时间按理来说会越来越大。

通过 Explain 命令对查询语句进行分析:

sql 复制代码
EXPLAIN
SELECT SQL_NO_CACHE * from `user` WHERE create_time BETWEEN '2000-01-01' AND '2023-12-31' LIMIT 1000000,10;

结果:

从结果中可知走的是全表扫描(ALL),扫描的数据行有 1022908 条,选取的行和读取的行的百分比为 11.11,使用到了 where 过滤。

4.添加索引后分页查询耗时

1.添加索引

sql 复制代码
ALTER TABLE user
ADD INDEX idx_createtime (create_time);

2.查看索引

sql 复制代码
SHOW INDEX FROM user;

3.查询耗时情况

LIMIT Time (ms)
1,10 42
100,10 43
1000,10 57
10000,10 209
100000,10 1502
200000,10 2860
400000,10 5975
600000,10 8492
800000,10 11155
1000000,10 14192

如果需要频繁通过 creae_time 字段进行条件查询,那么为该字段添加索引是必要的。 从上述实验结果可知,添加索引后,其执行时间反而增加。

通过 Explain 命令对查询语句进行分析:

sql 复制代码
EXPLAIN
SELECT SQL_NO_CACHE * from `user` WHERE create_time BETWEEN '2000-01-01' AND '2023-12-31' LIMIT 1000000,10;

从结果中可知走的是索引范围扫描(range),实际使用的索引是 idx_createtime,扫描的数据行有 511454 条;Using index condition 表示查询的列有非索引的列,先判断索引的条件,以减少磁盘的 I/O;Using MRR 指通过一次扫描来匹配多个范围。

究其原因,主要还是因为存在回表的情况。其执行过程如下:

  • 先从 「idx_createtime」B+ 树找到对应的叶子节点,获取主键值
  • 然后从上一步获取的主键值,在 「PRIMARY」B+ 树检索到对应的叶子节点,然后获取要查询的数据

5.优化方式

通过分析,可知是由于回表 导致的性能变差。那优化的方向就是减少回表或者避免回表。我们可以通过覆盖索引来避免回表的发生。

覆盖索引指在使用「二级索引」(非 PRIMARY 聚簇索引的索引)作为查询条件的时候,如果要查询的字段都包含在「二级索引」的叶子节点中,则不需要再从 「PRIMARY」B+ 树中查询,直接返回即可。

1.通过覆盖索引获取符合条件的 id

sql 复制代码
SELECT SQL_NO_CACHE id FROM `user` WHERE create_time BETWEEN '2000-01-01' AND '2023-12-31' LIMIT 1000000,10;

2.将 1 作为子查询

sql 复制代码
SELECT SQL_NO_CACHE * from (SELECT SQL_NO_CACHE id FROM `user` WHERE create_time BETWEEN '2000-01-01' AND '2023-12-31' LIMIT 1000000,10) AS temp INNER JOIN `user` AS u on temp.id=u.id;

3.查询耗时情况

LIMIT Time (ms)
1,10 0
100,10 2
1000,10 3
10000,10 7
100000,10 40
200000,10 80
400000,10 153
600000,10 307
800000,10 397
1000000,10 432

6.总结

对于超大分页的情况,可以通过 覆盖索引+子查询 的方式来减少回表情况,从而提高效率。

相关推荐
dleei12 分钟前
MySql安装及SQL语句
数据库·后端·mysql
CryptoPP28 分钟前
springboot 对接马来西亚数据源API等多个国家的数据源
spring boot·后端·python·金融·区块链
Source.Liu42 分钟前
【学Rust写CAD】27 双线性插值函数(bilinear_interpolation.rs)
后端·rust·cad
yinhezhanshen1 小时前
理解rust里面的copy和clone
开发语言·后端·rust
uhakadotcom1 小时前
Helm 简介与实践指南
后端·面试·github
栗筝i1 小时前
Spring 核心技术解析【纯干货版】- XIX:Spring 日志模块 Spring-Jcl 模块精讲
java·后端·spring
企鹅不耐热.2 小时前
Scala基础知识6
开发语言·后端·scala
程序员一诺2 小时前
【Django开发】前后端分离django美多商城项目第15篇:商品搜索,1. Haystack介绍和安装配置【附代码文档】
后端·python·django·框架
冷琅辞2 小时前
Go语言的嵌入式网络
开发语言·后端·golang
跟着珅聪学java5 小时前
spring boot +Elment UI 上传文件教程
java·spring boot·后端·ui·elementui·vue