当前位置: 首页 > news >正文

MySQL深分页优化及试验

深分页

试验准备

试验背景:MySQL8,三星高速ssd,AMD Ryzen 7 5800H

试验基础SQL:

构建一张大表(1000w)用于试验,嫌慢的可以用pg,但pg相对查的慢

-- drop table tb_sku;
CREATE TABLE `tb_sku` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '商品id',`sn` varchar(100) NOT NULL COMMENT '商品条码',`name` varchar(200) NOT NULL COMMENT 'SKU名称',`price` int(20) NOT NULL COMMENT '价格(分)',`num` int(10) NOT NULL COMMENT '库存数量',`alert_num` int(11) DEFAULT NULL COMMENT '库存预警数量',`image` varchar(200) DEFAULT NULL COMMENT '商品图片',`images` varchar(2000) DEFAULT NULL COMMENT '商品图片列表',`weight` int(11) DEFAULT NULL COMMENT '重量(克)',`create_time` datetime DEFAULT NULL COMMENT '创建时间',`update_time` datetime DEFAULT NULL COMMENT '更新时间',`category_name` varchar(200) DEFAULT NULL COMMENT '类目名称',`brand_name` varchar(100) DEFAULT NULL COMMENT '品牌名称',`spec` varchar(200) DEFAULT NULL COMMENT '规格',`sale_num` int(11) DEFAULT '0' COMMENT '销量',`comment_num` int(11) DEFAULT '0' COMMENT '评论数',`status` char(1) DEFAULT '1' COMMENT '商品状态 1-正常,2-下架,3-删除',PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';DELIMITER $$
CREATE PROCEDURE generate_sku_data()
BEGINDECLARE i INT DEFAULT 0;DECLARE batch_size INT DEFAULT 100000;DECLARE total_batches INT DEFAULT 100;DECLARE start_val INT DEFAULT 1;DECLARE end_val INT DEFAULT 0;WHILE i < total_batches DOSET start_val = 1 + (i * batch_size);SET end_val = (i + 1) * batch_size;INSERT INTO tb_sku (sn, name, price, num, alert_num, image, images, weight,create_time, update_time, category_name, brand_name, spec,sale_num, comment_num, status)SELECTCONCAT('SN', seq),                                               -- snCONCAT('商品', seq),                                             -- nameFLOOR(RAND() * 10000),                                           -- priceFLOOR(RAND() * 100),                                             -- numFLOOR(RAND() * 10),                                              -- alert_numCONCAT('http://example.com/img/', seq, '.jpg'),                  -- imageCONCAT('http://example.com/img/', seq, '.jpg,http://example.com/img/', seq+1, '.jpg'), -- imagesFLOOR(RAND() * 1000),                                            -- weightNOW() - INTERVAL FLOOR(RAND() * 1000) MINUTE,                    -- create_timeNOW() - INTERVAL FLOOR(RAND() * 1000) MINUTE,                    -- update_timeCONCAT('类目', (seq % 10) + 1),                                  -- category_nameCONCAT('品牌', (seq % 5) + 1),                                   -- brand_nameCONCAT('规格', (seq % 3) + 1),                                   -- specFLOOR(RAND() * 100),                                             -- sale_numFLOOR(RAND() * 50),                                              -- comment_num'1'                                                              -- statusFROM (SELECT (start_val + t4.i*10000 + t3.i*1000 + t2.i*100 + t1.i*10 + t0.i) AS seqFROM(SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t0,(SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t1,(SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t2,(SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t3,(SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t4HAVING seq BETWEEN start_val AND end_val) numbers;SET i = i + 1;-- 可选:每批提交后输出进度SELECT CONCAT('已插入第 ', i, ' 批数据,共 ', i*batch_size, ' 条记录') AS progress;END WHILE;
END$$
DELIMITER ;-- 执行存储过程
CALL generate_sku_data(); -- 9m18s
原始情况

查询偏移量过大的场景我们称为深度分页,这会导致查询性能较低,例如:

-- tb_sku是一张1000w数据的表,扫表情况
select * from tb_sku order by id limit 9999980,10; -- 13s(ssd)

解决思路:

连续id时

主键id是连续的,这其实蕴含这一个情况,就是可以直接计算所取页数对应的id,这是使用数学降维打击了:

select * from tb_sku where id > 9999980 order by id limit 10; -- 180ms

但很多时候往往数据id是不连续的。

使用主键加速

扫表的时候如果只扫主键,那么能快一点:

select * from tb_sku ORDER BY id limit 9999980,10; -- 11s
select id from tb_sku ORDER BY id limit 9999980,10; -- 5s

这是由于只取id,io开销少一点,速度就快了一些,但是如果还是想获取很多其他的字段,可以使用子查询

select *
from tb_sku t,(select id from tb_sku ORDER BY id limit 9999980,10) a
where t.id = a.id; -- 5s

这样速度快的同时也有了其他的字段,如果逻辑复杂起来,可以用join

-- 使用 INNER JOIN 进行延迟关联
SELECT t1.*
FROM tb_sku t1
INNER JOIN (SELECT id FROM tb_sku where id > 9999980 order by id LIMIT 10 ) t2 ON t1.id = t2.id;

通常这种手法叫做延迟关联
什么是延迟关联?延迟关联 是一种SQL查询优化技术,其核心思想是:先通过一个高效的范围查询(通常使用覆盖索引)快速定位到符合条件的主键ID,然后再通过主键ID关联回原表获取完整的行数据。

为什么延迟关联更高效?
传统写法的执行过程:

  1. 根据WHERE条件扫描索引或全表扫描
  2. 对于每一条匹配的记录,都立即回表获取完整数据
  3. 对所有完整数据进行排序
  4. 应用LIMIT和OFFSET
    问题:即使最后只需要20条数据,也可能需要回表上千次!

延迟关联的执行过程:

  1. 在索引上完成所有筛选和排序(索引通常包含where条件和order by字段)
  2. 只对最终需要的20条记录进行回表
  3. 用主键ID快速定位到完整数据
    优势:大大减少了回表操作次数!
加速手法
select *
from tb_sku t,(select id from tb_sku where 1=1 limit 9999980,10) a
where t.id = a.id; -- 5s

这种做法的基础上,还能加速,分析一下索引:

EXPLAIN (select id from tb_sku where 1=1 limit 9999980,10);
-- 1,SIMPLE,tb_sku,,index,,PRIMARY,4,,9876750,100,Using index

走的主键索引,但是,如果建立其他的二级索引,例如给name字段价一个索引

CREATE INDEX tb_sku_name_index ON tb_sku (name);-- 30s
EXPLAIN select id from tb_sku WHERE  1=1
#                   ORDER BY id -- order by id会让name索引失效limit 9999980,10; -- 2s(有二级索引情况);

速度就可以进一步加速,这属于具体情况具体分析的优化了。为什么会加速呢?explain结果:

1,SIMPLE,tb_sku,,index,,tb_sku_name_index,802,,9876750,100,Using index

是因为走了二级索引的缘故,二级索引没挂数据,io次数少,然后也不回表,所以哪怕都是索引,二级索引的时间比主键索引还能少一点。

游标做法
-- 第一次查
select * from tb_sku order by id limit 10;
-- 下一页查(记住上页最后一个 id = 100)
select * from tb_sku where id > 100 order by id limit 10;

优点:性能 O(limit) 而不是 O(offset+limit),非常快。
缺点:只能做“向后翻页”,不支持随机跳页,需要依赖前一次的查询。

预计算分页锚点
  • 建一张辅助表 page_anchor,存储每 N 条的起点 ID,比如每 1 万条存一个。
  • 用户要第 999,998 页时,可以快速找到最近的锚点,再往后翻少量 OFFSET。
-- page_anchor 表:
-- page_no | id_anchor
-- 100000  | 2000000
-- 200000  | 4000000
-- ...-- 跳到第 999,998 页
-- 先找到最近的锚点
SELECT id_anchor FROM page_anchor WHERE page_no <= 999998 ORDER BY page_no DESC LIMIT 1;-- 假设拿到 id_anchor = 9980000
-- 再往后偏移少量
SELECT *
FROM tb_sku
WHERE id >= 9980000ORDER BY id
LIMIT (999998-998000)*20, 20;

优点:速度很快。
缺点:需要额外的工作去维护表page_anchor

倒叙索引

如果是那种有些要从后往前翻的场景,可以建立一个倒叙索引。

CREATE INDEX tb_sku_id_desc_index ON tb_sku (id DESC);-- 18s
EXPLAIN (select id from tb_sku limit 9999980,10); -- 1s
其他

业务逻辑优化:把表拆了,分页。
禁止此行为:ui上封掉,或者后台拒绝随机访问页数等。

另外一条路:这个问题的讨论范畴已经在mysql这种OLTP(联机事务处理) 数据库的边缘了,也就是当数据量继续增长的时候,其实已经逐渐脱离OLTP,进入到OLAP(联机分析处理)的范畴了。直白的说,就是到底是继续在 MySQL 上优化(索引、架构、分表、缓存等方案(就是上述讨论的范畴)),还是换到 专门的分析型数据库(ClickHouse、Doris、Greenplum、TiDB HTAP 等)。如果使用ClickHouse / Doris,只要稍微做一些简单的优化,深分页有机会稳定在 1 秒以内,这是mysql做许多工作才能达到的效果。但是是否要换,那就是另一件事了。

数据仓库是独立于业务数据库之外的一套数据存储体系,OLAP(OnLine Analytical Processing联机分析处理)是数据仓库系统的主要应用,能够支持复杂的分析操作,与数据库需要直接直接线上业务不同,数据仓库侧重于分析决策,提供直观的数据查询结果。(当然数据仓库也是可以支持线上业务的,后面会举例子)。

**数据库:**是OLTP(联机事务处理)应用的场景,其存储的主要是与业务直接相关的数据,强调准确、低时延、高并发,如果没有特别强调,基本上数据库里只会去存储与业务相关的数据。

**数据仓库:**OLAP(联机分析处理)是数据仓库系统的主要应用,其支持的对象只要是面向分析场景的应用,提供结构化的、主题化的数据提供给运营,做业务反馈和辅助决策用,同时,有些场景下,也可以由数据仓库对业务进行支持。

ref:1. 什么是OLTP、OLAP、数据库和数据仓库? - 知乎

总结
方法核心思路 / 优点适用场景 / 限制
范围查询 / 分页标签 (cursor / keyset pagination / tag record 方法)不使用 OFFSET,而是记住上一次查询的某个关键字段(常是主键 ID 或者 timestamp),下一次分页从这个点开始查询,比如 WHERE id > last_idid < last_id。效率高,因为利用索引,不跳过太多。 (掘金)必须有可排序且可作为游标的字段,例如 ID 时间戳等。当允许顺序翻页(下一页),不要求用户能随意跳到第 N 页。对“跳页”(直接访问第 1000 页)支持差一些。
子查询 + 主键定位 + 联结(Delayed join / inner join)先用子查询(或内联查询)只查出主键(或索引列),加上 LIMIT 偏移来定位起点,然后再 join 主表取得完整记录。减少回表 / 排序 /扫描开销。 (JavaGuide)适合主键或唯一标识的场景,且子查询能用索引快查到起始点。对于非常大的偏移也可能有子查询开销和内存压力。
覆盖索引(covering index)如果所需返回的字段都在索引里,那么不需要回表查数据,只查索引数据,IO 小。排序/过滤字段如果也在索引中,效果更佳。 (JavaGuide)需要设计好索引;如果返回字段很多或者不能预先确定,覆盖索引可能难以完全覆盖所有情况。
设定阈值 / 自动切换策略当分页深度(Offset)超过某个阈值时,不用传统分页,而用不同策略(如 keyset 分页、流式查询、分片、缓存等)来处理。这样浅页保持通用分页方式,深页则用优化路径。 ((这个例子是分布式的方案)博客园)需要在业务 /代码中能判断分页深度,并做到有备用方案。用户体验上可能要有差异(例如无法跳太深)。
倒序 + 限制跳页行为(mysql8可以建立倒叙索引)在某些排行榜、日志类型的场景,如果用户要看“最后几页”或最新数据,可以倒序查询最近的记录,然后分页。这样深度不会大。而如果用户真的要看最早的一些旧数据,再用特定接口。 (CSDN 博客)不适所有场景。若排序是时间或 ID,倒序有意义;但用户跳页或按页码要求定定位可能受到限制。
另外的方法核心思路 / 优点适用场景 / 限制
分表 / 分区大表拆分成多个分区或多个子表(按时间、按某些标签等分区),这样单次分页查询只在某个子表中操作,数据量相对小。减少扫描量和索引树大小。 (CSDN 博客)分区管理复杂;有些查询跨多个分区会变复杂;写入 /维护成本增高。
缓存 / 预计算 /中间表对一些热门页数、热门查询预先缓存结果(或者分页列表的索引列表),避免每次都查询原始表。或者预先维护一个快照表(或物化视图),供分页查询。 (知乎专栏)当数据变更频繁时缓存会失效或维护成本高。快照 /预计算也会有延迟问题。
禁止或限制深页的跳转在 UI 或 API 层面限制用户不能跳太深;例如只允许前N页、最新数据或者“查看历史”用专门接口。这样业务上规避深分页带来的性能问题。对用户体验可能有影响,需要业务上接受这种限制或者给出替代方式(搜索、过滤、关键词定位等)。
OLAP技术(ClickHouse / Doris等)深分页性能好(千万级秒级返回)、聚合统计快、支持分布式扩展,适合大数据展示、报表、日志分析、OLAP 查询写入/事务能力弱、超大 offset 仍有性能消耗、建表索引规划重要、运维复杂
http://www.dtcms.com/a/400323.html

相关文章:

  • 外贸网站案例柳州建设网站
  • 深圳个人网站制作微网站建设价格对比
  • 高校教资--高等教育学
  • 源码建站和模板建站区别seo技巧是什么意思
  • 郑州网站公司哪家好重庆网站建设 熊掌号
  • 室温太赫兹设备打开了6G网络的大门
  • 【源码解析】spring-ai-alibaba-jmanus的ModelDataInitialization
  • 广联达 CONCETTO 产品与分析
  • 比较版本号-双指针比较
  • 网站推广seo代理长春广告设计公司
  • wordpress建站后怎样发布手绘动画制作软件
  • ppt模板做的好的网站有哪些品牌建设ppt
  • 行业电子网站建设长葛做网站
  • 2025版基于springboot的家政服务预约系统
  • agent概述
  • 万网虚拟主机上传网站php网站怎么做集群
  • 深圳网站优化包年wordpress footer.php
  • 网站备案安全承诺书公司名称大全及最新
  • 厦门网站建设方案咨询大连企业网站建设定制
  • Android Studio 详细安装与配置指南
  • 陶瓷类网站建设个人网站设计论文一万字
  • 带数据库的网站模板下载做网站网页的成本
  • 免费网站排名优化在线域名设计与分析
  • 漂亮公司网站源码打包下载大型户外广告设计公司
  • 网站建设 分类wordpress默认index
  • 免费建微网站平台网站换程序 搜索引擎
  • 门户网站建设滞后怎样做网站内链
  • 数字化转型:概念性名词浅谈(第五十讲)
  • Fiber、协程和 Generator行为上的区别?
  • 朝阳区建设工作办公室网站wordpress json