SQL性能排查记录

为何原本毫秒级的查询,突然执行时间变慢?

1. 原 SQL

sql 复制代码
SELECT   /*+  OPTIMIZER_OR_NBEXP(2)  USE_HASH(A D) */  count(1)
          from aviewer.v_app_inp_apply_sheet         a,
               aviewer.v_dic_bill_type_dict          b,
               aviewer.v_exam_report_master          c,
               zoeods_pacs.exam_report_apply_idx_rec d
         where a.bill_type_code = b.bill_type_code
           and a.apply_no = d.his_bill_no(+)
           and d.bill_no = c.bill_no(+)
           and a.apply_sheet_status_code != '9'
           and a.pres_no is not null
            and (b.item_class_code like 'E%' OR b.item_class_code LIKE 'M%')
            and a.ITEM_CODE<>'133241'
           and (a.REPORT_STATUS_CODE not in ('77', '66') or
               a.REPORT_STATUS_CODE is null)
           and c.VALID_FLAG = '1'
           and c.visit_type = '2'
           and (c.PATIENT_ID = '0000886536')
          and (c.VISIT_NO = '0113728006' or '0113728006' is null)
          AND (c.visit_type = '2' or '2' is null);

手里没有"毫秒级"时期的历史执行计划,无法直接进行前后对比,因此先从当前执行计划入手,分析哪里不符合一个毫秒级查询应有的数据访问特征。

2. 先分析四表关联关系

SQL 中 4 个对象的关联关系为:

text 复制代码
B --- A --- D --- C

其中 C 表存在比较精确的过滤条件:

text 复制代码
PATIENT_ID = 某患者
VISIT_NO   = 某次就诊
VISIT_TYPE = '2'
VALID_FLAG = '1'

从业务逻辑上看,C 表经过患者、就诊号等条件过滤后,结果集应该比较小,因此理想的数据访问路径应尽量接近:

text 复制代码
C 很小
↓
通过 BILL_NO 找 D
↓
通过 HIS_BILL_NO 找 A
↓
通过 BILL_TYPE_CODE 找 B

3. 看当前执行计划的 JOIN 顺序

整理当前执行计划的数据流(底层 → 高层):

执行计划 对应逻辑
节点17、22 → 节点16(15) A 表
节点16(15)、12 → 节点11(10) A 表 + B 表(NEST LOOP)
节点11(10)、27 → 节点9 A 表 + B 表 + D 表(HASH)
节点9、5 → 节点4 A 表 + B 表 + D 表 + C 表(HASH)

当前实际 JOIN 顺序大致为:

text 复制代码
A + B
↓
再关联 D
↓
最后关联 C

这与开始设想的 C → D → A → B 几乎是反过来的。

4. 看"预估行数 → 实际行数"

当前执行计划带有实际执行信息,可以看到优化器预估行数与真实执行行数之间存在明显偏差:

  • 节点21:相差约 30 倍;
  • 节点26:相差约 800 倍;
  • 节点16:相差约 700 倍;
  • 节点11:相差约 4000 倍;
  • 节点9:相差约 400 倍。

其中节点4最后才加入 C 表,实际结果为 0 行。

也就是说,在最终一行数据都不需要的情况下,前面 A、B、D 已经产生并处理了大量中间结果,存在明显无用功。当前计划中 C 表参与 JOIN 过晚。

5. 第一阶段假设:是否只是 JOIN 顺序导致性能下降?

看到当前 JOIN 顺序后,先提出一个最直接的假设:

如果能够让 C 表先利用 PATIENT_ID、VISIT_NO 等高选择性条件缩小结果集,再沿着 C → D → A → B 进行关联,是否就能恢复到毫秒级?

先将 SQL 改写成更容易观察关联链的形式,用于验证这个假设。

5.1 JOIN 顺序验证 SQL

sql 复制代码
WITH C_FILTER AS
(
    SELECT BILL_NO
      FROM AVIEWER.V_EXAM_REPORT_MASTER
     WHERE VALID_FLAG = '1'
       AND VISIT_TYPE = '2'
       AND PATIENT_ID = '0000886536'
       AND VISIT_NO = '0113728006'
),
CD AS
(
    SELECT D.BILL_NO,
           D.HIS_BILL_NO
      FROM C_FILTER C
      JOIN ZOEODS_PACS.EXAM_REPORT_APPLY_IDX_REC D
        ON D.BILL_NO = C.BILL_NO
)
SELECT COUNT(1)
  FROM CD
  JOIN AVIEWER.V_APP_INP_APPLY_SHEET A
    ON A.APPLY_NO = CD.HIS_BILL_NO
  JOIN AVIEWER.V_DIC_BILL_TYPE_DICT B
    ON B.BILL_TYPE_CODE = A.BILL_TYPE_CODE
 WHERE A.APPLY_SHEET_STATUS_CODE <> '9'
   AND A.PRES_NO IS NOT NULL
   AND (B.ITEM_CLASS_CODE LIKE 'E%' OR B.ITEM_CLASS_CODE LIKE 'M%')
   AND A.ITEM_CODE <> '133241'
   AND (A.REPORT_STATUS_CODE NOT IN ('77', '66')
        OR A.REPORT_STATUS_CODE IS NULL);

这段 SQL 的目的不是直接作为最终优化方案,而是把关联关系尽量写清楚,方便验证:

text 复制代码
C_FILTER
↓
D
↓
A
↓
B

5.2 验证结果

待补充:执行时间、执行计划、实际 JOIN 顺序。

如果改写后仍然没有恢复到预期毫秒级,则说明 JOIN 顺序不合理只是现象之一,还需要继续往下缩小问题范围。

