高效分页策略:掌握 LIMIT 语句的正确使用方法与最佳实践

news2024/11/29 2:51:45

本文主要介绍limit 分页的弊端及线上应该怎么用

LIMIT M,N

平时经常见到使用 <limit m,n>+ 合适的 order by 来实现分页查询,这样做到底性能如何呢?

先来简单分析下,然后再实际验证一下。

  1. 无索引条件下,需要做大量的文件排序操作,性能将会非常糟糕;
  2. 有索引条件下,刚开始的分页查询效率会比较理想,但越往后,分页查询的性能就越差。

这主要是因为,在使用 LIMIT 的时候,偏移量 M 在分页越靠后的时候,值就越大,数据库检索的数据也就越多。
例如 LIMIT 90000,10 这样的查询,数据库需要查询 90010 条记录,最后返回 10 条记录。也就是说将会有 90000 条记录被查询出来没有被使用到。

下面我们来验证下
首先创建一张会员表,表结构如下

CREATE TABLE `member` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `member_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `member_phone` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `join_date` datetime DEFAULT CURRENT_TIMESTAMP,
  `member_id` bigint(20) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_member_id` (`member_id`)
)

插入 10 万条数据

DELIMITER //

CREATE PROCEDURE InsertMember()
BEGIN
    DECLARE i INT DEFAULT 0;
    WHILE i < 100000 DO
        -- 为member_id生成一个10位随机数
        SET @random_member_id = FLOOR(RAND() * 9000000000 + 1000000000) + i*RAND();
        
        -- 插入数据
        INSERT INTO member (member_name, member_phone, member_id)
        VALUES (
            CONCAT('Member', LPAD(i + 1, 5, '0')), -- 会员姓名,编号后面跟5个0
            CONCAT('13', LPAD(RAND()*(9999999999-1000000000+1)+1000000000, 10, '0')), -- 随机生成电话号码
            @random_member_id -- 随机生成的会员编号
        );
        
        -- 增加循环计数器
        SET i = i + 1;
    END WHILE;
END //

DELIMITER ;

执行存储过程

CALL InsertMember();

验证 limit 查询

执行sql

select * from member order by member_id limit 90000, 10;

limit 分页

可以看到,所用查询时间为 0.227s,相对来说时间偏长了。

子查询优化

先查询出所需要的 10 行数据中的最小 ID 值,然后通过偏移量返回所需要的 10 行数据,可以通过索引覆盖扫描,使用子查询的方式来实现分页查询

SELECT 
    *
FROM
    member
WHERE
    id > (SELECT 
            id
        FROM
            member
        ORDER BY member_id
        LIMIT 90000 , 1)
LIMIT 10;

子查询优化
执行时间 0.024s

线上分页

那么在实际的生产环境中,该怎么使用呢?下面我来介绍下我当时是怎么做的。
核心思想就是:分段查询

假如有个订单表,在 【2024-01-01 00:00:00,2024-01-02 00:00:00】有12万条数据, 前 11 个小时段有接近于 1 万条数据,第 12 个小时段有大于 1 万条数据。

现在我们采用分时间段查询,间隔为 1 小时,每次查询 2000 条,那么每个小时段需要查询 5-6次。

先贴出 SQL 代码,方便查看

<select id="grabBizDataSlice" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List"/>
    from order
    where update_time &gt;= \#{startTime} and update_time &lt; \#{endTime}
    and status = 'PROCESS'
    and id > \#{startRow}
    order by id
    limit  \#{pageSize}
 </select>

第一个小时

第一次查询

时间段:【2024-01-01 00:00:00,2024-01-01 01:00:00】,
startRow:0
pageSize:2000

第二次查询

时间段:【2024-01-01 00:00:00,2024-01-01 01:00:00】,
startRow:2000
pageSize:2000

第三次查询

时间段:【2024-01-01 00:00:00,2024-01-01 01:00:00】,
startRow: 4000
pageSize:2000

第四次查询

时间段:【2024-01-01 00:00:00,2024-01-01 01:00:00】,
startRow:6000
pageSize:2000

第五次查询

时间段:【2024-01-01 00:00:00,2024-01-01 01:00:00】,
startRow:> 8000
pageSize:2000

注意:第 5 次查询的时候,实际返回的数据量总量已经小于 2000 条了,此时我们就可以判断到第一个小时段的数据已经查询结束了,然后开始第二个时间段的查询,道理是一样的。

redis 存储分段条件

通过上面可以看出来,我们需要有一个地方来保存每次查询的条件的。
这里我是采用的 redis hash 结构。

private final  Map<String, String> bizIdxKeyMap = new HashMap<>();
private final Integer pageSize = 2000;

