【问题标题】:Oracle stored procedure not using index when using case statement in a where clause在 where 子句中使用 case 语句时,Oracle 存储过程不使用索引
【发布时间】:2019-01-24 22:32:52
【问题描述】:

我需要查询一个表,并且可以在 WHERE 子句中传递一个或最多六个参数,具体取决于前端选择的选项。我在 WHERE 子句中使用了 CASE 语句来处理所有排列。

这里是SP代码sn-p:

create procedure return_data ( 
     p_field1 in varchar(20), 
     p_field2 in varchar2(30), 
     p_field3 in varchar2(30), 
     cur out sys_refcursor)
is 
begin
    open cur for
        select col1, col2, col3,col4,col5,col6 
        from master_table
        where (case when (p_field1 is null) then (1) 
                   when (p_field1 is not null) and (col1=p_field1) then 1 
                   else 0 end) =1 
       and (case when (p_field2 is null) then (1) 
                 when (p_field2 is not null) and (col2=p_field2) then 1 
                 else 0 end) =1 
      ... so one repeat for all columns.

但是,master_table 有超过 500 万行,所以我在每个搜索字段上创建了一个索引,但存储过程没有使用索引并且正在执行全表扫描,导致性能下降。

如果我从 WHERE 子句中删除 CASE 语句并传递类似 where col1=p_field1) 的内容,则使用索引并且查询性能非常好。

但是,由于并非所有输入字段都是前端必填项,因此我必须在 WHERE 子句中使用 CASE 语句。

准确地说,在 WHERE 子句中使用 CASE 语句,Oracle 没有使用索引。

有人可以建议如何调整上面的 sql 以便它使用索引来提高性能吗?

提前致谢。

【问题讨论】:

  • 你需要创建一个Function-based index
  • 为什么在 where 子句中使用 case stmt?
  • 我正在使用 case stmt,因为并非所有字段都始终作为输入传递。如果该值被传递,我需要将它与数据库进行匹配,否则该字段不应包含在“where”条件中。
  • 您可以将您的条件写成(p_field1 is null or col1=p_field1)

标签: oracle performance indexing query-optimization oracle12c


【解决方案1】:

您编写了一个查询,实际上是 64 (2^6) 个查询。不同的参数排列将适合不同的访问路径。

优化器根据它对表的了解为查询创建特定的访问路径。例如,在 WHERE 子句中使用这些列的选择性如何?

  • 是否有任何列具有唯一索引?这对于索引查找很有用。
  • 直方图是否表明一列只有三个值,平均分布在各行中?这对索引查找不利。
  • 集群因子呢?如果索引列的值分布在每个表块中,则使用全表扫描比读取索引更有效。
  • 此列是否高度偏斜,以至于一两个值构成了大部分条目?索引读取对于这两个值将是灾难性的,但对于其他值的长尾非常有效。

这就是您设置优化器的挑战:提出一个单一的执行计划,该计划适用于用户传递的任何参数排列。当然不能那样做。问题是,有一个参数排列只能由全表扫描提供服务,并且所有六个参数都为空。 (您说存储过程每次至少传递一个参数,但这是在查询外部强制执行的,因此优化器不知道这一点。)此外,某些索引列可能没有选择性,以至于造成灾难性后果。因此,优化器似乎很可能选择使用全表扫描作为所有可能排列的最危险的,即使最终对于大多数排列来说效率低下。

怎么办?

一种方法是让优化器在每次运行时重新解析查询。有complicated ways of doing this (mis-)using Row Level Security,但也许您需要做的只是将/*+ BIND_AWARE */ 提示应用于您的查询。

或者,使用动态 SQL。根据填充的参数,从字面上编写不同的 SQL 语句。像这样

...
is 
   stmt varchar2(32767);
begin
    stmt := '
        select col1, col2, col3,col4,col5,col6 
        from master_table
        where 1=1';
    if p_field1 is not null then
        stmt := stmt || ' and col1 = '''|| p_field1 ||'''';
    end if;
    if p_field2 is not null then
        stmt := stmt || ' and col2 = '''|| p_field2 ||'''';
    end if;
    if p_field3 is not null then
        stmt := stmt || ' and col3 = '''|| p_field3 ||'''';
    end if;
    open cur for stmt;

注意:在上面的代码中我选择不使用绑定变量。我这样做的主要原因是在 OPEN .., USING 语句中需要 64 个参数排列。但是,@WilliamRobertson 建议了一个指向 Tom Kyte 文章的链接,该文章有一种巧妙的处理方式。 Find out more.

这是我按照 Tom Kyte 行编写的代码:

...
is 
   stmt varchar2(32767);
begin
    stmt := '
        select col1, col2, col3,col4,col5,col6 
        from master_table';
    if p_field1 is not null then
        stmt := stmt || ' where col1 = :p_field1 ';
    else 
        stmt := stmt || ' where (1 = 1 or :p_field1 is null)';
    end if;
    if p_field2 is not null then
        stmt := stmt || ' and col2 =:p_field2 ';
    else 
        stmt := stmt || ' and (1 = 1 or :p_field2 is null)';
    end if;
    if p_field3 is not null then
        stmt := stmt || ' and col3 = :p_field3 ';
    else 
        stmt := stmt || ' and (1 = 1 or :p_field3 is null)';
    end if;
    open cur for stmt
        using p_field1, p_field2, p_field3;

【讨论】:

  • 我之前确实尝试过,但由于空检查(其中 p_field1 为空),它仍然会进行表全扫描。如果我删除它,那么它使用索引并且查询运行得更快。我确实创建了一个包含所有元素的复合索引
  • 我已经重写了我原来的答案。事后看来,复合索引是错误的解决方案。
  • 动态语句中绑定变量呢?
  • @WernfriedDomscheit - 问题说 “最多六个” 参数。代码示例已缩短。
  • @APC >是的,对不起,我错过了。我看到您已将答案编辑为我同意的 64 种排列。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-04-29
  • 2021-11-12
  • 2021-03-14
  • 1970-01-01
  • 1970-01-01
  • 2018-07-04
  • 1970-01-01
相关资源
最近更新 更多