【金仓数据库征文】百万级订单深分页实战:索引已经命中,为什么第 50000 页还是慢?
一开始我只准备在现有的小表上跑几条分页 SQL。执行完才发现数据量太少无论首页还是后面的页码都很快这组结果没有多少分析价值。于是我在 Windows 上重新安装 KingbaseES建立独立实验库生成 200 万条订单。第 1、100、1000、10000 和 50000 页各测 5 次原始结果都保留截图。真正让我继续查下去的是第 50000 页。复合索引已经出现在执行计划里首页也降到 0.047ms可深页仍接近 1.9 秒。我先怀疑计划是不是看错了重新核对节点后才注意到索引仍输出了 100 万行。后面的游标、插入和删除实验都是在这一轮复查后增加的。一、先把环境跑起来KingbaseES V9 直接安装在 Windows 上。创建实例时我填写的实例名是contest_lab选择 PG 兼容模式端口改为 54321。端口没有沿用常见默认值主要是避免与电脑里原有的数据库服务冲突。实例向导执行完成后管控工具中的状态变成“运行中”。我先截下这一页再继续排查端口和连接。PowerShell 中可以看到 54321 已经监听IPv4、IPv6 都有记录。随后在 KStudio 2.0.0 中填写127.0.0.1:54321测试连接成功服务端返回 KingbaseES V009R001C010。到这里后面的 SQL 才有了可重复使用的运行环境。实验数据库名为contest_db。测试时记录到的参数是server_version12.1、shared_buffers128MB、work_mem4MB、effective_cache_size4GB、max_connections100。这些值只用于说明本次实验环境并不是一套推荐配置。二、场景不是“查20条”而是“跳过多少条”我模拟的是运营后台中的商家订单列表固定一个商家按创建时间倒序每页20条。第1页的查询很普通第50000页换算后却是OFFSET 999980。最终虽然仍只返回20条但数据库要先越过前面的999980条记录。本次使用的表结构如下CREATETABLEpagination_lab.orders(idBIGINTPRIMARYKEY,order_noVARCHAR(24)NOTNULL,merchant_idINTEGERNOTNULL,user_idBIGINTNOTNULL,statusVARCHAR(16)NOTNULL,amountNUMERIC(12,2)NOTNULL,created_atTIMESTAMPNOTNULL,paid_atTIMESTAMP,region_codeVARCHAR(8)NOTNULL,remarkVARCHAR(64));排序没有只写created_at DESC而是增加了id DESCORDERBYcreated_atDESC,idDESC排序字段中加入id是一次中途修正。只用created_at DESC时同一秒产生的订单没有唯一先后关系游标也无法只靠时间戳确定位置。因此最终查询统一使用created_at DESC, id DESC后面的游标锚点同样由这两个值组成。第一次造数只插入20万行用来检查字段、状态分布和样例记录。检查没有发现问题我执行TRUNCATE后重新生成200万行。造数规则把偶数ID分给商家42因此这个商家会占据约一半数据可以覆盖需要测试的深页位置。INSERTINTOpagination_lab.orders(id,order_no,merchant_id,user_id,status,amount,created_at,paid_at,region_code,remark)SELECTg::BIGINT,ORD||lpad(g::TEXT,16,0),CASEWHENg%20THEN42ELSE1(g%500)END,100000(g%200000),CASEWHENg%2012THENPAIDWHENg%2016THENSHIPPEDWHENg%2019THENCANCELLEDELSEREFUNDEDEND,((g%200000)100)::NUMERIC/100,TIMESTAMP2023-01-01 00:00:00(((g*47)%94608000)*INTERVAL1 second),CASEWHENg%2016THENTIMESTAMP2023-01-01 00:00:00((((g*47)%94608000)300)*INTERVAL1 second)ELSENULLEND,R||lpad((g%32)::TEXT,2,0),md5(g::TEXT)FROMgenerate_series(1,2000000)ASg;ANALYZEpagination_lab.orders;正式插入结束后全表计数为200万商家42共有100.4万条时间范围从2023-01-01到2025-12-30。表主体约274MB包含索引等对象后约317MB。我把总数和测试ID查询保留下来实验结束清理临时记录时又执行了一遍。三、没有专用索引时LIMIT 20 也救不了深分页基线组只保留建表时的主键不增加与业务查询有关的索引。第50000页使用下面的SQLEXPLAIN(ANALYZE,BUFFERS)SELECTid,order_no,status,amount,created_atFROMpagination_lab.ordersWHEREmerchant_id42ORDERBYcreated_atDESC,idDESCOFFSET999980LIMIT20;我分别记录第1、100、1000、10000和50000页。每档预热一次再执行5次表中取中位数。页码OFFSET中位耗时上层处理/输出行数主要计划特征101579.900ms20Parallel Seq Scan、top-N heapsort10019801684.015ms2000Parallel Seq Scan、top-N heapsort1000199801718.007ms20000部分 external merge出现临时I/O100001999801956.558ms200000external merge500009999802474.909ms1000000external merge、Gather Merge第50000页耗时中位数为2474.909ms。计划从并行顺序扫描开始过滤商家42后进行排序和合并约100万行进入Limit节点最后留下20行。这里的LIMIT 20只约束最终结果并没有把前面节点的处理量限制为20行。四、单列索引建成了执行计划却没有使用第一次尝试针对过滤条件给merchant_id建索引CREATEINDEXidx_orders_merchantONpagination_lab.orders(merchant_id);创建成功后执行ANALYZE再跑原来的分页 SQL。计划没有出现idx_orders_merchant仍是并行顺序扫描和排序。商家42有100.4万条记录接近全表一半这个字段的区分度并不高而且单列索引无法提供created_at DESC, id DESC的顺序。首页中位数为1543.470ms第50000页为2331.461ms。随后删除这个索引改为测试created_atCREATEINDEXidx_orders_createdONpagination_lab.orders(created_atDESC);对象列表中可以查到新索引分页计划中却仍然没有它。该查询还要处理merchant_id42排序中也缺少id一个时间字段没有覆盖完整访问路径。这轮首页中位数是1590.261ms第50000页是2315.793ms节点结构与基线相同。2315.793ms和2331.461ms都小于基线的2474.909ms但我没有把这点差值记作优化效果。原因是新索引根本没有进入计划扫描行数和临时I/O也没有明显变化。结合5轮数据来看它们仍在正常波动范围内。五、复合索引把首页降到0.047ms但还没解决深OFFSET第三次才把过滤列和两个排序列放到同一个索引里CREATEINDEXidx_orders_merchant_created_idONpagination_lab.orders(merchant_id,created_atDESC,idDESC);ANALYZEpagination_lab.orders;这次计划终于变了首页只剩Limit → Index Scan原先的并行扫表、排序和合并节点都不见了。5 次结果为 0.093、0.040、0.047、0.054、0.041ms中位数 0.047ms缓冲区命中数是 4。首页已经足够快我又按原来的页码全部跑了一遍结果如下页码OFFSET中位耗时Index Scan实际输出行数100.047ms2010019800.803ms20001000199806.889ms200001000019998041.467ms200000500009999801894.843ms1000000第 10000 页是 41.467ms到第 50000 页却重新变成 1894.843ms。看到这里我先检查有没有退回顺序扫描答案是否定的计划仍使用刚建立的复合索引。问题在索引节点的实际输出行数它为了满足OFFSET 999980还是交出了 100 万行。复合索引没有失效。它省掉了扫表和排序却省不掉 OFFSET 从开头计数的过程。六、换成游标后深页只读20行游标分页不再告诉数据库“跳过多少行”而是告诉它“从上一页最后一条之后继续”。本次排序键有两个因此游标也必须包含created_at和idSELECTid,order_no,status,amount,created_atFROMpagination_lab.ordersWHEREmerchant_id42AND(created_at,id)(TIMESTAMP2023-01-05 08:34:30,8010)ORDERBYcreated_atDESC,idDESCLIMIT20;我没有手写一个假锚点而是先从第 50000 页前取到真实的末尾位置(2023-01-05 08:34:30, 8010)。把它代入游标 SQL 后连续执行 5 次分别是 0.071、0.059、0.052、0.048 和 0.040ms中位数为 0.052ms。再看计划索引条件里已经带上商家、时间和 ID 边界节点输出 20 行只命中 5 个共享缓冲块。同一位置OFFSET 查询是 1894.843ms游标查询是 0.052ms。用这两次结果相除约为 36439。两条 SQL 使用的是同一个复合索引区别只在读取起点OFFSET 从头走并丢掉前面的行游标从保存的时间和 ID 后面接着读。快并不代表结果一定对。我把同一位置的 OFFSET 结果和游标结果各保存 20 行再分别做一次差集。两边独有的记录数都是 0也就是offset_only0、cursor_only0。七、性能之外我更在意翻页是否稳定1. 相同时间必须有第二排序键核对正式数据的时候我发现200 万行里的created_at居然没有重复值这样反而测不出同时间戳下的排序边界。于是我临时插了 3 条时间完全一致的订单ID 特意按 101、103、102 的顺序写入。查询结果稳定是 103、102、101说明排序确实是按id DESC来的不是碰巧沿用了插入顺序。这步还踩了个小坑。测试完我单独执行了ROLLBACK本以为 3 条记录已经回滚掉了结果一查还在。后来才反应过来KStudio 这个连接开了自动提交单独跑ROLLBACK根本没用。最后我按预留的测试 ID 逐条删掉再核对才清零。也正是因为这次失误后面每次清理数据我都以实际查询结果为准不再光看命令有没有执行成功。2. 顶部插入新订单OFFSET出现重复和漏数插入测试先保存原始第一页、第二页再向列表顶部加入一条订单。重新读取第二页时OFFSET结果与已经读过的第一页重复1条并且缺少原第二页中的1条使用原第一页末尾锚点继续查询游标结果的重复数、漏数均为0。新增记录改变了后续数据的绝对位置OFFSET 20仍按变化后的行号切分。原游标保存的是上一页末尾记录的时间和ID这个排序边界没有随顶部插入发生变化。3. 第一页删除记录OFFSET漏一条又多带一条删除实验使用单独的测试商家和41条隔离记录。保存前两页后从第一页删除1条。OFFSET重新取得的第二页相对原结果少1条又从后续位置带入1条旧锚点游标没有出现这两种差异。最后一步是清理。我按预留ID删除临时订单再删除隔离测试商家的记录。查询结果回到总表200万行、商家42为100.4万行测试残留为0。索引也重新核对过只保留主键和最终复合索引。八、我的落地选择不要把游标当成OFFSET的无条件替代品方案浅页表现深页表现任意跳页数据变化时更适合的场景无专用索引OFFSET慢更慢支持容易漂移不建议用于本场景复合索引OFFSET很快随OFFSET线性退化支持仍可能重复或漏数页数受控的管理后台复合索引游标很快基本不受逻辑页码影响不直接支持连续翻页更稳定信息流、API、移动端列表混合方案浅页体验好深页改游标或导出有限支持依实现而定同时存在跳页和连续浏览的系统做完这些测试我不会把OFFSET全部替换为游标。后台列表需要输入页码时OFFSET使用简单浅页性能也可以接受不过要限制最大跳转深度。连续加载的列表和API没有任意跳页需求可以返回上一页末尾的时间、ID作为下一次游标。游标参数还要处理编码、防篡改和筛选条件绑定并不是零成本方案。再深的历史检索则更适合做成异步导出。结合本次场景具体处理如下商家过滤并按时间、ID倒序的查询使用(merchant_id, created_at DESC, id DESC)后台浅页保留 OFFSET同时限制可跳转的最大页数连续下拉、API 和深页浏览传递created_at id游标大范围历史检索走异步导出上线前重新使用生产数据分布和并发量压测不照搬本机毫秒数。九、结语这次测试改变了我看执行计划的顺序。看到索引命中以后检查并没有结束还要继续看节点实际输出行数、排序和临时I/O。第50000页使用了正确的复合索引却仍输出100万行耗时重新升高的原因就在这里。两次单列索引没有进入计划KStudio自动提交还造成了一次无效回滚。这些过程提醒我建完索引要重新读取计划清理测试数据也要查询确认。比较分页方案时耗时和结果一致性需要放在一起看。全部数据来自同一台Windows电脑上的KingbaseES V009R001C010实例。硬件、缓存、并发和数据分布改变后绝对耗时也会改变因此文中的毫秒数不能直接作为生产指标执行计划中的扫描量、排序方式和OFFSET处理行数才是更值得复查的部分。

