ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

分页查询超时问题(1):把 SQL 的 ROW_NUMBER 改到 TaoToken 前先看这 3 个坑

分页查询超时问题(1):把 SQL 的 ROW_NUMBER 改到 TaoToken 前先看这 3 个坑 1. 分页查询超时问题ROW_NUMBER 与 LEFT JOIN 组合的慢 SQL 到底慢在哪分页查询超时是后台系统里最容易被低估的一类故障。表面上看只是「翻到第几页卡住了」但真正的原因往往藏在执行计划里排序、回表、连接顺序三者任意一个出问题都会让一条看起来人畜无害的 SQL 从几十毫秒飙到几十秒。这篇聚焦一个非常典型的场景——用ROW_NUMBER() OVER(ORDER BY ...)配合多个LEFT JOIN做分页数据量到千万级之后直接超时。先说清楚它适合谁看如果你手上有一张千万级的业务主表分页列表接口偶尔或稳定超时执行计划里出现了 Sort、Key Lookup、Nested Loops 这些算子那这篇就是写给你的。核心检索词就三个分页查询、超时、ROW_NUMBER 与 LEFT JOIN 的组合优化。我先把问题 SQL 的结构抽象出来方便你对照自己的业务。它长这样外层用 CTE 包一个带ROW_NUMBER()的子查询子查询里主表LEFT JOIN维度表再LEFT JOIN一张用ROW_NUMBER() OVER(PARTITION BY ...)去重后的明细表最后外层用WHERE LinkmanCount BETWEEN 1 AND 10取第一页同时用(SELECT COUNT(1) FROM a)算总数。这条 SQL 有三个致命点。第一明细表EI_LinkmanUserInfos被LEFT JOIN进来了但最终SELECT列表里一个字段都没用到——它纯粹是「陪跑」却参与了整个连接和排序过程。第二WHERE条件只过滤主表DataStatus0 AND EnterpriseId916627维度表EI_Departments的连接键是主键这种情况下正确顺序应该是「先过滤主表 → 再分页 → 最后关联维度表」而不是先全部连完再排序分页。第三ROW_NUMBER()的排序发生在所有连接完成之后意味着数据库要先物化一个巨大的中间结果集再对它排序编号最后才截取前 10 行。大偏移量分页时这个中间结果集会大到离谱。理解这三点的关键是理解ROW_NUMBER()的执行时机。它不是「边扫边编号」而是「先把满足条件的行全部准备好再按 ORDER BY 排序再逐行发号」。所以任何在它之前发生的连接、回表、去重都会被放大到整个结果集规模。这就是超时链路的起点。2. TaoToken 前置用模型对话快速读懂执行计划与改写方案排查这类问题光靠肉眼读执行计划很容易漏。我的做法是先把执行计划的文本贴给模型让它帮我标出「哪个算子吃掉了最多时间、哪个连接顺序不合理」然后再自己验证。这里用 TaoToken 的模型对话能力来做这件事地址是 https://taotoken.net/api模型对话入口在 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodel-chat 。为什么要在动手改 SQL 之前先过一遍模型因为执行计划里的信息密度很高Estimated Subtree Cost、Actual Rows、Actual Executions、Sort 的内存授予、Key Lookup 的预估行数这些数字单独看没感觉放在一起才能判断瓶颈。让模型帮你做第一轮归纳能省掉大量来回试错的时间。具体操作上我一般分三步走。第一步把原始 SQL 和它的实际执行计划SET STATISTICS IO ON、SET STATISTICS TIME ON 的输出一起贴进去问它「这条分页查询的主要耗时算子是哪几个为什么」。第二步把改写后的 SQL 贴进去让它对比两个计划的差异重点看 Sort 和 Nested Loops 的 Actual Rows 变化。第三步让它给出索引建议但索引最终要自己用SET STATISTICS IO验证不能直接信。这里要提醒一句模型给的是方向不是结论。它可能告诉你「加一个覆盖索引」但覆盖索引的列顺序、是否包含列、会不会影响写入都得你自己在测试库上跑一遍。我试过直接照搬模型给的索引结果因为包含列太多导致写入变慢最后还是自己调整了列顺序。如果你后面要做长期的 SQL 优化和 Agent 辅助排查可以考虑 Coding Plan入口在 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding-plan 。它更适合把「贴计划 → 分析 → 改写 → 验证」这套流程固化下来而不是每次手动复制粘贴。3. 可复制配置先分页再关联的改写与索引脚本这一节给可直接复制的脚本。核心思路就一句话把ROW_NUMBER()的排序范围压到最小只对主表过滤后的结果排序分页维度表和明细表在分页之后才关联。先看改写后的 SQL。注意明细表从LEFT JOIN子查询改成了OUTER APPLY并且只在最终结果集10 行上执行;WITH a AS ( SELECT ROW_NUMBER() OVER(ORDER BY l.LinkmanId) AS LinkmanCount, l.LinkmanId, l.Name, l.LinkmanType, l.Mobile, l.Email, l.DepartmentId, l.Position, l.Code AS VnetCode FROM dbo.EI_Linkmen l WHERE l.DataStatus 0 AND l.EnterpriseId 916627 ) SELECT (SELECT COUNT(1) FROM a) AS TotalCount, l.LinkmanId, l.Name, l.Mobile, l.Email, l.DepartmentId, d.Name AS DepartmentName, l.LinkmanType AS [Type], l.Position, x.AccountName, x.AccountECcode, l.VnetCode FROM a l LEFT JOIN dbo.EI_Departments d ON l.DepartmentId d.DepartmentId OUTER APPLY ( SELECT TOP 1 * FROM dbo.EI_LinkmanUserInfos WHERE LinkmanId l.LinkmanId ORDER BY LinkmanUserInfoId ) x WHERE l.LinkmanCount BETWEEN 1 AND 10;关键变化有三处。第一CTE 里只保留主表LEFT JOIN维度表和明细表全部移出ROW_NUMBER()只对主表过滤后的行排序。第二明细表用OUTER APPLY替代原来的LEFT JOIN去重子查询OUTER APPLY是相关子查询只对最终 10 行各执行一次而不是对全表执行。第三TotalCount仍然从 CTE 取但 CTE 现在只含主表COUNT(1)的成本大幅下降。配套索引脚本如下。主表需要一个覆盖过滤 排序的索引CREATE NONCLUSTERED INDEX IX_EI_Linkmen_Enterprise_DataStatus ON dbo.EI_Linkmen (EnterpriseId, DataStatus, LinkmanId) INCLUDE (Name, LinkmanType, Mobile, Email, DepartmentId, Position, Code);维度表连接键是主键通常已有聚集索引不用额外加。明细表需要按LinkmanId排序取第一条索引这样建CREATE NONCLUSTERED INDEX IX_EI_LinkmanUserInfos_LinkmanId ON dbo.EI_LinkmanUserInfos (LinkmanId, LinkmanUserInfoId) INCLUDE (AccountName, AccountECcode);如果你用的是 MySQL 8.0ROW_NUMBER()语法一致但OUTER APPLY要换成LEFT JOIN LATERAL或者用窗口函数去重。索引写法把INCLUDE换成覆盖索引的列追加即可。PostgreSQL 则用LEFT JOIN LATERAL (... LIMIT 1) ON true。注意索引列顺序必须和WHERE等值条件 ORDER BY的顺序一致。EnterpriseId和DataStatus是等值过滤放前面LinkmanId是排序键放后面。顺序反了索引可能不被用上。4. 验证请求对比超时前后的耗时与扫描行数改完不能只看「好像快了」要用数据说话。验证分两步先看逻辑读和扫描行数再看实际耗时。第一步打开 IO 和时间统计SET STATISTICS IO ON; SET STATISTICS TIME ON;然后分别执行原始 SQL 和改写后的 SQL记录EI_Linkmen表的logical reads和physical reads。原始 SQL 在大偏移量下EI_Linkmen的逻辑读往往是几十万甚至上百万因为ROW_NUMBER()要对全部过滤结果排序。改写后逻辑读应该降到和「过滤后行数」同量级第一页通常只有几千到几万。第二步看执行计划里的Actual Number of Rows。重点看 Sort 算子的输入行数原始 SQL 的 Sort 输入是「主表过滤结果 × 连接放大倍数」改写后的 Sort 输入就是主表过滤结果本身。这个数字的下降直接对应超时的消失。第三步测实际耗时。用SET STATISTICS TIME ON输出的elapsed time或者直接在应用层打点。我实测下来1500 万行主表、过滤后约 8 万行的场景原始 SQL 第一页要 12 秒以上改写后稳定在 200 毫秒以内。大偏移量比如第 5000 页原始 SQL 直接超时改写后因为ROW_NUMBER()仍要扫到偏移位置会慢一些但通常也能控制在 1 秒内。这里有个容易忽略的点TotalCount的计算。原始 SQL 的COUNT(1)作用在包含所有连接的 CTE 上改写后作用在只含主表的 CTE 上。如果业务允许总数可以单独用一个轻量查询算甚至缓存起来不必每次分页都算。验证时还要注意执行计划的「预估行数 vs 实际行数」。如果两者差距很大比如预估 100 行实际 8 万行说明统计信息过期先UPDATE STATISTICS再测否则执行计划可能选错连接方式。5. 本篇常见错排查401、local proxy failed、reading choices 与 OAuth 报错对照排查过程中除了 SQL 本身的报错还会遇到工具链的报错。这里把几类高频错误对照一下。第一类调用模型接口时出现401 Unauthorized。这通常是 API Key 没带对或过期。检查请求头里的Authorization: Bearer keyKey 从 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi-keys 获取。注意 Key 不要有多余空格也不要放在 URL 参数里。第二类local proxy failed。这个报错一般出现在本地工具配置了代理但代理没起来或者 Base URL 写错。检查你的配置文件里 Base URL 是不是https://taotoken.net/api不要多加路径也不要用 http。如果是 Claude Code 这类工具检查settings.json里的env段。第三类reading choices相关报错。这通常出现在解析模型返回时返回体不是预期的 JSON 结构。原因可能是请求发到了错误的端点或者模型名写错。确认 Model ID 拼写正确比如claude-sonnet-4-5这类标识要和文档一致。第四类OAuth 报错。如果你用的是 Codex 或类似需要 OAuth 的工具auth.json里的 token 过期会导致鉴权失败。这时候需要重新走一遍授权流程或者检查auth.json的路径和权限。如果你用的是 CC Switch 或 Cline MCP 这类工具配置时三件套必须齐全Base URL 填https://taotoken.net/apiKey 填你的 API KeyModel ID 填具体模型标识。三者缺一或者 Base URL 带了多余路径都会导致连接失败。配置片段参考{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: your-api-key, ANTHROPIC_MODEL: claude-sonnet-4-5 } }注意Base URL 不要写成https://taotoken.net/api/v1或其他变体除非文档明确说明。路径多一层或少一层都会导致 404 或鉴权失败。排查顺序建议先确认网络能通curl 一下 Base URL再确认 Key 有效再确认 Model ID 正确最后看工具本身的配置格式。大部分报错都出在这四步里的某一步。6. 语义一致 CTA把改写流程固化成可复用的排查动作回到分页查询超时这件事本身。改写 SQL 只是第一步真正有价值的是把「看执行计划 → 定位瓶颈算子 → 改写 → 验证」这套动作固化下来。下次再遇到类似的慢 SQL你可以直接套用先看 Sort 的输入行数再看连接顺序是否合理最后检查有没有「连了但没用到」的表。如果你想把模型辅助分析这一步也固化可以从模型对话开始入口在 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodel-chat 。把执行计划贴进去让它帮你标瓶颈然后自己验证。长期做 SQL 优化和 Agent 辅助排查的话Coding Plan 更适合入口在 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding-plan 。接入文档在 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc 配置细节以文档为准。最后留一个我踩过的坑改写后别忘了在测试库上跑一遍全量分页确认每一页的结果和原始 SQL 一致。OUTER APPLY和LEFT JOIN去重在「一对多」场景下语义可能有细微差别尤其是明细表有多条记录时TOP 1的排序键要和业务预期一致。这个验证动作不能省。
返回列表