千万级数据分页: limit 1000000, 10性能堪忧?用“延迟关联“改写SQL
千万级数据分页: limit 1000000, 10性能堪忧用延迟关联改写SQL引言: 一个随页码增大而崩溃的查询几乎所有开发者都写过这样的分页SQL:SELECT * FROM users ORDER BY id LIMIT 1000000, 10;前几页运行飞快但当你翻到第10万页时查询突然变得奇慢无比甚至超时。问题出在哪不是索引没用上而是LIMIT的offset机制要求数据库必须扫描并丢弃前N条数据。本节将完整拆解深分页的性能瓶颈并提供延迟关联、游标分页、覆盖索引子查询三种优化方案让你面对千万级数据也能轻松应对。一、LIMIT深分页的性能瓶颈1.1 LIMIT的工作机制-- 典型分页SQL SELECT * FROM users ORDER BY id LIMIT 1000000, 10;聚簇索引(主键)二级索引MySQL Server客户端聚簇索引(主键)二级索引MySQL Server客户端大量无效回表!loop[前1000000条]终于到达offset位置LIMIT 1000000, 10扫描id索引(如果有)回表获取完整行数据丢弃(因为offset未到)回表获取第1000001~1000010行返回10条结果1.2 性能问题的本质public class DeepPaginationProblem { public static void main(String[] args) { System.out.println( LIMIT深分页的性能瓶颈 \n); System.out.println(问题核心: 数据库必须扫描并丢弃OFFSET条记录\n); System.out.println(LIMIT 1000000, 10 的执行过程:); System.out.println(1. 通过索引扫描前1,000,010条记录); System.out.println(2. 对这1,000,010条记录全部回表); System.out.println( - 每条回表都是一次随机IO); System.out.println( - 从二级索引获取主键ID); System.out.println( - 再到聚簇索引读取完整行); System.out.println(3. 丢弃前1,000,000条); System.out.println(4. 返回最后10条\n); System.out.println(为什么慢?); System.out.println(- 100万次回表随机IO(这是最慢的操作!)); System.out.println(- 传输和丢弃100万条完整数据); System.out.println(- MySQL需要临时存储这些数据\n); System.out.println(数据量计算:); System.out.println(- 假设每行1KB); System.out.println(- 100万行 1GB数据被扫描和丢弃!); } }1.3 性能对比测试-- 创建测试表 CREATE TABLE users ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50), email VARCHAR(100), age INT, city VARCHAR(50), created_at DATETIME, INDEX idx_age (age), INDEX idx_created_at (created_at) ) ENGINEInnoDB; -- 插入1000万测试数据 -- (省略插入过程) -- 测试: 不同offset的性能对比 -- 第1页: 很快 SELECT * FROM users ORDER BY id LIMIT 0, 10; -- 执行时间: ~0.001s -- 第1000页: 还能接受 SELECT * FROM users ORDER BY id LIMIT 10000, 10; -- 执行时间: ~0.05s -- 第10万页: 明显变慢 SELECT * FROM users ORDER BY id LIMIT 1000000, 10; -- 执行时间: ~2.5s -- 第100万页: 无法接受 SELECT * FROM users ORDER BY id LIMIT 10000000, 10; -- 执行时间: ~25s 或超时二、方案一: 延迟关联(推荐)2.1 延迟关联的原理延迟关联Step1: 只扫描主键利用覆盖索引不回表Step2: 获取10个IDStep3: 通过ID关联获取10行回表仅10次!传统LIMIT分页扫描100万行回表100万次丢弃100万行返回10行2.2 延迟关联SQL实现-- 方案1: 延迟关联(最通用) -- 原始SQL(慢): SELECT * FROM users ORDER BY id LIMIT 1000000, 10; -- 延迟关联SQL(快): SELECT u.* FROM users u INNER JOIN ( SELECT id FROM users ORDER BY id LIMIT 1000000, 10 ) AS tmp ON u.id tmp.id; -- 为什么快? -- 子查询只扫描索引(id是主键索引覆盖) -- 子查询不需要回表! -- 外层查询只对10个ID做回表2.3 性能对比-- 原始SQL: SELECT * FROM users ORDER BY id LIMIT 1000000, 10; -- 执行时间: 2.5s -- 扫描行数: 1,000,010 -- 回表次数: 1,000,010 (全部回表!) -- 延迟关联: SELECT u.* FROM users u INNER JOIN ( SELECT id FROM users ORDER BY id LIMIT 1000000, 10 ) tmp ON u.id tmp.id; -- 执行时间: 0.05s (快50倍!) -- 子查询扫描行数: 1,000,010 (只扫索引不回表) -- 外层回表次数: 10 (仅10次!) -- 使用EXPLAIN验证 EXPLAIN SELECT u.* FROM users u INNER JOIN ( SELECT id FROM users ORDER BY id LIMIT 1000000, 10 ) tmp ON u.id tmp.id; -- Extra列会显示: -- 子查询: Using index (覆盖索引) -- 外层: eq_ref (主键关联)/** * 延迟关联性能分析 */ public class DelayedJoinAnalysis { public static void main(String[] args) { System.out.println( 延迟关联性能分析 \n); System.out.println(延迟关联为什么快?\n); System.out.println(1. 子查询使用覆盖索引:); System.out.println( - SELECT id (只取主键)); System.out.println( - id上有主键索引); System.out.println( - 扫描只在索引树上完成); System.out.println( - 不需要回表!\n); System.out.println(2. 扫描成本对比:); System.out.println( 原SQL: 扫描100万行数据页(每页16KB)); System.out.println( 延迟关联: 扫描100万行索引页(只有id值)); System.out.println( 索引页可存储更多行IO更少\n); System.out.println(3. 回表成本对比:); System.out.println( 原SQL: 100万次回表); System.out.println( 延迟关联: 10次回表); System.out.println( 减少了99.999%的回表!\n); System.out.println(4. 适用条件:); System.out.println( - 必须有主键或唯一索引); System.out.println( - ORDER BY的列在索引中); System.out.println( - SELECT的列包含非索引列(需要回表)); } }三、方案二: 游标分页(推荐用于滚动加载)3.1 游标分页原理-- 方案2: 游标分页(Cursor-Based Pagination) -- 传统分页: 按页码 -- 第1页: LIMIT 0, 10 -- 第2页: LIMIT 10, 10 -- 第3页: LIMIT 20, 10 -- 游标分页: 按上一页最后一条记录的ID -- 第1页: SELECT * FROM users ORDER BY id LIMIT 10; -- 返回: id 1~10 -- 第2页: SELECT * FROM users WHERE id 10 ORDER BY id LIMIT 10; -- 返回: id 11~20 -- 第3页: SELECT * FROM users WHERE id 20 ORDER BY id LIMIT 10; -- 返回: id 21~30 -- 核心: 用 WHERE id 上一页最后ID 代替 LIMIT offset3.2 游标分页实现/** * 游标分页的代码实现 */ public class CursorPagination { public static void main(String[] args) { System.out.println( 游标分页实现 \n); System.out.println(SQL模板:); System.out.println( SELECT * FROM users); System.out.println( WHERE id ? -- 上一页最后一条的ID); System.out.println( ORDER BY id); System.out.println( LIMIT ?\n); System.out.println(优点:); System.out.println(1. 每次只扫描10行不回表100万行); System.out.println(2. 性能恒定不受页码影响); System.out.println(3. 适合无限滚动(Infinite Scroll)); System.out.println(4. 没有OFFSET机制不会丢数据或重复\n); System.out.println(缺点:); System.out.println(1. 不支持跳页(只能上一页/下一页)); System.out.println(2. 需要自增连续的主键); System.out.println(3. 如果主键不连续(删除过)需要调整逻辑); } } // 实际代码示例 // public ListUser getNextPage(Long lastId, int pageSize) { // String sql SELECT * FROM users WHERE id ? ORDER BY id LIMIT ?; // return jdbcTemplate.query(sql, userMapper, lastId, pageSize); // }3.3 游标分页的EXPLAIN验证-- 游标分页的EXPLAIN EXPLAIN SELECT * FROM users WHERE id 1000000 ORDER BY id LIMIT 10; -- 输出: -- type: range (范围扫描) -- key: PRIMARY (使用主键) -- rows: 10 (只扫描10行!) -- Extra: Using where -- 对比传统分页: EXPLAIN SELECT * FROM users ORDER BY id LIMIT 1000000, 10; -- type: ALL 或 index -- rows: 1000010 -- Extra: Using filesort (可能)四、方案三: 覆盖索引子查询4.1 使用覆盖索引避免回表-- 方案3: 覆盖索引子查询 -- 场景: 按非主键排序(如按创建时间排序) -- 有索引: idx_created_at (created_at) -- 原始SQL(慢): SELECT * FROM users ORDER BY created_at DESC LIMIT 1000000, 10; -- 覆盖索引子查询: SELECT u.* FROM users u INNER JOIN ( SELECT id, created_at FROM users ORDER BY created_at DESC LIMIT 1000000, 10 ) tmp ON u.id tmp.id; -- 注意: 子查询中需要id created_at都来自索引 -- 可以创建联合索引: idx_created_at_id (created_at, id) -- 创建优化索引 ALTER TABLE users ADD INDEX idx_created_at_id (created_at, id); -- 再次执行子查询将使用覆盖索引 EXPLAIN SELECT id, created_at FROM users ORDER BY created_at DESC LIMIT 1000000, 10; -- Extra: Using index (覆盖索引!)4.2 各种索引策略对比/** * 不同索引策略对深分页的影响 */ public class IndexStrategyComparison { public static void main(String[] args) { System.out.println( 索引策略对比 \n); System.out.println(场景1: 主键排序); System.out.println( 索引: PRIMARY KEY (id)); System.out.println( 延迟关联: SELECT u.* FROM users u); System.out.println( JOIN (SELECT id FROM users ORDER BY id LIMIT N,10) tmp); System.out.println( ON u.id tmp.id); System.out.println( 子查询使用覆盖索引(主键本身就是覆盖索引)\n); System.out.println(场景2: 非主键排序); System.out.println( 索引: idx_created_at (created_at)); System.out.println( 问题: 子查询需要id和created_at); System.out.println( 解决: 建联合索引 idx_created_at_id (created_at, id)); System.out.println( 这样子查询就能使用覆盖索引\n); System.out.println(场景3: 多条件查询); System.out.println( WHERE status1 ORDER BY created_at DESC); System.out.println( 优化索引: idx_status_created_at_id (status, created_at, id)); System.out.println( 子查询: SELECT id FROM users); System.out.println( WHERE status1); System.out.println( ORDER BY created_at DESC); System.out.println( LIMIT N,10); System.out.println( 完全使用覆盖索引!); } }五、三种方案适用场景对比5.1 方案选择决策是否只需上下翻页主键排序非主键排序有无优点优点优点深分页优化是否支持跳页?排序字段?方案2: 游标分页WHERE id lastId性能最优方案1: 延迟关联JOIN子查询通用性强是否有联合索引?方案3: 创建覆盖索引idx_sort_col_id再用延迟关联恒定性能无offset支持跳页通用性强适用任意排序但需加索引5.2 方案速查表| 方案 | 适用场景 | 性能 | 跳页 | 实现复杂度 ||------|---------|------|------|-----------|| 延迟关联 | 主键排序 任意字段 | 优秀 | 支持 | 中等 || 游标分页 | 主键连续 不需跳页 | 最优 | 不支持 | 低 || 覆盖索引子查询 | 非主键排序 | 优秀 | 支持 | 高(需建索引) |六、实际项目中的最佳实践6.1 完整的深分页解决方案/** * 完整的分页查询Service */ Service public class UserPaginationService { Autowired private JdbcTemplate jdbcTemplate; /** * 方案1: 延迟关联分页(支持跳页) */ public PageResultUser getUsersByPage(int page, int pageSize) { int offset (page - 1) * pageSize; // 延迟关联SQL String sql SELECT u.* FROM users u INNER JOIN ( SELECT id FROM users ORDER BY id LIMIT ?, ? ) tmp ON u.id tmp.id ; ListUser users jdbcTemplate.query( sql, new UserRowMapper(), offset, pageSize ); // 查询总数(如果不需要精确总数可以去掉) long total jdbcTemplate.queryForObject( SELECT COUNT(*) FROM users, Long.class ); return new PageResult(users, total, page, pageSize); } /** * 方案2: 游标分页(适合滚动加载) */ public ListUser getUsersByCursor(Long lastId, int pageSize) { String sql SELECT * FROM users WHERE id ? ORDER BY id LIMIT ? ; return jdbcTemplate.query( sql, new UserRowMapper(), lastId null ? 0L : lastId, pageSize ); } /** * 方案3: 按时间排序的分页(覆盖索引) */ public PageResultUser getUsersByTime(int page, int pageSize) { int offset (page - 1) * pageSize; // 前提: 已创建索引 idx_created_at_id (created_at, id) String sql SELECT u.* FROM users u INNER JOIN ( SELECT id, created_at FROM users ORDER BY created_at DESC LIMIT ?, ? ) tmp ON u.id tmp.id ; ListUser users jdbcTemplate.query( sql, new UserRowMapper(), offset, pageSize ); return new PageResult(users, -1, page, pageSize); } } /** * 分页结果封装 */ class PageResultT { private ListT data; private long total; private int page; private int pageSize; public PageResult(ListT data, long total, int page, int pageSize) { this.data data; this.total total; this.page page; this.pageSize pageSize; } // getters... }6.2 COUNT(*)的优化-- 深分页常常伴随着COUNT(*)的困扰 -- 方案1: 如果不需要精确总数不查COUNT -- 很多场景(如App的信息流)不需要总页数 -- 方案2: 使用EXPLAIN估算 EXPLAIN SELECT COUNT(*) FROM users; -- rows列显示估算行数 -- 方案3: 使用缓存 -- 将COUNT结果缓存到Redis定期更新 -- 方案4: 条件允许时使用SHOW TABLE STATUS SHOW TABLE STATUS LIKE users; -- Rows列显示近似行数(不精确但有参考价值)七、总结7.1 深分页优化口诀深分页不要慌延迟关联来帮忙。 子查询取主键覆盖索引快如光。 只需上下翻页时游标分页性能强。 WHERE id大于上一条永不扫描重复行。7.2 面试应答模板问: 千万级数据表LIMIT 1000000,10 很慢怎么优化? 答: 核心问题是数据库必须扫描并丢弃100万条数据 每条都要回表。三种优化方案: 1. 延迟关联(最通用): SELECT * FROM t JOIN (SELECT id FROM t ORDER BY id LIMIT 1000000,10) tmp ON t.id tmp.id; 子查询只扫索引不回表外层只回表10次。 2. 游标分页(性能最优): SELECT * FROM t WHERE id 1000000 ORDER BY id LIMIT 10; 直接用主键定位不需要OFFSET。 3. 覆盖索引子查询(非主键排序): 建联合索引(idx_sort_col, id) 子查询用覆盖索引再关联回表。 推荐优先使用游标分页(如果不需要跳页) 否则用延迟关联。

