Oracle select with "in (multiple values)" 不使用索引

Posted

技术标签:

【中文标题】Oracle select with "in (multiple values)" 不使用索引【英文标题】:Oracle select with "in (multiple values)" does not use index 【发布时间】:2018-02-13 09:37:08 【问题描述】:

Oracle (12.1) 不使用真正有利的索引。 使用 union all 重写手动查询会有所帮助,但确实很难看。 有什么想法吗?

架构:

表“umsatz”,按“monat”的每个值进行分区(= 年 + 月作为数字)。 (monat, kundengruppe) 上的简单非唯一索引。 统计信息可用且是最新的。 “monat”的选择性不高(每个月都有很多值),但“kundengruppe”列的大部分值都具有选择性。 “kundengruppe”有一个混合直方图,这里使用的具体值不会没有出现在表中(即非常有选择性) - 将它们更改为很少出现的值不会改变任何东西。查询单个频繁出现的值会导致全表/分区扫描 - 在这种情况下是可以的。

查询(减少到相关的最小部分):

 SELECT SUM(u.umsatz_euro)
  FROM umsatz u
 WHERE u.monat BETWEEN 201701 AND 201712
   AND u.kundengruppe IN (123,456,987)

以及它的执行计划:

------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                                        | Name                       | Rows | Bytes | Cost | Time     |
------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                                 |                            |    1 |    14 | 3074 | 00:00:01 |
|   1 |   SORT AGGREGATE                                 |                            |    1 |    14 |      |          |
|   2 |    MAT_VIEW ACCESS BY GLOBAL INDEX ROWID BATCHED | UMSATZ_BUDGETBLATT         |    1 |    14 | 3074 | 00:00:01 |
| * 3 |     INDEX RANGE SCAN                             | UMSATZ_BUDGETBLATT_GRP_NUI |    1 |       | 3073 | 00:00:01 |
------------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
------------------------------------------
* 3 - access("UB"."MONAT">=201701 AND "UB"."MONAT"<=201712)
* 3 - filter("UB"."KUNDENGRUPPE"=123 OR "UB"."KUNDENGRUPPE"=456 OR "UB"."KUNDENGRUPPE"=987)

如您所见,只有“monat”通过访问谓词(即作为索引访问)使用,“kundengruppe”通过过滤器使用。

. 如果我将WHERE 更改为简单的AND u.kundengruppe = 123,则索引用于两个值,并且成本会降低很多(3000 -> 4):

------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                                        | Name                       | Rows | Bytes | Cost | Time     |
------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                                 |                            |    1 |    14 |    4 | 00:00:01 |
|   1 |   SORT AGGREGATE                                 |                            |    1 |    14 |      |          |
|   2 |    MAT_VIEW ACCESS BY GLOBAL INDEX ROWID BATCHED | UMSATZ_BUDGETBLATT         |    1 |    14 |    4 | 00:00:01 |
| * 3 |     INDEX RANGE SCAN                             | UMSATZ_BUDGETBLATT_GRP_NUI |    1 |       |    3 | 00:00:01 |
------------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
------------------------------------------
* 3 - access("UB"."MONAT">=201701 AND "UB"."KUNDENGRUPPE"=123 AND "UB"."MONAT"<=201712)
* 3 - filter("UB"."KUNDENGRUPPE"=123)

. 我可以使用语义等效的 UNION ALL手动重写多值-IN:

SELECT SUM(u.umsatz_euro)
  FROM 
  (select * from umsatz u
    where u.kundengruppe = 123
   union all
   select * from umsatz u
    where u.kundengruppe = 456
   union all
   select * from umsatz u
    where u.kundengruppe = 987
  ) u
 WHERE u.monat BETWEEN 201701 AND 201712

执行计划使用索引三次,成本很多低于第一个计划 (3000 -> 14),Oracle 甚至将“monat”谓词下推到每个选择中:

