在做报表开发的时候,有时我们会遇到这样的需求:不同权限的账户各自对应不同的权限,从而看到不同的数据,这时我们一般会采用 Row Level Security 实现这样的需求。2011 年,作者罗老师在惠普的时候,遇到过多起 Row Level Security 引发的 SQL 性能问题。

    Obiee 报表开发人员发来邮件反映,使用权限较低的账户打开报表非常缓慢,报表运行了 15 分钟还没响应;而使用权限最高的账户,报表可以在 16 秒内执行完毕。执行缓慢的 SQL 代码如下。

    1. select sum(nvl(T1796547.ACTL_GIV_AMT , 0)) as c1,
    2. T1792779.ACCT_LONG_NAME as c2,
    3. T1792779.NAME as c3,
    4. T1796631.PRMTN_NAME as c4,
    5. T1796631.PRMTN_ID as c5,
    6. case when case when T1796631.CORP_PRMTN_TYPE_CODE = 'Target Account' then 'Corporate' else T1796631.CORP_PRMTN_TYPE_CODE end is null then 'Private' else
    7. case when T1796631.CORP_PRMTN_TYPE_CODE = 'Target Account' then 'Corporate' else T1796631.CORP_PRMTN_TYPE_CODE end end as c6,
    8. T1796631.PRMTN_STTUS_CODE as c7,
    9. T1796631.APPRV_BY_DESC as c8,
    10. T1796631.APPRV_STTUS_CODE as c9,
    11. T1796631.AUTO_UPDT_GTIN_IND as c10,
    12. T1796631.CREAT_DATE as c11,
    13. T1796631.PGM_START_DATE as c12,
    14. T1796631.PGM_END_DATE as c13,
    15. nvl(case when T1796631.PRMTN_STTUS_CODE = 'Confirmed' then cast(( TRUNC( TO_DATE('2011-04-26' , 'YYYY-MM-DD') ) - TRUNC( T1796631.PGM_END_DATE ) ) as
    16. VARCHAR ( 10 ) ) end , '') as c14,
    17. T1796631.PRMTN_STOP_DATE as c15,
    18. T1796631.SHPMT_START_DATE as c16,
    19. T1796631.SHPMT_END_DATE as c17,
    20. T1796631.CNBLN_WK_CNT as c18,
    21. T1796631.ACTVY_DETL_POP as c19,
    22. T1796631.CMMNT_DESC as c20,
    23. T1796631.PRMTN_AVG_POP as c21,
    24. T1792779.CHANL_TYPE_DESC as c22,
    25. T1796631.PRMTN_SKID as c23
    26. from
    27. OPT_ACCT_FDIM T1792779 /* OPT_ACCT_PRMTN_FDIM */ ,
    28. OPT_BUS_UNIT_FDIM T1796263,
    29. OPT_CAL_MASTR_DIM T1796564 /* OPT_CAL_MASTR_DIM01 */ ,
    30. OPT_PRMTN_FDIM T1796631,
    31. OPT_BASLN_FCT T1796547
    32. where ( T1792779.ACCT_SKID = T1796547.ACCT_SKID
    33. and T1792779.BUS_UNIT_SKID = T1796547.BUS_UNIT_SKID
    34. and T1796263.BUS_UNIT_SKID = T1796547.BUS_UNIT_SKID
    35. and T1796547.WK_SKID = T1796564.CAL_MASTR_SKID
    36. and T1796547.BUS_UNIT_SKID = T1796631.BUS_UNIT_SKID
    37. and T1792779.ACCT_LONG_NAME = 'FN-AEON_GROUP(JUSCOJ4)(C005) - 1900001326'
    38. and T1796263.BUS_UNIT_NAME = 'Japan'
    39. and T1796547.PRMTN_SKID = T1796631.PRMTN_SKID
    40. and T1796564.FISC_YR_ABBR_NAME = 'FY10/11'
    41. and T1792779.ACCT_LONG_NAME is not null
    42. -- add RLS
    43. and T1796547.acct_skid IN (select org.org_skid from (SELECT DISTINCT ap.org_skid
    44. FROM opt_acct_postn_lkp ap, opt_party_persn_lkp pp, opt_user_lkp u
    45. WHERE ap.postn_id = pp.party_id
    46. AND pp.persn_id = u.user_id
    47. AND u.login_name = 'BT0016'
    48. union select 0 as org_skid
    49. from sys.dual) org
    50. )
    51. and T1792779.bus_unit_skid IN (0,11769,11772,11774,11777,11779,11780,14329,14334,14339,14340,14341,14350,14800,14801)
    52. and T1796547.bus_unit_skid IN (0,11769,11772,11774,11777,11779,11780,14329,14334,14339,14340,14341,14350,14800,14801)
    53. and T1796263.bus_unit_skid IN (0,11769,11772,11774,11777,11779,11780,14329,14334,14339,14340,14341,14350,14800,14801)
    54. and T1796631.bus_unit_skid IN (0,11769,11772,11774,11777,11779,11780,14329,14334,14339,14340,14341,14350,14800,14801)
    55. -- end RLS
    56. and (case when T1796631.CORP_PRMTN_TYPE_CODE = 'Target Account' then 'Corporate'
    57. else T1796631.CORP_PRMTN_TYPE_CODE end in ('Corporate', 'Planned', 'Private'))
    58. and T1796631.PRMTN_LONG_NAME in (select distinct T1796631.PRMTN_LONG_NAME as c1
    59. from
    60. OPT_ACCT_FDIM T1792779 /* OPT_ACCT_PRMTN_FDIM */ ,
    61. OPT_BUS_UNIT_FDIM T1796263,
    62. OPT_CAL_MASTR_DIM T1796564 /* OPT_CAL_MASTR_DIM01 */ ,
    63. OPT_PRMTN_FDIM T1796631,
    64. OPT_PRMTN_PROD_FLTR_LKP T1796906
    65. where ( T1792779.ACCT_SKID = T1796906.ACCT_PRMTN_SKID
    66. and T1792779.BUS_UNIT_SKID = T1796906.BUS_UNIT_SKID
    67. and T1796263.BUS_UNIT_SKID = T1796906.BUS_UNIT_SKID
    68. and T1796564.CAL_MASTR_SKID = T1796906.DATE_SKID
    69. and T1796631.PRMTN_SKID = T1796906.PRMTN_SKID
    70. and T1792779.ACCT_LONG_NAME = 'FN-AEON_GROUP(JUSCOJ4)(C005) - 1900001326'
    71. and T1796263.BUS_UNIT_NAME = 'Japan' and T1796564.FISC_YR_ABBR_NAME = 'FY10/11'
    72. and T1796631.BUS_UNIT_SKID = T1796906.BUS_UNIT_SKID
    73. and (case when T1796631.CORP_PRMTN_TYPE_CODE = 'Target Account' then 'Corporate' else T1796631.CORP_PRMTN_TYPE_CODE end in ('Corporate', 'Planned',
    74. 'Private')) and ROWNUM >= 1 ) ) )
    75. group by T1792779.NAME, T1792779.CHANL_TYPE_DESC,
    76. T1792779.ACCT_LONG_NAME, T1796631.PRMTN_SKID,
    77. T1796631.PRMTN_ID, T1796631.PRMTN_NAME,
    78. T1796631.SHPMT_END_DATE, T1796631.SHPMT_START_DATE,
    79. T1796631.PRMTN_STTUS_CODE, T1796631.APPRV_STTUS_CODE,
    80. T1796631.CMMNT_DESC, T1796631.PGM_START_DATE,
    81. T1796631.PGM_END_DATE, T1796631.CREAT_DATE,
    82. T1796631.APPRV_BY_DESC, T1796631.AUTO_UPDT_GTIN_IND,
    83. T1796631.PRMTN_STOP_DATE, T1796631.ACTVY_DETL_POP,
    84. T1796631.CNBLN_WK_CNT, T1796631.PRMTN_AVG_POP,
    85. case when case
    86. when T1796631.CORP_PRMTN_TYPE_CODE = 'Target Account'
    87. then 'Corporate' else T1796631.CORP_PRMTN_TYPE_CODE end is null
    88. then 'Private' else case when
    89. T1796631.CORP_PRMTN_TYPE_CODE = 'Target Account' then 'Corporate'
    90. else T1796631.CORP_PRMTN_TYPE_CODE end end , nvl(case when
    91. T1796631.PRMTN_STTUS_CODE = 'Confirmed'
    92. then cast(( TRUNC( TO_DATE('2011-04-26' , 'YYYY-MM-DD') ) - TRUNC( T1796631.PGM_END_DATE ) ) as VARCHAR ( 10 ) )
    93. end , '')
    94. order by c23, c2;

    执行缓慢的 SQL 与执行较快的 SQL 相比,缓慢的 SQL 在 where 条件中多了以下部分代码。

    1. -- add RLS
    2. and T1796547.acct_skid IN (select org.org_skid from (SELECT DISTINCT ap.org_skid
    3. FROM opt_acct_postn_lkp ap, opt_party_persn_lkp pp, opt_user_lkp u
    4. WHERE ap.postn_id = pp.party_id
    5. AND pp.persn_id = u.user_id
    6. AND u.login_name = 'BT0016'
    7. union select 0 as org_skid
    8. from sys.dual) org
    9. )
    10. and T1792779.bus_unit_skid IN (0,11769,11772,11774,11777,11779,11780,14329,14334,14339,14340,14341,14350,14800,14801)
    11. and T1796547.bus_unit_skid IN (0,11769,11772,11774,11777,11779,11780,14329,14334,14339,14340,14341,14350,14800,14801)
    12. and T1796263.bus_unit_skid IN (0,11769,11772,11774,11777,11779,11780,14329,14334,14339,14340,14341,14350,14800,14801)
    13. and T1796631.bus_unit_skid IN (0,11769,11772,11774,11777,11779,11780,14329,14334,14339,14340,14341,14350,14800,14801)
    14. -- end RLS

    这部分代码就是实现 Row Level Security 功能的代码,对于权限较低的账户过滤掉一部分数据,而对于权限最高的账号不做过滤。如果不加 RLS 代码,报表能在 16 秒内执行完毕,但是增加了 RLS 代码,报表执行了 15 分钟不出结果。通过以上信息,我们判断是由于增加了 RLS 代码,导致执行计划发生了变化,从而导致 SQL 性能问题。

    RLS 代码中有一个 in 子查询,in 子查询中有 union 关键字。在第 7 章中讲到过子查询非嵌套,当 where 条件中有子查询,优化器会尝试将子查询展开,从而消除 Filter。in 子查询中有 union 是可以展开的(unnest),而 exists 子查询中有 union 是不可以展开的。如果 where 条件中的子查询不能展开(no_unnest),执行计划中会出现 Filter,Filter 一般是在 SQL 的最后阶段执行。如果 where 条件中的子查询展开了,子查询会与主表提前关联。

    因为增加了 RLS 代码导致 SQL 产生了性能问题,RLS 代码中有 in 子查询,因为 in 子查询可以展开(unnest),所以我们推断是优化器的子查询非嵌套(Subquery Unnesting)导致产生的性能问题,让 Obiee 开发人员在 in 子查询中添加 HINT:NO_UNNEST,让子查询不展开。子查询不展开,执行计划中就会出现 Filter,但是 Filter 是在最后进行过滤,子查询不展开就不会干扰原始的(跑得快的)执行计划,只是在跑得快的执行计划的最后一步添加 Filter 过滤而已。添加完 HINT 之后,SQL 能在 12 秒内执行完毕。

    因为子查询中有 union,这里也可以不添加 HINT:NO_UNNEST,将 in 改写为 exists,这时优化器会自动走 Filter,也能达到优化目的。需要提醒大家的是,千万不要因为我们将 in 改写为 exists、exists 执行快就说 exists 性能比 in 高。如果有谁遇到本案例,将 in 改写为 exists,然后发布博客说今天又用 exists 优化了 in 子查询,这只会让人贻笑大方。

    罗老师的个人技术博客中还记录了另一个 RLS 引发的性能问题,大家如有兴趣也可以查看网页:ROW LEVEL SECURITY(RLS) 性能问题