List<B> bizDataList = ...; //从数据库查询的记录
Long pageIdx = bizDataList.size() == pageSize ? bizDataList.get(pageSize - 1).getId() : -1;
bizIdxKeyMap.put("sliceStartCache", sliceStartTime);
bizIdxKeyMap.put("sliceEndCache", sliceEndTime);
bizIdxKeyMap.put("pageIdxCache", pageIdx.toString());
redisCluster.hmset(bizIdxKey, bizIdxKeyMap);

从这里可以看到,当pageIdx = -1时,代表本时间段查询结束了。在下次循环时,再从 redis 中取出来这三个字段 sliceStartCachesliceEndCachepageIdxCache

完整代码

class InitController {

    @Autowired
    private BizCommonService bizCommonService;

    @Autowired
    private OrderInitServiceImpl orderInitServiceImpl;

    void calculationFlow(Date startTime, Date endTime) {

        bizCommonService.initFinanceCalculationCycle(startTime, endTime);

        orderInitServiceImpl.orderInit();
    }
}


@Service
public class OrderInitServiceImpl{

    @Autowired
    private OdsPackOrderDAO odsPackOrderDAO;

    @Autowired
    private BizCommonService  bizCommonService;


    public void orderInit() throws InterruptedException {
        while(true){

            String packOrderCalculationSwitch = redisCluster.get("pack_order_switch");
            if(packOrderCalculationSwitch != null && packOrderCalculationSwitch.equals("switch_off")){
                break; //查询结束
            }
            
            List<OdsPackOrderDO> odsPackOrderDOList = bizCommonService.grabBizDataSlice(3,
                TimeUnit.MINUTES, 2000, odsPackOrderDAO, null);

            // 对查询出来的odsPackOrderDOList做一些业务逻辑
        }
    }

}


@Component
public class BizCommonServicelImpl{

    @Autowired
    protected RedisCluster redisCluster;

    private Date financeCycleStartTime;
    private Date financeCycleEndTime;

    private final  Map<String, String> bizIdxKeyMap = new HashMap<>();

    private final static Calendar calendar= Calendar.getInstance();

    public void initFinanceCalculationCycle(Date startTime, Date endTime) {
        this.financeCycleStartTime = startTime;
        this.financeCycleEndTime = endTime;
    }

    public List<B> grabBizData(@NonNull Integer interval, TimeUnit intervalUnit, @NonNull Integer pageSize, BD bizDataSource, @Nullable Object customParam){

        try{
            String bizIdxKey = "order_index_key"; // 分页条件键
            String bizSwitchKey = "pack_order_switch"; // 查询终止状态键

            // 从 redis 查询分页条件键
            List<String> bizIdxCache = redisCluster.hmget(bizIdxKey, "sliceStartCache", "sliceEndCache", "pageIdxCache");
            Long pageIdx;
            Date sliceEndTime;
            Date sliceStartTime;
            
            if(bizIdxCache.get(2) == null ||  bizIdxCache.get(2).equals("-1")){
                pageIdx = 0L;
                if(bizIdxCache.get(0) == null){
                    sliceStartTime = financeCycleStartTime;
                    sliceEndTime = timer(sliceStartTime, interval, intervalUnit);
                }else{
                    sliceStartTime = DateUtils.getDateByMySQLDateTimeString(bizIdxCache.get(1));
                    sliceEndTime = timer(sliceStartTime, interval, intervalUnit);
                }
            }else{
                sliceStartTime = DateUtils.getDateByMySQLDateTimeString(bizIdxCache.get(0));
                sliceEndTime = DateUtils.getDateByMySQLDateTimeString(bizIdxCache.get(1));
                pageIdx = Long.valueOf(bizIdxCache.get(2));
            }

            // 判断结束标志
            if(sliceStartTime != null && (sliceStartTime.after(financeCycleEndTime) || sliceStartTime.equals(financeCycleEndTime))){
                redisCluster.set("pack_order_switch", SWITCH_OFF);
                return null;
            }

            List<B> bizDataList;
            if(customParam == null) {
                bizDataList = bizDataSource.grabBizDataSlice(
                        sliceStartTime,
                        sliceEndTime.after(financeCycleEndTime) ? financeCycleEndTime : sliceEndTime,
                        pageIdx,
                        pageSize);
            }else{
                bizDataList = bizDataSource.grabBizDataSliceByCustomParam(
                        sliceStartTime,
                        sliceEndTime.after(financeCycleEndTime) ? financeCycleEndTime : sliceEndTime,
                        pageIdx,
                        pageSize,
                        customParam);
            }
            pageIdx = bizDataList.size() == pageSize ? bizDataList.get(pageSize - 1).getId() : -1;
            bizIdxKeyMap.put("sliceStartCache", sliceStartTime);
            bizIdxKeyMap.put("sliceEndCache", sliceEndTime);
            bizIdxKeyMap.put("pageIdxCache", pageIdx.toString());
            redisCluster.hmset("order_index_key", bizIdxKeyMap);
            return bizDataList;
        }catch (Exception e){
            return null;
        }
    }