6. 继续缩小范围:只验证关键的 A、D 关联

四表关联中,最关键的一段是:

sql 复制代码
A.APPLY_NO = D.HIS_BILL_NO

因此继续缩小 SQL,只保留 A、D 两个对象,判断问题究竟出在 JOIN 本身,还是 A 对象的访问路径上。

6.1 使用 A 视图测试

sql 复制代码
SELECT COUNT(1)
  FROM AVIEWER.V_APP_INP_APPLY_SHEET A
  JOIN ZOEODS_PACS.EXAM_REPORT_APPLY_IDX_REC D
    ON A.APPLY_NO = D.HIS_BILL_NO
 WHERE D.BILL_NO = '待替换为实际 BILL_NO';

记录:

text 复制代码
执行时间:
执行计划:
A 是否走 APPLY_NO 相关索引:

6.2 将 A 视图替换为底层物理表测试

sql 复制代码
SELECT COUNT(1)
  FROM ZOEODS_HIS.APP_INP_APPLY_SHEET A
  JOIN ZOEODS_PACS.EXAM_REPORT_APPLY_IDX_REC D
    ON A.APPLY_NO = D.HIS_BILL_NO
 WHERE D.BILL_NO = '待替换为实际 BILL_NO';

记录:

text 复制代码
执行时间:
执行计划:
A 是否走 APPLY_NO 主键/索引:

如果直接使用物理表时可以正常利用 APPLY_NO 的主键/索引,而换成 V_APP_INP_APPLY_SHEET 视图后无法走上相同访问路径,则问题范围可以进一步缩小到 A 视图本身。

7. 检查 A 视图定义

查看 AVIEWER.V_APP_INP_APPLY_SHEET 定义,发现两个 UNION ALL 分支都对 APPLY_NO 做了类型转换:

sql 复制代码
SELECT
    TO_CHAR(APPLY_NO) APPLY_NO,
    ...
FROM ZOEODS_HIS.APP_INP_APPLY_SHEET

UNION ALL

SELECT
    TO_CHAR(APPLY_NO) APPLY_NO,
    ...
FROM ZOEODS_HIS.APP_INP_APPLY_SHEET_POOL;

而原 SQL 中 A、D 的关键关联条件是:

sql 复制代码
A.APPLY_NO = D.HIS_BILL_NO

通过视图后,外层看到的 A.APPLY_NO 实际已经不是底层物理表中的原始 APPLY_NO 列,而是:

sql 复制代码
TO_CHAR(APPLY_NO)

因此实际关联逻辑相当于:

sql 复制代码
TO_CHAR(APP_INP_APPLY_SHEET.APPLY_NO) = D.HIS_BILL_NO

底层 APPLY_NO 原有主键/索引建立在原始列上,当列被 TO_CHAR() 包裹后,原有索引无法像直接使用裸列等值关联时一样被高效利用。

这会影响原本希望形成的访问路径:

text 复制代码
C
↓
D
↓ 通过 HIS_BILL_NO
A.APPLY_NO 主键/索引定位
↓
A

8. 当前结论

本次性能问题从执行计划上首先表现为:

  • JOIN 顺序不理想;
  • C 表高选择性条件没有尽早发挥作用;
  • A 表产生大量中间结果;
  • 最终节点返回 0 行,前面的海量数据处理成为无效工作。

继续通过 SQL 简化和"视图 / 物理表"对比后,将问题逐步缩小到 V_APP_INP_APPLY_SHEET

最终发现该视图对关键关联字段 APPLY_NO 使用了:

sql 复制代码
TO_CHAR(APPLY_NO)

类型转换,导致外层 A.APPLY_NO = D.HIS_BILL_NO 实际作用在函数表达式上,影响底层 APPLY_NO 主键/索引的正常使用。

因此,JOIN 顺序异常更像是最终表现出来的现象;更深层的原因是 A 视图对关联字段做了类型转换,使得原本可以通过 D.HIS_BILL_NO → A.APPLY_NO 进行高效级联定位的访问路径无法正常形成。


后续待补充:

  1. JOIN 顺序验证 SQL 的实际执行计划和耗时;
  2. A-D 视图测试与物理表测试的执行计划截图;
  3. APP_INP_APPLY_SHEET.APPLY_NO 主键/索引定义;
  4. 修正视图或统一字段类型后的最终验证结果。
相关推荐
渡我白衣32 分钟前
深入理解 Transformer:Transformer 究竟是什么?
java·linux·开发语言·c++·人工智能·深度学习·transformer
Tisfy3 小时前
LeetCode 1927.求和游戏:抵消+看最值
java·leetcode·游戏·题解·博弈论
Elastic 中国社区官方博客7 小时前
搜索倍增器:推动收入、生产力和 AI 实现规模化
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·全文检索
保定公民9 小时前
达梦数据库存储过程中的数组类型详解:基础数组与记录数组的差异化应用
数据库·达梦·存储过程·达梦数据库·dm8·dm
秋饼10 小时前
JDK 27 企业级 AI 服务落地实战:G1 默认、紧凑对象头、后量子 TLS 与 JFR 脱敏全解析
java·ai·技术分享·后端开发
梦Arrebol10 小时前
Redis 内容及相关实验
数据库·redis
卓怡学长10 小时前
w176基于SpringBoot的医院管理系统
java·spring boot·spring·maven·intellij-idea
lemon_sjdk10 小时前
ObjectProperty
java·开发语言·算法
大模型码小白10 小时前
Spring AI Tool 实现自然语言操作 MySQL 数据库详解
服务器·开发语言·数据库·人工智能·python·mysql·spring