陈顼
大家好,我是来自天善BI社区的老头子,专注于 BI 方向,是个 Oracle 爱好者,同时也是 ACOUG 成员。今天想给大家分享一个SQL性能方面的话题 — 不合理视图合并引发的性能问题。
在开始分享具体案例之前,首先我们需要了解两个概念:视图合并和笛卡尔积。下面我来分别解释一下:
视图合并:视图合并是 SQL 算法生成时所发生的一种查询转换,这表示 CBO 在确保查询结果正确的前提下,为了产生更好的执行计划而隐式的等价改写了 SQL。而视图合并的改写方式则是就是 CBO 把不同查询块中的对象拆分重新组合后,再把本来没有直接关联关系的表进行重写为 CBO 认为最优的表关联顺序。如果在一个查询块中使用了如聚集合运算、rownum 等这种需要整体计算的函数的时候,CBO 就无法进行视图合并,因为如果进行合并改写则可能会引起结果不正确。
如:当存在以下情况时,CBO 则无法进行视图合并操作:
- 内联视图中含有集合运算:UNION, UNION ALL, INTERSECT, MINUS 等
- 聚集函数:AVG, COUNT, MAX, MIN, SUM 等
- rownum、connect by、rollup、cube 等
在 CBO 允许的情况下,我们可以使用/*+ MERGE(V)*/的hint来强制视图合并,但如果 Oracle 发现改写后 SQL 的结果和原始的不一致,那么即便加了hint也无法改变执行计划。
同上,在结果集不变的前提下,强制禁止视图合并的 Hint:/*+ NO_MERGE */
如果 Oracle 没有做视图合并,那么我们可以在执行计划中看到 view 关键字(特殊情况除外,如临时视图等)
视图合并大概介绍这么多,大家应该有个大概的了解,那么我们为什么还要讲笛卡儿积呢?因为笛卡儿积是在 SQL 性能问题中一个典型的性能故障点,而在我后面分享的案例中,就遭遇了由于不当的视图合并导致错误的笛卡儿积算法。
笛卡尔积:又称直积,两个集合X和Y的笛卡尔积表示为 X * Y,是所有X和Y可能组合的集合。在数据库中表现为表A和表B没有任何关联条件而产生的结果集。
例如:
X 集合有3条数据{1,2,3}
Y集合有3条数据{3,4,5}
那么X和Y的笛卡尔积为:{(1,3), (1,4), (1,5), (2,3), (2,4), (2,5), (3,3), (3,4), (3,5)} 共9组数据。
但并非所有笛卡尔积都是有问题的,如:关联的其中一个集合只有一条数据时,此时笛卡尔积就不会引发性能问题,因为当X = 1时,X * Y = Y。
我之前的博客曾经写过对典型笛卡尔积引发性能问题的优化案例:
这里再简单介绍下,例如:A、B、C三表做连接查询:
SELECT * FROM A AA, B BB, C CC
WHERE AA.ID = BB.ID
AND BB.ID = CC.ID
那么根据 SQL 来看正确的关联顺序应该是A先和B表关联,再和C表关联。
然而由于统计信息不准确,或 CBO bug 引起的A和C两表走了关联,由于A 和 C 之间没有关联条件,从而导致低效的笛卡尔积。
基于上面的介绍,我分享一个简单的小案例由于视图合并而引起的低效笛卡尔积从而导致 SQL 性能低下。
案例背景:
表数据量:
ZQGS:88
H1H2H3:518459
HDB:81
CCOD_PROV_INFO:32
CC_CITY_INFO:335
SQL 如下,要跑 20s:
SELECT COUNT(Z.REMOTE_URL)
FROM ZQGS Z,
(SELECT H.H1H2H3
FROM H1H2H3 H, HDB D, CCOD_PROV_INFO P, CC_CITY_INFO C
WHERE H.AREA_CODE = C.CITY_ID
AND C.PROV_ID = P.PROV_ID
AND P.PROV_ID = 1
AND H.H1H2H3 LIKE D.HD || '%'
AND D.ID = '1') M
WHERE (Z.END_TYPE = 255 OR Z.END_TYPE = 254)
AND Z.REMOTE_URL LIKE 'TEL:' || M.H1H2H3 || '%';
COUNT(Z.REMOTE_URL)
——————-
21
Elapsed: 00:00:20.21
预估执行计划及统计信息如下:
从执行计划中可以看出,SQL 走了2个笛卡尔积(MERGE JOIN CARTESIAN)
第一个笛卡尔积 ID = 6:
我们暂不谈 CBO 评估是否有误,且认为 CBO 评估的 rows 是正确的,所以这里是因为 CBO 对 CCOD_PROV_INFO 评估只有1条数据(ID 为7的行,Rows 列的值评估为1),从而 CBO 评估笛卡尔积的连接方式代价比其他连接更低。所以这一个笛卡尔积,属于上文所提到的其中一个集合只有1条数据时,笛卡尔积是更高效的匹配方式。
第二个笛卡尔积 ID = 5:
这里就是由于 HDB 表和其他表并没有任何关联,而由于视图合并引发两个没有关联的表做了关联查询,从而引发的笛卡尔积。
由于开发人员说 SQL 中的主表是测试使用的表,即便优化后也无法模拟真实情况,于是在优化前换了个主表 TBPT,重新造了数据(表结构一样,接近真实数据量),数据量为18682454(其他表不变)
TBPT:18682454
H1H2H3:518459
HDB:81
CCOD_PROV_INFO:32
CC_CITY_INFO:335
SQL 如下:
SELECT /*+ GATHER_PLAN_STATISTICS */
COUNT(Z.REMOTE_URL)
FROM TBPT Z,
(SELECT H.H1H2H3
FROM H1H2H3 H, HDB D, CCOD_PROV_INFO P, CC_CITY_INFO C
WHERE H.AREA_CODE = C.CITY_ID
AND C.PROV_ID = P.PROV_ID
AND P.PROV_ID = 1
AND H.H1H2H3 LIKE D.HD || '%'
AND D.ID = '1') M
WHERE (Z.END_TYPE = 255 OR Z.END_TYPE = 254)
AND Z.REMOTE_URL LIKE 'TEL:' || M.H1H2H3 || '%';
主表从 ZQGS 的40条 , 增加到了 TBPT 表数据量的1800万条,基本跑不出数据来,所以超过5分钟我就把 SQL 中断掉了。
新的评估执行计划如下:
通过上面的执行计划看出,换了表的 SQL 依旧存在笛卡尔积,但由于数据量的改变,导致两个表的关联顺序发生了改变,所以性能故障点并没有改变,
- ID = 6 和 ID = 7 的两个笛卡尔积
- ID=12 的 TBPT 的全表扫描
根据 ID= 8 的 rows = 1得出 ID = 7 的笛卡尔积是高效关联。所以,此 SQL 最大的开销在 ID=6 的笛卡尔积,和 ID=12 的 TBPT 的全表扫描。所以要减少这两步的 cost 首先要让 SQL 走正确的关联路径,其次在 TBPT 表增加索引,减少全表扫描所带来的开销。
优化点:
1. 在内联视图中增加提示: /*+ NO_MERGE */ 禁止视图合并,目的是防止无关联的表进行关联,而引起低效的笛卡尔积。
关于 NO_MERGE 的案例,大家也可以参考杨老师很早以前写过的一个博文:
2. 主表 TBPT 的 remote_url 字段增加索引,目的是增加嵌套循环被驱动表的扫描速度(也可以建立 remote_url + entd_type 的组合索引,这样在 CBO 模式下,很可能选择快速索引扫描替代全表扫描,执行代价会更低)。
CREATE INDEX INDEX_TBPT_URL ON TBPT(REMOTE_URL);
SELECT /*+ GATHER_PLAN_STATISTICS */
COUNT(Z.REMOTE_URL)
FROM TBPT Z,
(SELECT /*+ NO_MERGE*/ H.H1H2H3
FROM H1H2H3 H, HDB D, CCOD_PROV_INFO P, CC_CITY_INFO C
WHERE H.AREA_CODE = C.CITY_ID
AND C.PROV_ID = P.PROV_ID
AND P.PROV_ID = 1
AND H.H1H2H3 LIKE D.HD || '%'
AND D.ID = '1') M
WHERE (Z.END_TYPE = 255 OR Z.END_TYPE = 254)
AND Z.REMOTE_URL LIKE 'TEL:' || M.H1H2H3 || '%';
优化后执行计划:
在这里可以看到 ID=4 的 View 关键字,说明我们已成功禁止视图的合并,让内联视图作为一个单独的整体查询后再进行和其他表的关联查询。
ID=13 的索引扫描也极大减少了 NestLoop 的成本
测试时间:1.45s
禁用视图合并 + 索引的优化效果:
40条数据 20s –> 1800 万条数据1.45s
优化点结论:
- 由于 CBO 的算法缺陷或统计信息的不完整,并非所有的查询改写都是优化,有可能有些不必要的视图合并,引起了更严重的性能问题。
- 嵌套循环的被驱动表中如果有索引将会大大提高扫描、关联的效率。