    private Date timer(Date currentTime, Integer interval, TimeUnit intervalUnit){
        calendar.setTime(currentTime);
        if(intervalUnit == TimeUnit.DAYS){
            calendar.add(Calendar.DATE, interval);
        }else if(intervalUnit == TimeUnit.HOURS){
            calendar.add(Calendar.HOUR, interval);
        }else if(intervalUnit == TimeUnit.MINUTES){
            calendar.add(Calendar.MINUTE, interval);
        }else if(intervalUnit == TimeUnit.SECONDS){
            calendar.add(Calendar.SECOND, interval);
        }else {
            throw new RuntimeException("");
        }
        return calendar.getTime();
    }
}

总结

采取合理的分页方式可以有效的提升系统性能,应根据实际情况选择适合自己的方式。
欢迎各位老师分享工作中是怎么使用的,可以交流交流。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2062257.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

代码随想录 刷题记录-13 回溯(2)组合问题

在这里涉及到的回溯中的抽象树&#xff0c;都是“选哪一个元素”的思想。 1.第77题. 组合 回溯法就用递归来解决嵌套层数的问题。 把组合问题抽象为如下树形结构&#xff1a; 可以看出这棵树&#xff0c;一开始集合是 1&#xff0c;2&#xff0c;3&#xff0c;4&#xff0c; …

探索Python交互式编程的新境界:Python-prompt-toolkit的魔法

文章目录 探索Python交互式编程的新境界&#xff1a;Python-prompt-toolkit的魔法背景&#xff1a;为何选择Python-prompt-toolkit&#xff1f;Python-prompt-toolkit是什么&#xff1f;如何安装Python-prompt-toolkit&#xff1f;简单使用&#xff1a;Python-prompt-toolkit的…

MongoDB Compass初体验

入坑Mongodb也好多年了&#xff0c;客户端一直都是使用的Robomongo&#xff0c;后改名为Robo 3T了&#xff0c;现在又改名为Studio 3T&#xff0c;还分了免费版和付费版。 最近换了新电脑&#xff0c;需要重新安装Mongodb的客户端&#xff0c;加上公司对安装软件的各种限制&…

国内首颗ASIL D级高端旗舰级R52+内核车规MCU发布,中国汽车芯片强势崛起

8月21日&#xff0c;在2024紫光同芯合作伙伴大会上&#xff0c;紫光同芯正式发布第二代THA6系列高端旗舰级新品THA6412。该芯片在安全性、可靠性、算力、实时性等方面全方位升级&#xff0c;是继今年7月紫光同芯发布THA6206芯片后&#xff0c;又一款通过ASIL D产品认证的旗舰级…

GDB的基本使用(1)

我有话说 因为时间和精力原因&#xff0c;本文写的虎头蛇尾了&#xff0c;除了启动调试与程序执行以外只有少量截图演示&#xff0c;只是简单的说明。如果有需要可以联系我&#xff0c;我有时间的话会把演示补上&#xff0c;谢谢理解。 启动调试与程序执行 启动调试并传递参数…

小白快速上手 SRC漏洞挖掘科普攻略!零基础入门到精通,收藏这一篇就够了

前言 随着网络安全的快速发展&#xff0c;黑客攻击的手段也越来越多样化&#xff0c;因此SRC漏洞挖掘作为一种新的网络安全技术&#xff0c;也在不断发展和完善。那么&#xff0c;作为一个网安小白如果想要入门SRC漏洞挖掘&#xff0c;需要掌握哪些知识呢&#xff1f;以下是本…

css属性 clip-path切割多边形polygon

如果我们要把一个矩形切割成如图所示&#xff0c;可以使用 clip-path来做切割 clip-path&#xff1a;polygon&#xff08;x1 y1&#xff0c;x2 y2&#xff09;里面的参数是切割后每个顶点的坐标&#xff0c;坐标的原点是div的左上角&#xff0c;每个顶点的坐标如下&#xff1a…

海山数据库(He3DB)源码详解:事务源码执行过程

海山数据库(He3DB)源码详解&#xff1a;事务源码执行过程 本文介绍了He3DB数据库在DDL和DML操作过程中&#xff0c;两种事务在底层源码执行流程细节。 操作过程 1、启动数据库并进入GDB 首先&#xff0c;在终端A启动一个测试用的数据库test&#xff0c;并查看当前测试用的t…

【Qt】输入类控件QLineEdit

目录 输入类控件QLineEdit 例子&#xff1a;录入个人信息 例子&#xff1a;使用正则表达式验证输入框的数据 例子&#xff1a;验证俩次输入密码一致 例子&#xff1a;切换显示代码 输入类控件QLineEdit QLineEdit 用来表示单行输入框&#xff0c;可以输入一段文本&#xf…

