某日监控到数据库中有条DML语句对一个核心表锁表时间较长,查看造成长事务的这个语句的执行计划发现了一些问题,我们一起看一下该语句和执行计划(表名已做脱敏处理):
UPDATE T_TAB_ERROR E SET E.STAT = 'S0S'
WHERE E.PAY_ORG = :B1
AND E.ERROR_SOURCE = '1'
AND EXISTS
(SELECT 1
FROM T_TAB_ORDER O
WHERE 1 = 1
AND ((O.STAT IN ('S0R', 'S1R') AND O.ORDER_TYPE = 'OT001') OR
(O.STAT = 'S0C' AND O.ORDER_TYPE= 'OT501'))
AND O.ORDER_ID = E.ORDER_ID)
OR EXISTS (SELECT 1
FROM T_TAB_REFUND R
WHERE R.STAT = 'S0C'
AND R.ORDER_ID = E.ORDER_ID)
可以看到由于子查询之间使用了OR造成oracle优化器无法对子查询进行展开,这时oracle优化器只能按照如下方法生成执行计划:
1.在主查询上取一条记录,然后探测子查询
2.在主查询上再取一条记录然后再探测
3.直到主查询所有记录取完
这里T_TAB_ERROR是张大表,它的PAY_ORG,ERROR_SOURCE谓词列上没有可选的索引而且过滤效果差,经过全表扫描后大约还有348W条记录,作为一张"驱动表"数据量如此的大,
也就是说子查询中的T_TAB_REFUND和T_TAB_ORDER两表要进行348w次的在order_id上的主键查询性能当然比较差,这句sql锁表至少360秒
接下来我们通过使用merge代替update、用union all代替or的常用手段进行改写:
merge into T_TAB_ERROR E
using (select distinct order_id
from T_TAB_ORDER O
WHERE 1 = 1
AND (O.STAT IN ('S0R', 'S1R') AND O.ORDER_TYPE = 'OT001')
OR (O.STAT = 'S0C' AND O.ORDER_TYPE= 'OT501')
union all
select distinct order_id
from T_TAB_REFUND R
WHERE R.STAT = 'S0C') b
on (E.PAY_ORG ='PT0005' AND E.ERROR_SOURCE = '1' and e.order_id = b.order_id)
when matched then
update set STAT = 'S0S'
改写后的执行计划:
现在主查询和子查询进行HASH JOIN,执行时间降至18S!
案列二:
select * from A
where (A.id in x_subquery or A.id not in y_subquery )
and ( A.id in z_subquery or A.id not in w_subquery )
这里同样无法子查询展开,
x和y来自同一张表,而且x是y的子集,z和w来自同一张表 而且z是w的子集
A.id in x_subquery or A.id not in y_subquery具体是什么呢,画个等价图比较好理解,红色框的部分即是查询结果:
那么可以将sql改写为:
select * from A
where A.id not in ( y_subquery minus x_subquery)
and A.id not in ( w_subquery minus z_subquery)
改写后,A可以与子查询的结果做hash anti join,而不是无数次filter,效率大提升,
这个案列中执行时间从5天变成了10分钟!
总结:
where条件中OR出现的时候很容易出现子查询无法展开 可以使用union的方法来代替,避免因or造成执行计划选择filter表连接方式
普通update语句只适合驱动表为少量数据。如果驱动表过大,可考虑改下成merge into方式,走hash join