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)" 不使用索引的主要内容,如果未能解决你的问题,请参考以下文章