--------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                                          | Name                       | Rows | Bytes | Cost | Time     |
--------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                                   |                            |    1 |    26 |   12 | 00:00:01 |
|   1 |   SORT AGGREGATE                                   |                            |    1 |    26 |      |          |
|   2 |    VIEW                                            |                            |    3 |    78 |   12 | 00:00:01 |
|   3 |     UNION-ALL                                      |                            |      |       |      |          |
|   4 |      MAT_VIEW ACCESS BY GLOBAL INDEX ROWID BATCHED | UMSATZ_BUDGETBLATT         |    1 |    14 |    4 | 00:00:01 |
| * 5 |       INDEX RANGE SCAN                             | UMSATZ_BUDGETBLATT_GRP_NUI |    1 |       |    3 | 00:00:01 |
|   6 |      MAT_VIEW ACCESS BY GLOBAL INDEX ROWID BATCHED | UMSATZ_BUDGETBLATT         |    1 |    14 |    4 | 00:00:01 |
| * 7 |       INDEX RANGE SCAN                             | UMSATZ_BUDGETBLATT_GRP_NUI |    1 |       |    3 | 00:00:01 |
|   8 |      MAT_VIEW ACCESS BY GLOBAL INDEX ROWID BATCHED | UMSATZ_BUDGETBLATT         |    1 |    14 |    4 | 00:00:01 |
| * 9 |       INDEX RANGE SCAN                             | UMSATZ_BUDGETBLATT_GRP_NUI |    1 |       |    3 | 00:00:01 |
--------------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
------------------------------------------
* 5 - access("UB"."MONAT">=201701 AND "UB"."KUNDENGRUPPE"=123 AND "UB"."MONAT"<=201712)
* 5 - filter("UB"."KUNDENGRUPPE"=123)
* 7 - access("UB"."MONAT">=201701 AND "UB"."KUNDENGRUPPE"=456 AND "UB"."MONAT"<=201712)
* 7 - filter("UB"."KUNDENGRUPPE"=456)
* 9 - access("UB"."MONAT">=201701 AND "UB"."KUNDENGRUPPE"=987 AND "UB"."MONAT"<=201712)
* 9 - filter("UB"."KUNDENGRUPPE"=987)

. 这真的很麻烦,尤其是如果您有一个 Java 列表并通过 Hibernate 轻松地将其绑定到单个绑定变量 (IN (:kgrpList))。

查询提示use_concat 没有帮助。 有什么想法可以改善这种情况吗?


另一个奇怪的事情在这种情况下:

如果我对数字使用绑定变量

 SELECT SUM(u.umsatz_euro)
  FROM umsatz u
 WHERE u.monat BETWEEN 201701 AND 201712
   AND u.kundengruppe = to_number(:a)    -- also for fixed  to_number('a123')

计划使用(慢)索引跳过扫描:

------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                                        | Name                       | Rows | Bytes | Cost | Time     |
------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                                 |                            |    1 |    14 |  955 | 00:00:01 |
|   1 |   SORT AGGREGATE                                 |                            |    1 |    14 |      |          |
|   2 |    MAT_VIEW ACCESS BY GLOBAL INDEX ROWID BATCHED | UMSATZ_BUDGETBLATT         | 1769 | 24766 |  955 | 00:00:01 |
| * 3 |     INDEX SKIP SCAN                              | UMSATZ_BUDGETBLATT_GRP_NUI | 1769 |       |   36 | 00:00:01 |
------------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
------------------------------------------
* 3 - access("UB"."MONAT">=201701 AND "UB"."KUNDENGRUPPE"=TO_NUMBER(:A) AND "UB"."MONAT"<=201712)
* 3 - filter("UB"."KUNDENGRUPPE"=TO_NUMBER(:A))

甲骨文到底为什么要这样做?

【问题讨论】:

您最近是否运行过该表的收集统计信息? 使用 INDEX 提示的选项怎么样? 统计数据是最新的(见编辑文本)。 INDEX 提示不会改变任何东西,可能是因为索引已被使用,但仅用于“monat”列。 可以代工吗? INDEX 提示应该强制使用提到的索引而不是自动选择的.. :-/ 【参考方案1】:

复合索引最终与所有索引一样,是一个键后跟数据(rowid)。

因此 (monat,kundengruppe ) 上的索引在有序索引结构中的键在概念上类似于:

201701-123
201701-...
201701-...
201701-456
201701-...
201701-...
201701-987
201701-...
201701-...
201701-...
201701-...
201712-123
201712-...
201712-...
201712-456
201712-...
201712-...
201712-...
201712-987
201712-...

所以在查询时请记住这一点:

WHERE u.monat BETWEEN 201701 AND 201712 AND u.kundengruppe = 123

我有一个逻辑起点 (201701-123) 和一个逻辑终点 (201702-123)。

当我们向优化器展示如下内容时:

WHERE u.monat BETWEEN 201701 AND 201712 AND u.kundengruppe in (123,456,789)

要提出最佳索引使用策略是一个更难的提议。

理想情况下它可以在 kundengruppe 中对列表进行排序,并使用 201701-[min list value] 和 201712-[max list value] 之间的键的索引访问,或者正如您在手动重写中所做的那样,将其分成 3 个单独的访问。

但在这两种情况下,就成本这样的访问而言,这是一个艰难的提议,即,它是最佳选项吗,因为(根据我的索引键列表),可能散布在感兴趣的索引键之间的值的数量很难估计。同样,在什么时候你会放弃将列表分成单独的部分的策略。如果第二个谓词是:

