首页 > 数据库 > Oracle > 生产环境DBLINK查询执行计划失控排查与优化
生产环境DBLINK 查询执行计划失控排查与优化
大家好,我是睿。
前段时间,某客户的生产系统里出现了一条卡住的跨库报表查询。业务方第一反应是:是不是数据库缺索引?
这句话在慢查询现场很常见。但这次如果只看索引,方向很容易跑偏。真正的问题不是本地索引,也不是远程索引数量,而是分布式执行计划把远程表的返回行数估算错了,随后选择了一个不该选择的连接方式。
这次处理链路大致如下:
远程表统计信息失真
↓
优化器低估远程结果集行数
↓
本地表与远程结果做 MERGE JOIN CARTESIAN
↓
中间结果集放大,明细数据被拉回生产端
↓
CPU 、 TEMP 和网络压力同时上升
↓
通过 Hint 和分步聚合控制执行站点与回传数据量
本文中的客户名称、数据库名、Schema 、表名、列名、 DBLINK 名称和参数值都已替换为通用业务场景; SQL 只保留技术结构,不包含真实业务数据。
现场 SQL 是一条典型的跨库查询,涉及两张远程历史库表 T_ORDER 、 T_DETAIL ,以及一张本地基础表 T_COMPANY 。远程对象统一通过 DBLINK_HIST 访问。
原始 SQL 的结构大概如下:
SELECT c.company_name,
SUM(t.pay_amount / 100)
FROM PROD_DB.T_ORDER@DBLINK_HIST t,
PROD_DB.T_DETAIL@DBLINK_HIST a,
PROD_DB.T_COMPANY c
WHERE t.biz_type = '06'
AND a.finish_time BETWEEN '2019-01-01 00:00:00'
AND '2020-01-01 00:00:00'
AND a.ticket_no IS NULL
AND t.order_no = a.plan_id
AND t.company_id = c.company_code
GROUP BY c.company_name;
拉取执行计划后,问题就很明显了:
SELECT STATEMENT
Cost: 150,556
└── HASH GROUP BY
└── NESTED LOOPS
├── MERGE JOIN CARTESIAN
│ ├── REMOTE
│ │ └── T_DETAIL@DBLINK_HIST
│ │ Cardinality: 1
│ └── BUFFER SORT
│ └── TABLE ACCESS FULL
│ └── T_COMPANY
│ Cardinality: 7,998
└── REMOTE
└── T_ORDER@DBLINK_HIST
优化器把远程表 T_DETAIL 的返回行数估算成了 1 行,本地表 T_COMPANY 估算为 7,998 行。基于这个判断,优化器认为先做一次笛卡尔积成本不高,再和 T_ORDER 做嵌套循环也能接受。
问题在于,实际历史明细可能有数万行。即使最终过滤后只剩一部分,中间结果一旦被放大到数千万甚至数亿行,生产端的 CPU 、临时空间和网络都会受到冲击。
这里要注意,Cost: 150,556 只是优化器的成本估算,不是实际耗时。真正危险的是估算行数和实际行数出现数量级偏差,执行计划因此走错了路。
同事问我是不是缺索引,我的回答是:先别急着加,当前执行计划更像是远程统计信息失真导致的连接顺序错误。
跨 DBLINK 查询和单库查询最大的区别,是优化器对远程侧信息的掌握程度不同。对于本地表,统计信息、索引、列分布和实际执行成本都比较清楚;对于远程表,生产库看到的更接近一个黑盒。
如果远程统计信息过期,或者关键列没有直方图,优化器就可能把一个本来有几万行的结果集估成 1 行。行数估错以后,连接方式、连接顺序和执行站点都会跟着错。
索引当然有价值,但它解决的是访问 路径 问题,不一定能纠正基数估算。一个本来应该返回数万行的远程查询,不会因为本地多了一个索引,就自动变成一个应该回传几十行的小结果集。
所以在跨库查询里,第一步不是问 “ 缺不缺索引 ” ,而是回答下面几个问题:
1. 远程表的统计信息是否有效?
2. 执行计划里哪些步骤在远程端完成?
3. 哪些数据需要经过 DBLINK 回传?
4. 中间结果集有没有被错误估算和放大?
先把这四个问题看清楚,再决定要不要加索引,顺序才正确。
跨库查询的优化,不能只收集生产端本地表的统计信息。两张大数据表都在远程历史库,真正决定过滤和连接成本的信息也在远程。
现场处理时,需要在远程库本地,或者在确认权限和链路条件允许的情况下通过远程调用,重新收集相关表、索引和关键列的统计信息,重点关注:
T_ORDER : biz_type 、 order_no 、 company_id
T_DETAIL : plan_id 、 finish_time 、 ticket_no
T_COMPANY: company_code
如果连接列、时间列或状态列的数据分布明显倾斜,还要检查直方图是否合适。这里的重点不是 “ 统计信息一收就一定能解决 ” ,而是先让优化器拿到接近真实的行数,再重新评估计划。
原 SQL 使用逗号连接,几个表之间的关联关系不够直观。排查阶段先把隐式连接改成显式 INNER JOIN ,更容易看清哪个条件对应哪两张表。
SELECT c.company_name,
SUM(t.pay_amount / 100) AS total_amount
FROM PROD_DB.T_ORDER@DBLINK_HIST t
INNER JOIN PROD_DB.T_DETAIL@DBLINK_HIST a
ON t.order_no = a.plan_id
INNER JOIN PROD_DB.T_COMPANY c
ON t.company_id = c.company_code
WHERE t.biz_type = '06'
AND a.finish_time BETWEEN '2019-01-01 00:00:00'
AND '2020-01-01 00:00:00'
AND a.ticket_no IS NULL
GROUP BY c.company_name;
改写后,SQL 的结构更清楚,但不要误以为写成 INNER JOIN 就一定会改变执行计划。它首先解决的是可读性和后续控制问题,真正的执行方式还要看优化器怎么选。
两张大数据表都在同一个远程历史库上。理想情况下,应该让远程端先完成 T_ORDER 和 T_DETAIL 的关联、过滤与聚合,再把少量汇总结果传回生产端。
可以通过 DRIVING_SITE 指定分布式查询块的驱动站点:
SELECT /*+ DRIVING_SITE(t) */
c.company_name,
SUM(t.pay_amount / 100) AS total_amount
FROM PROD_DB.T_ORDER@DBLINK_HIST t
INNER JOIN PROD_DB.T_DETAIL@DBLINK_HIST a
ON t.order_no = a.plan_id
INNER JOIN PROD_DB.T_COMPANY c
ON t.company_id = c.company_code
WHERE t.biz_type = '06'
AND a.finish_time BETWEEN '2019-01-01 00:00:00'
AND '2020-01-01 00:00:00'
AND a.ticket_no IS NULL
GROUP BY c.company_name;
DRIVING_SITE 控制的是执行站点,不等于直接指定 “ 小表驱动大表 ” 。采用这个 Hint 后,本地小表 T_COMPANY 可能需要被送到远程站点参与计算,适合的场景是本地表较小,而远程侧先关联、先过滤、先聚合能显著减少回传数据。
Hint 写上去并不代表任务结束。必须重新查看执行计划,确认:
1. 主要计算是否已经落到远程站点。
2. T_COMPANY 的传输量是否可控。
3. 返回生产端的是汇总结果,而不是大量明细。
4. 远程站点是否有权限和资源执行这个查询块。
其中第 4 点经常被忽略。如果远程站点无法访问本地对象,或者反向传输代价过高, DRIVING_SITE 就不是合适方案。
如果无法直接控制执行站点,可以先用 CTE 或内联视图把两张远程明细表收口到公司维度,再回生产端关联基础表。
WITH remote_data AS
(
SELECT t.company_id,
SUM(t.pay_amount / 100) AS amount
FROM PROD_DB.T_ORDER@DBLINK_HIST t
INNER JOIN PROD_DB.T_DETAIL@DBLINK_HIST a
ON t.order_no = a.plan_id
WHERE t.biz_type = '06'
AND a.finish_time BETWEEN '2019-01-01 00:00:00'
AND '2020-01-01 00:00:00'
AND a.ticket_no IS NULL
GROUP BY t.company_id
)
SELECT c.company_name,
SUM(r.amount) AS total_amount
FROM remote_data r
INNER JOIN PROD_DB.T_COMPANY c
ON r.company_id = c.company_code
GROUP BY c.company_name;
这里必须说明一点:CTE 本身不保证下推, Oracle 可能把结果拉回本地后再执行。它只是把 “ 先聚合明细、再关联基础表 ” 的逻辑写清楚,最终是否真正减少 DBLINK 回传量,仍然要以实际执行计划为准。
如果 CTE 被物化后反而增加了成本,可以改用内联视图,或者结合 MATERIALIZE 等 Hint 做对比测试。生产环境不能只看 SQL 长得合理,要看计划里每一步在哪里执行。
当统计信息修正、SQL 结构明确、执行站点也确认以后,再回到索引问题。
针对这次查询,远程侧可以重点检查下面两组复合索引是否合理:
T_ORDER : (biz_type, order_no, company_id)
T_DETAIL : (plan_id, finish_time, ticket_no)
本地 T_COMPANY 的 company_code 也应有合适的唯一约束或索引。
但索引不是越多越好。跨库查询里,如果大量明细必须回到生产端,增加远程索引只能改善远程读取速度,解决不了网络传输和本地聚合压力。真正要控制的是数据在哪一端计算,以及有多少行会跨越 DBLINK 。
这次问题解决以后,我们进一步梳理了生产端的管控要求。生产库不能允许业务代码随意通过 DBLINK 查询大量历史数据,至少要从三个层面限制。
第一,应用和 SQL 层要有硬约束。
跨库查询必须带明确的时间范围,不能因为参数为空就退化成全量扫描。查询结果应该限制在可接受的规模内,分页和限流由服务层实现,不要用 ROWNUM 掩盖一条本来就会扫描大量数据的 SQL 。涉及历史库的查询最好封装成独立服务,统一做参数校验、限流、熔断和降级。
第 二 ,DBA 要做兜底监控和资源隔离。
可以对跨库查询设置资源限制,监控单条 SQL 的 CPU 、 TEMP 、逻辑读和 DBLINK 传输量。对于已经优化稳定的关键 SQL ,可以考虑用 SQL Plan Management 固定执行计划,避免统计信息变化后再次走回笛卡尔积。
这次问题的关键,不是 “ 加不加索引 ” ,而是先看执行计划里有没有 MERGE JOIN CARTESIAN ,再看远程表的行数估算为什么失真,最后决定数据应该在远程端还是生产端完成计算。
跨 DBLINK 查询最怕的,不是 SQL 写得长,而是优化器不知道远程表到底有多少行。统计信息是基础, DRIVING_SITE 是控制执行站点的手段, CTE 或内联视图是收口结果集的方式,索引则应该在访问路径明确之后再评估。
下次遇到类似的慢查询,可以先看一眼执行计划里有没有笛卡尔积,再确认数据到底在哪里完成关联和聚合。
点赞 + 转发 + 关注,下期我们继续分享!
—— 睿 | Oracle 性能优化老司机
专注硬核干货,欢迎一起卷技术!

来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70047128/viewspace-3183419/ ,如需转载,请注明出处,否则将追究法律责任。