相关新闻

手把手教你学pcie--为什么实际带宽总是“打折扣”?

手把手教你学pcie--为什么实际带宽总是“打折扣”?

目录 八、为什么实际带宽总是“打折扣”?(非常重要) 1️⃣ 协议层的“隐形税”(躲不掉) 2️⃣ 编码不是 100%(你已经知道了) 3️⃣ TLP 大小的影响(非常关键) 4️⃣…

2026/7/23 21:14:52 阅读更多 →
RAG技术解析:检索增强生成在金融问答中的实战应用

RAG技术解析:检索增强生成在金融问答中的实战应用

1. RAG技术概述:检索增强生成的核心逻辑检索增强生成(Retrieval-Augmented Generation,简称RAG)是当前大模型应用开发中的关键技术突破。简单来说,它就像给一位学识渊博但记忆有限的教授配备了一个实时更新的数字图书馆…

2026/7/23 21:14:52 阅读更多 →
OpenClaw本地部署与配置优化实战指南

OpenClaw本地部署与配置优化实战指南

1. OpenClaw本地部署初体验上周在技术社区看到OpenClaw这个开源AI助手项目,号称5分钟就能完成本地部署。作为一个常年被云端AI服务隐私问题困扰的开发者,我决定亲自验证这个"5分钟神话"是否靠谱。经过实测,从零开始到成功运行确实只…