相关新闻

COMSOL模拟重金属污染土壤电动修复技术

COMSOL模拟重金属污染土壤电动修复技术

1. 重金属污染土壤修复的技术背景重金属污染土壤修复是当前环境工程领域的重要课题。随着工业化进程加快,铅、镉、铬等重金属在土壤中的累积问题日益严重。传统物理化学修复方法如客土法、化学淋洗等存在成本高、二次污染风险大等缺点。电动修复技术(Electrokinetic…

2026/10/20 12:59:19 阅读更多 →
OpenCode全平台部署与高阶使用指南:从工具到智能开发工作流

OpenCode全平台部署与高阶使用指南:从工具到智能开发工作流

1. 项目概述:从“工具”到“工作流”的认知升级最近在和一些开发者朋友交流时,发现一个挺有意思的现象:很多人把opencode简单地理解为一个“代码生成工具”或者“AI辅助插件”。这种认知不能说错,但确实有些片面,导致在…

2026/10/21 18:42:25 阅读更多 →
FairyGUI扫光特效Shader实现:5分钟打造UI动态高亮质感

FairyGUI扫光特效Shader实现:5分钟打造UI动态高亮质感

1. 项目概述:为什么UI动态高亮是提升产品质感的利器在游戏和应用开发中,UI的静态呈现早已无法满足用户对视觉体验日益增长的需求。一个按钮、一个图标,如果只是静静地躺在那里,即便设计再精美,也容易显得呆板。而动态效…

2026/10/22 9:21:25 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/10/22 10:18:38 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/10/22 11:01:53 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/10/22 9:44:47 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/10/22 10:42:00 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/10/22 10:40:08 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/10/22 10:56:37 阅读更多 →

日新闻

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/22 1:47:45 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/22 21:33:42 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/22 5:12:39 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/22 22:56:44 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/22 22:56:12 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/22 19:06:46 阅读更多 →