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.总结

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

相关推荐
指令集梦境1 小时前
Cursor + Spring Boot实战:从零写一个RESTful API
spring boot·后端·restful
码云之上2 小时前
聊聊如何设计一个高效、稳定的 Node.js 接入层
前端·后端·node.js
IT_陈寒3 小时前
Vite项目build后路由404了?你可能漏了这个小配置
前端·人工智能·后端
宸津-代码粉碎机4 小时前
Spring AI企业级实战|从RAG优化到Agent多工具调度
java·大数据·人工智能·后端·python·spring
吴佳浩4 小时前
AI Infra 的真相:Go 没输,rust也不是取代
后端·rust·go
喵个咪4 小时前
实时游戏网络协议深度对比:KCP vs WebRTC vs WebSocket
后端·websocket·webrtc
普通网友4 小时前
springboot之集成Elasticsearch
spring boot·后端·elasticsearch
QuZero4 小时前
Guava Cache Deep Dive
java·后端·算法·guava
leeyi5 小时前
SSE 实时推流 —— Token 怎么一个个蹦出来
后端·agent
leeyi5 小时前
ReAct 循环的 50 行 Go 实现,逐行拆解
后端·agent