ubuntu18.04更改系统语言及换源的方法步骤

ubuntu的虚拟机不知道第几次被玩崩溃了&#xff0c;无奈只好重装&#xff0c;这里记录下更改语言和换源的操作步骤。 一、更改系统语言为简体中文 1&#xff0c;点击虚拟机右上角的开始按钮&#xff0c;选择设置。 2&#xff0c;在左侧选项中选择Region & Language,再选择…

day28-测试自动化之Requests库的发送请求、响应内容、Cookie和session

目录 一、发送请求 1.1.GET请求 1).作用 2).步骤 3).响应对象 4).代码 5).带参代码 1.2.POST请求 1).作用 2).应用 3).参数 4).代码&#xff08;地址已失效&#xff09; 5).扩展 1.3.PUT请求 1).作用 2).应用 3).参数 4).响应 5).代码&#xff08;地址已失效&#xff09; 1.4.D…

DC-2综合渗透,rbash逃逸,git提权,wordpress靶场渗透教程

前言 博客主页&#xff1a;【h0ck1r丶羽】的公众号~~ ​ 本文主要讲解了渗透测试中的完整渗透测试流程&#xff0c;主要介绍了【wpscan】、【cewl】、【rbash逃逸】的使用技巧&#xff0c;靶场为vulnhub的机器大家可以自行下载&#xff0c;如果文章哪有不对&#xff0c;还请师…

大模型在应用开发安全左移实践

1.应用开发安全左移势在必行 近年来&#xff0c;应用系统被入侵或敏感信息泄漏类的安全事件时有发生&#xff0c;大部分安全事件的根本原因是应用软件设计或实现中存在安全漏洞。由于软件安全性问题导致各种信息泄密、信息被篡改、网络服务中断的事件频发&#xff0c;给企业和…

如何基于 langchain与 LLM 构建自己的知识库系统

如何基于 langchain与 LLM 构建自己的知识库系统 前些时候字节上了自己的扣子&#xff0c;用来构建我们自己的 agent AI 实战&#xff1a;手把手教你使用「扣子/coze」来搭建个人blog知识库 - 掘金 (juejin.cn)[1] 受到启发&#xff0c;想在本地或者自己的服务器上面搭建一个知…

机器学习第五十二周周报 Distribution Shift of GNN

文章目录 week52 Distribution Shift of GNN摘要Abstract一、文献阅读1. 题目2. Abstract3. 预测标准3.1 问题提出3.2 图结构3.3 分布转移 4. 文献解读4.1 Introduction4.2 创新点4.3 实验过程 5. 结论 二、若依框架1. 框架概述2. 核心功能 3. 技术栈4. 框架特点优缺点小结参考…

写作小白2024年逆袭,AI写作工具top4的正确打开方式

以前&#xff0c;人们总觉得写文章、编故事是人特有的本事&#xff0c;机器肯定搞不定。但现在&#xff0c;AI越来越牛&#xff0c;这些AI写作工具&#xff0c;用上了深度学习、自然语言处理这些高科技&#xff0c;能模仿人的思路来写东西。它们不仅能帮人写&#xff0c;有时候…

AI大模型日报#0822:OpenAI推GPT-4o微调服务、混元大模型负责人专访

导读&#xff1a;AI大模型日报&#xff0c;爬虫LLM自动生成&#xff0c;一文览尽每日AI大模型要点资讯&#xff01;目前采用“文心一言”&#xff08;ERNIE-4.0-8K-latest&#xff09;、“智谱AI”&#xff08;glm-4-0520&#xff09;生成了今日要点以及每条资讯的摘要。欢迎阅…

APO 集成生态exporter一键完成指标采集

Metrics 作为可观测性领域的三大支柱之一&#xff0c;Metrics数据采集显得尤为重要。传统的prometheus工具采集指标&#xff0c;需要指定路径抓取&#xff0c;当指标越来越多配置会显得复杂。同时prometheus只能采集指定的指标&#xff0c;当用户需要节点系统相关、中间件等指标…

虚幻5|制作一个木桩,含血量及伤害数字

一&#xff0c;基础设置 1.创建Actor蓝图类 2.编辑胶囊体和网格体的碰撞预设 3.打开敌人的角色蓝图&#xff0c;编辑飙血特效 二&#xff0c;创建敌人血量的ui&#xff0c;命名为敌人血量&#xff0c;如下 1. 2&#xff0c;打开后&#xff0c;添加一个画布画板和进度条&#…

为什么有了session和cookie还要使用JWT

session和cookie 为什么要使用session和cookie,他的流程是怎么样的 因为浏览器是无状态的,相当于每一次访问都是一次全新的访问,我们一般是登录来进行校验获取他的全部信息,所以会出现这个情况 这样当然也可以使用,但是很繁琐,并且影响性能,所以出现了session和cookie,他会生…