第一章 适用范围
本案例中的问题现象发生于当前主流的ORACLE 11G环境。不排除随着后续版本的升级可能会有一定细微的差异表现。但问题的原因及解决方案,在所有版本中都是通用的。
数据库版本:ORACLE 11G
第二章 问题概述
问题SQL的主要现象是对同一个表访问了两次或多次,且关联列通过主键关联。产生的结果是对同一个表的多次访问。造成性能问题。
一般内层SQL在完成相应过滤后,如果外层有对其中部分表的主键关联访问,一般都是可以省去的,主键代表着非空唯一,因此内部的一条记录一定与外部相同表的记录一一对应。保证了数据既不会多也不会少。如果外部查询还有相应条件的话,也可以考虑放入内部做处理。
下面通过SQL案例进行说明:
SELECT *
FROM T1_USER A
WHERE USER_ID IN (SELECT A.USER_ID
FROM T1_USER A
LEFT JOIN T1_BIND B
ON LPAD(A.USER_ID, 32) = B.USER_ID
WHERE B.AGENT_CODE IS NULL
AND A.PLATFORM = '8')
AND TO_CHAR(A.INSERT_TIME, 'yyyy-mm-dd') = :B1;
分析SQL语句结构,内部子查询中通过关联两个表获得内层结果,之后与外部查询再次通过USERID做关联后,通过INSERT_TIME做过滤返回最后结果。
两次访问同一张表,意味着消耗双倍的表资源访问。查看执行计划:

可以看到两次访问同一张表,且通过全表访问。造成SQL执行非常低效。
第三章 问题分析优化
出现上述执行低效的问题原因就是重复的表访问。
对内层而言,A表是对主键索引取前32个字符。经过分析测试及与业务沟通,该表LPAD(A.USER_ID, 32)与A.USER_ID列在数据上完全等价。只是由于字段设计为CHAR(64),为了去掉空格才这样写法。

B表的连接条件也是唯一键。

这样关联后,保证了内层查询不会出现重复记录。
对外层而言:
A表是通过主键索引与外层表做关联。主键索引的同表访问,是可以把外层条件放入内层,并去掉外部表的再次访问的。
调整后的SQL语句如下:
SELECT A.*
FROM T1_USER A
LEFT JOIN T1_BIND B
ON LPAD(A.USER_ID, 32) =B.USER_ID
WHERE B.AGENT_CODE IS NULL
AND A.PLATFORM = '8'
AND TO_CHAR(A.INSERT_TIME, 'yyyy-mm-dd') = '2022-08-15';
分析调整后的执行计划:

对于T1_USER表只访问了一次,且由于查询条件的增加,自动使用上了相应的索引。进一步降低了全表扫描的影响。性能提升有10倍以上。
分析是否还有其余优化空间:
可以看到,上面的T1_USER表预估行数与实际的差异较大。预估返回行数较多,导致了与T1_BIND的访问通过HASH连接。
出现问题的原因来自于条件列的函数访问:
TO_CHAR(A.INSERT_TIME, ‘yyyy-mm-dd’) = ‘2022-08-15’
去掉上述函数写法。改为如下测试:
SELECT A.*
FROM T1_USER A
LEFT JOIN T1_BIND B
ON LPAD(A.USER_ID, 32) =B.USER_ID
WHERE B.AGENT_CODE IS NULL
AND A.PLATFORM = '8'
and A.INSERT_TIME>=date'2022-08-15' and A.INSERT_TIME<date'2022-08-15'+1;
查看调整后的执行计划:

调整后,预估行数变小了,对于被驱动表,也从HASH关联调整为NL连接,且有合适的索引给与支撑。提升效率明显。
另外查看T1_USER表,索引访问也去掉了filter部分改为使用access。进一步得到了性能提升。
第四章 解决方案
经过上述章节的分析,主要通过以下手段优化:
- 对于主键关联的表,降低重复访问,并将条件放入最内层;
- 去除索引列上的函数动作,提升查询效率。
第五章 问题总结
本案例中,最重要的优化是去掉了重复的表访问,此时将所有的过滤条件都放入了最内层执行。数据库是会考虑所有的路径走出相对优异的执行计划的(省去一次表访问并改为索引访问)。
正是这一步的改进,再结合后续的去掉索引列的函数转换动作。进一步提升了SQL的执行效率。