2026/7/23 21:14:52 阅读更多 →

最新新闻

Kimi长回答批量导出Word:DS随心转实践

Kimi长回答批量导出Word:DS随心转实践

一句话答案:Kimi 长回答和多轮对话适合先按主题批量导出 Markdown 备份,再整理成 Word、PDF、Excel 或图片。DS随心转可以批量选择当前页面已加载的多轮消息,将当前账号有权访问的内容整理成常用文档格式,其中 Markdown 导出免费。…

2026/7/23 21:24:56 阅读更多 →
神经网络架构搜索(NAS)原理与强化学习实践

神经网络架构搜索(NAS)原理与强化学习实践

1. 项目背景与需求分析这个看似随机的字符串标题实际上反映了当前深度学习领域的一个重要研究方向——神经网络架构搜索(Neural Architecture Search, NAS)。作为从业多年的AI工程师,我经常遇到类似"测试02测试03"这样的命名方式&a…

2026/7/23 21:24:56 阅读更多 →
工业高可用 IDC 基础设施方案供应商甄选思路:依托万可工程实力综合研判

工业高可用 IDC 基础设施方案供应商甄选思路:依托万可工程实力综合研判

工业企业甄选高可用性基础设施数据中心方案商,不能仅聚焦硬件算力指标,还需综合评估冗余设计、自控硬件、工业网络、能耗管理、工程组态平台能否协同支撑 724 小时不间断运行。万可(WAGO)完整输出控制器、分布式 I/O、工业交换机、…

