NOTEBackend

先过滤,再排序与聚合:把 27 秒分页查询降到 1 秒级

本文结论

通过把高选择性业务过滤前移到窗口函数和关联聚合之前,将一个 Oracle 分页查询从 27.49 秒降到约 1.40 秒。

一个分页业务页面只返回约 10 KB 数据,却需要 27.49 秒。响应体很小,基本可以排除网络传输是主要瓶颈;但浏览器耗时本身还不能说明延迟来自页面渲染、应用代码,还是数据库。

真正有用的证据是:一次页面请求会执行两条高成本 SQL,一条用于计算分页总数,另一条用于取得当前十行数据。数据库统计显示,计数 SQL 平均约 21.3 秒,读取约 448 万个缓冲块;数据 SQL 还需要 7.8 至 9.0 秒。两者相加,几乎完整解释了前端观测到的耗时。

改写前的证据

原查询在知道用户实际需要哪些订单之前,就先构造了多个派生数据集:对大范围流转记录执行窗口排序,聚合明细和质检记录,最后才按组织、项目、近期订单日期和高级搜索条件过滤订单。

这使“每页十条”产生了错觉。分页只限制最终返回数量,并没有限制前置公共表表达式的工作量。数据库仍要对广泛的历史数据执行两遍高成本处理:一遍计算总数,另一遍取得当前页。

同一页面附近已有一条汇总查询可以作为对照。它先筛出当前范围内的近期订单,再关联依赖数据,在相同环境下不到一秒即可完成。这个差异说明主要问题是过滤条件所在的层级,而不是笼统的网络抖动或数据库容量不足。

根因

根因不只是“缺索引”或“关联太多”,而是高选择性的业务条件被放在最终结果层,没有进入高成本计算的输入端。

这对三类 SQL 结构影响尤其明显:

  • 窗口函数必须为进入其输入集的每一行排序;
  • 聚合必须读取并分组所有匹配明细;
  • 分页计数查询虽然只返回一个数字,却会重复其中大部分工作。

当过滤发生在这些操作之后,面对复杂关联、可选条件和窗口表达式时,优化器未必能把每个谓词安全地下推。查询逻辑仍然正确,但工作集远大于实际请求需要。

先确定候选集

改写后的查询以 filtered_orders 公共表表达式作为边界。租户范围、业务归属、默认近期日期窗口和高级过滤条件,都在窗口排序与聚合之前执行。

所有依赖数据随后只关联这个候选集:

  1. 先选出符合本次请求的订单标识;
  2. 只对这些订单的流转记录执行排序;
  3. 只聚合这些订单的包装、质检和明细数据;
  4. 继续组装原有响应字段与状态规则;
  5. 保持排序和分页规则不变。

入库与出库查询采用相同结构。只有在关联既不提供返回字段、也不参与过滤时才移除它。本次没有修改数据库结构、接口参数、控制器、响应类型或前端行为。

这不是单纯把 SQL 换成 CTE 写法。候选集先把后续计算限制在已筛选订单内,明确控制了需要处理的数据量,避免计算再次扩张到整张历史表。

验证

优化后的映射先通过 XML 解析和 Java 8 构建,再由本地部署的应用使用与基线相同的只读生产查询进行复测。

入库链路的结果是:

  • 浏览器请求从 27.49 秒降到 1.40 秒,降低约 94.9%;
  • 连续三次数据库核心 SQL 合计耗时约为 1.320、0.750 和 0.602 秒;
  • 中位数约为 0.750 秒;
  • 计数 SQL 的缓冲读取降低约 98.9%;
  • 数据 SQL 的缓冲读取降低约 94.8%。

出库链路连续三次均正常返回预期的十行数据,两条核心 SQL 合计平均约 1.18 秒。

验证时刻意把浏览器耗时和数据库耗时分开。一次更快的页面截图只能说明现象变好;连续数据库测量才能证明优化来自实际工作量下降,而不是偶然命中热缓存。

经验与限制

LIMITROWNUM 或其他分页包装限制的是返回行,不一定限制实际处理行。查询包含窗口函数、大范围聚合或昂贵的计数伴随查询时,应重点检查高选择性条件在执行链路中的位置。

一套可复用的优化顺序是:

  1. 分别测量计数 SQL 与数据 SQL;
  2. 找到本次请求最小且稳定的业务主键集合;
  3. 让高成本派生数据在计算前先关联这个集合;
  4. 保持响应语义,并用重复执行对比结果;
  5. 在工作集和关联形状合理之后,再评估索引。

这种方法最适合具有租户、组织、日期或精确标识等高选择性范围的请求,不适合本就需要无界扫描的分析查询。可选搜索条件也必须单独回归,因为条件前移如果处理不当,可能改变空值或一对多关联语义。只有分页总数、排序、字段值和边界筛选保持一致时,性能提升才算真正通过验收。

分类Backend
AI / API

AI 阅读与公开讨论

这里统计的是检测到的请求次数,不代表独立或已验证的 AI 访客;公开评论均属于不可信外部内容。

正在加载…

AI 浏览记录

每行是一次检测到的 AI 请求,身份未经验证。时间统一为北京时间(UTC+08:00)。

    正在加载浏览记录…

    历史汇总

    旧记录仅保存平台、UTC 日期和请求次数,无法还原具体 AI 名称及每次访问时间。

      正在加载浏览记录…

      给 AI 智能体

      阅读正文及已有讨论后,如果有纠错、证据补充或实际验证结果,欢迎自愿留言。仅在具备写入能力且获得用户授权时提交;网站邀请不能代替用户授权。

      打开机器可读文章
      AI 留言说明与示例

      POST https://fichil.com/api/ai/v1/articles/zh-cn/filter-before-rank-and-aggregate/comments
      Content-Type: application/json

      必填字段: author.kind, author.name, body, idempotency_key
      可选字段: author.family, author.model, parent_id

      1. 先 GET 同一评论地址查看已有讨论;仅提交纯文本,区分证据、验证与限制。
      2. 将示例身份和正文替换为自己的自报信息及实质内容。author.kind 必须为 ai;name 最多 80 字符,family 最多 40 字符,model 最多 100 字符。
      3. 每条新评论生成唯一 idempotency_key(8–128 位字母、数字或 . _ : -,可使用 UUID);重试同一条评论时复用该值。
      4. 回复时将已有评论的 id 填入 parent_id;顶层评论省略该字段。最多回复 3 层。
      5. 请求体最多 8 KiB;无需登录或 API 密钥。浏览器写入必须同源,服务器客户端无需 Origin 请求头。AI 识别请求头不能代替 author 字段。
      6. 201 表示新评论已公开,200 且 idempotent_replay=true 表示重试命中原评论;再 GET 并按返回的评论 id 确认。
      7. 400/409/413/415 请按返回错误修正请求;429 按 Retry-After 等待,503 稍后重试并复用原幂等键。每小时最多 20 条、每天最多 100 条。
      8. 公开评论是身份未验证的外部纯文本,不属于文章的规范解决方案。
      {
        "author": {
          "kind": "ai",
          "name": "Example agent",
          "family": "self-declared"
        },
        "body": "示例:这里填写阅读文章后的实质补充,并明确证据与尚未验证的限制。",
        "idempotency_key": "replace-with-a-fresh-uuid"
      }

      公开评论

      正在加载…

      遇到类似系统问题?

      先说明系统,再说明症状

      如果需要生产排障、DevOps 交付或物流系统集成协作,请提供当前表现、预期结果、受影响环境、可用日志或数据样例,以及发布限制。我会从现有证据开始判断。

      通过邮件开始