u.kundengruppe in (123,456,... [500 more values]...789)

在这种情况下,您可能不想将其拆分。

抱歉,我没有任何好的解决方案供您使用,但最终,您对优化器和(自动)查询转换的要求只有这么多。

【讨论】:

【参考方案2】:

我根据康纳的回答引发的想法做了一些测试。结果回答了我自己的问题。

我将索引中列的顺序更改为(kundengruppe, monat),即选择性列排在最前面(“monat”只有大约 30 个不同的值,但因为它是分区属性而被放在首位 - 永远是错误的想法在这种情况下访问)。

现在计划和成本看起来像预期的那样:

SELECT SUM(u.umsatz_euro)
  FROM umsatz u
 WHERE u.monat BETWEEN 201701 AND 201712
   AND u.kundengruppe IN (123,456,987)

.

-------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                                         | Name                       | Rows | Bytes | Cost | Time     |
-------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                                  |                            |    1 |    14 |    6 | 00:00:01 |
|   1 |   SORT AGGREGATE                                  |                            |    1 |    14 |      |          |
|   2 |    INLIST ITERATOR                                |                            |      |       |      |          |
|   3 |     MAT_VIEW ACCESS BY GLOBAL INDEX ROWID BATCHED | UMSATZ_BUDGETBLATT         |    1 |    14 |    6 | 00:00:01 |
| * 4 |      INDEX RANGE SCAN                             | UMSATZ_BUDGETBLATT_GRP_NUI |    1 |       |    5 | 00:00:01 |
-------------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
------------------------------------------
* 4 - access(("UB"."KUNDENGRUPPE"=123 OR "UB"."KUNDENGRUPPE"=456 OR "UB"."KUNDENGRUPPE"=987) AND "UB"."MONAT">=201701 AND "UB"."MONAT"<=201712)

. 即使在查询更多出现频率更高的值时,也会使用索引:

-------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                                         | Name                       | Rows | Bytes | Cost | Time     |
-------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                                  |                            |    1 |    14 | 1518 | 00:00:01 |
|   1 |   SORT AGGREGATE                                  |                            |    1 |    14 |      |          |
|   2 |    INLIST ITERATOR                                |                            |      |       |      |          |
|   3 |     MAT_VIEW ACCESS BY GLOBAL INDEX ROWID BATCHED | UMSATZ_BUDGETBLATT         | 2664 | 37296 | 1518 | 00:00:01 |
| * 4 |      INDEX RANGE SCAN                             | UMSATZ_BUDGETBLATT_GRP_NUI | 2900 |       |   12 | 00:00:01 |
-------------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
------------------------------------------
* 4 - access(("UB"."KUNDENGRUPPE"=1899 OR "UB"."KUNDENGRUPPE"=2032 OR "UB"."KUNDENGRUPPE"=2160 OR "UB"."KUNDENGRUPPE"=2165 OR "UB"."KUNDENGRUPPE"=5048) AND "UB"."MONAT">=201701 AND
  "UB"."MONAT"<=201712)

. 如果我查询经常出现的(甚至单个)“kundengruppe”,Oracle 会选择适当的全表扫描(嗯,full mat_view 扫描,因为基表是物化视图):

------------------------------------------------------------------------------------------------
| Id  | Operation                   | Name               | Rows   | Bytes   | Cost  | Time     |
------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |                    |      1 |      14 | 12494 | 00:00:01 |
|   1 |   SORT AGGREGATE            |                    |      1 |      14 |       |          |
|   2 |    PARTITION RANGE ITERATOR |                    | 117769 | 1648766 | 12494 | 00:00:01 |
| * 3 |     MAT_VIEW ACCESS FULL    | UMSATZ_BUDGETBLATT | 117769 | 1648766 | 12494 | 00:00:01 |
------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
------------------------------------------
* 3 - filter("UB"."KUNDENGRUPPE"=5047 AND "UB"."MONAT"<=201712)

. 这也回答了我问题的最后一部分——至少有一点: 使用绑定变量时,Oracle 不知道具体使用的值 - 它可能是一个经常出现的值(但为什么 index skip scan 而不是 index range scan一个已知值?)。 使用更改的索引列顺序,使用 索引范围扫描,即使在查询多个绑定变量时也是如此。

总的来说,结果并不令人惊讶: 索引中的列顺序很重要——在使用分区时也是如此!

【讨论】:

以上是关于Oracle select with "in (multiple values)" 不使用索引的主要内容,如果未能解决你的问题,请参考以下文章

ORACLE WITH AS 用法,创建临时表

oracle with as 用法

关于oracle with as用法

关于oracle with as用法

Oracle 树操作(select…start with…connect by…prior)

关于oracle with as用法