2026/7/23 21:24:56 阅读更多 →
RAG技术解析:检索增强生成在金融领域的实践与优化

RAG技术解析:检索增强生成在金融领域的实践与优化

1. RAG技术概述:检索增强生成的核心逻辑RAG(Retrieval-Augmented Generation)技术正在重塑大模型应用的开发范式。这种将检索系统与生成模型相结合的方法,本质上是在解决大模型应用落地的三个核心痛点:知识局限性、幻觉…

2026/7/23 21:24:56 阅读更多 →
深入解析TI C6000 DSP EMIF HOLD/HOLDA总线仲裁时序设计与工程实践

深入解析TI C6000 DSP EMIF HOLD/HOLDA总线仲裁时序设计与工程实践

1. 项目概述与核心价值在嵌入式系统,尤其是基于德州仪器C6000系列这类高性能数字信号处理器的设计中,外部存储器接口(EMIF)是连接DSP核心与外部世界(如SDRAM、Flash、FPGA或ASIC)的关键桥梁。当系统中有多个…

2026/7/23 21:23:56 阅读更多 →
TMS320C6418 DSP时序参数深度解析:从理论到硬件设计实践

TMS320C6418 DSP时序参数深度解析:从理论到硬件设计实践

1. 项目概述:为什么DSP时序参数是硬件设计的“生命线”在嵌入式系统,尤其是像TMS320C6418这样的高性能数字信号处理器设计中,我们常常把注意力集中在算法优化、内存带宽和CPU主频上。然而,一个项目能否稳定运行,往往取…

2026/7/23 21:23:56 阅读更多 →

日新闻

从单点好评到指数级传播:AI副业主理人必须掌握的4层口碑渗透模型(含ROI测算表)

从单点好评到指数级传播:AI副业主理人必须掌握的4层口碑渗透模型(含ROI测算表)

更多请点击: https://intelliparadigm.com 第一章:从单点好评到指数级传播:AI副业主理人必须掌握的4层口碑渗透模型(含ROI测算表) 当AI副业主理人不再仅满足于单次服务交付,而是主动构建可复用、可裂变、可…

2026/7/23 0:00:25 阅读更多 →
AI写作开头钩子设计:为什么你的AI文案完读率不足18%?——基于2,346篇A/B测试报告的归因分析

AI写作开头钩子设计:为什么你的AI文案完读率不足18%?——基于2,346篇A/B测试报告的归因分析

更多请点击: https://codechina.net 第一章:AI写作开头钩子设计:为什么你的AI文案完读率不足18%?——基于2,346篇A/B测试报告的归因分析 在对2,346篇跨行业AI生成文案的A/B测试数据进行聚类分析后,我们发现&#xff1…

2026/7/23 0:01:26 阅读更多 →
Chitchatter完整指南:免费开源的终极点对点安全聊天工具

Chitchatter完整指南:免费开源的终极点对点安全聊天工具

Chitchatter完整指南:免费开源的终极点对点安全聊天工具 【免费下载链接】chitchatter Secure peer-to-peer chat that is serverless, decentralized, and ephemeral 项目地址: https://gitcode.com/gh_mirrors/ch/chitchatter Chitchatter是一款革命性的安…

2026/7/23 0:01:26 阅读更多 →

周新闻

Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中,我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源,还是配置文件、证书等,都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下,但这…

2026/7/22 8:58:19 阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP(轻量级目录访问协议)作为企业级身份认证的黄金标准,已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时,发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/22 19:43:43 阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击: https://intelliparadigm.com 第一章:AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”,而是以可解释、可审计、可迭代的方式,赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/23 17:49:47 阅读更多 →

月新闻