Oracle 更快的重叠检查
Posted
技术标签:
【中文标题】Oracle 更快的重叠检查【英文标题】:Oracle faster overlap check 【发布时间】:2012-04-12 13:15:56 【问题描述】:我有一个包含两列 beginrange 和 endrange 的表格。不应允许重叠范围。这些列上有索引,我们尝试了许多 sql 条件,例如
inputBegin between beginRange and endRange or
inputEnd between beginRange and endRange
not ( inputEnd < beginRange or inputStart > endRange )
等 哪个工作正常,除了它们非常慢,因为表包含超过 500 万条记录。
有没有写一个更有效的重叠检查?
编辑: 我想到了另一种解决方案,oracle 仅当 count() 在具有索引的 NOT NULL 列上完成时才会计算索引。如果 beginRange 和 endRange 不是 NULL 并且都有一个索引,我们可以得到三个和:
count(endRange) where inputBegin > endRange
+
count(beginRange) where inputEnd < beginRange
=
count(beginRange/endRange)
所以使用 UNION ALL 我会得到三行,并且在代码中我需要检查前两个的总和是否等于第三行。当然,我假设只计算索引并且不会访问任何行。还有什么办法吗?
【问题讨论】:
当您说这些列上有索引时,您能否准确指定它们的索引,因为听起来您有 2 个单独的索引。 @Ben 它们只是列上的两个非唯一索引。 您是否尝试在(begin,end)
两列而不是(begin)
和(end)
每一列上添加索引?
@Ben:很好的提示——我们有一个复合索引,它运行良好。我们还按begin
进行分区。 OP 可以发布计划给我们看...
这不是 DBA 问题。它只需要认识到没有一个索引可以帮助这两种情况。而是有两个索引和两个查询,然后将它们全部联合起来。
【参考方案1】:
我不确定你是否愿意:
-
检查您要插入的行是否与某些现有行重叠,或者
搜索所有现有行并找出重叠的行?
如果 (1),那么您基本上已经在做的事情......
SELECT *
FROM YOUR_TABLE
WHERE :inputEnd > beginRange AND :inputStart < endRange;
...如果您有一个复合索引,其组件在相反方向:beginRange ASC, endRange DESC
。
如果 (2),那么您可以像这样使用窗口化:
SELECT *
FROM (
SELECT
YOUR_TABLE.*,
LEAD(beginRange) OVER (ORDER BY beginRange) nextBeginRange
FROM YOUR_TABLE
)
WHERE endRange > nextBeginRange;
这将为您提供与其下一个范围重叠的每个范围(其中“下一个”的含义在 beginRange
排序的上下文中定义)。
严格来说,这甚至不需要复合索引(除非你想要covering)——只需一个简单的beginRange
索引就可以确保良好的性能。
【讨论】:
一条评论清楚地表明 Op 需要你的答案 1。复合索引会有所帮助,但是,我不确定。第一个谓词将产生一个索引范围查找(表的开头)->(:inputEnd)。然而,第二个谓词很少使用索引中的第二个字段。相对较少的范围将同时开始,这使得第一个字段的选择性如此之高,以至于第二个字段也可能是随机的。需要进行一些额外的优化。 @Dems 我看到endRange
在执行计划中被用作索引扫描的访问(而不是过滤)谓词,我得出的结论是索引是最佳使用的,但是我越想想在这种特殊情况下如何使用索引必须,我认为你是对的。【参考方案2】:
这是一个答案 - 如果可以做出某些断言:
您有一个包含 beginRange
和 endRange
列的表,其中没有两个现有行重叠 (beginRange, endRange)
。
您想用(inputStart, inputEnd)
插入一个新行,但请检查它是否与表上的任何现有行重叠。
然后你可以使用这个应该很快的条件 - 在startRange
上使用一个简单的索引:
WHERE input_Start <
( SELECT endRange
FROM
( SELECT endRange
, ROW_NUMBER() OVER(ORDER BY startRange DESC) AS rn
FROM tableX
WHERE startRange < input_End
) tmp
WHERE rn = 1
)
--- TRUE --> Overlaps
--- FALSE --> No overlap
【讨论】:
在这种情况下,您当然应该在startrange,endrange
上有一个索引,因此您不需要在内部选择中通过rowid
访问表。你只能访问这个索引。
@Ben:是的,但它只会是单行读取 - 使用聪明的优化器。【参考方案3】:
没有一个索引可以满足这个查询。这实际上意味着您最好创建两个索引并运行两个查询,然后对结果进行 UNION...
1) 在 InputBegin 上创建索引 2) 在 InputEnd 上创建一个单独的索引 3)运行以下查询
SELECT * FROM yourTable WHERE InputEnd < ExclusionPeriodStart
UNION ALL
SELECT * FROM yourTable WHERE InputBegin > ExclusionPeriodEnd
然后,第一个查询可以在 InputEnd 索引上使用范围查找。 然后第二个查询也可以使用范围搜索,但在不同的索引上。
通过将查询分开,两种不同的需求不会相互干扰,并且可以使用最优化的索引。
您还已经知道(通过了解您的数据)结果中没有重叠(没有记录可以在完成之前开始,因此两个查询中都不能出现记录)。这意味着可以使用UNION ALL
代替较慢的UNION
。
据我所知,没有办法比这更快地执行此查询。 (对于 500 万条记录,只扫描小数据集上的整个表可能会更快。)
编辑:该答案假定您正在尝试查找未出现在固定范围内的所有记录。如果您想对照其他记录检查每条记录,那么您需要一种不同的方法...
检查每个重叠的成本很高。此外,如果您有这四个范围,则无法确定要删除的范围...
1 -->--> 4
3 -->--> 6
5 -->--> 8
7 -->--> 9
应该删除范围 1 和 3,还是 2 和 4?
您可以做的是找到所有与另一个范围重叠的范围。
而你不想要的是发现A与B重叠,B与A重叠。
SELECT
*
FROM
yourTable AS first_range
INNER JOIN
yourTable AS second_range
ON second_range.start_date >= first_range.start_date
AND second_range.start_date <= first_range.end_date
这将需要扫描整个表的 first_range。但是因为您只检查第二个范围的 start_date,它将能够在 start_date 索引上使用范围搜索来检测任何冲突。
EDIT2:或者您可能需要与第一个答案相反的答案?
如果您希望所有确实与设定范围发生冲突的范围,则可以修改相同的方法。
SELECT * FROM yourTable WHERE InputEnd >= ExclusionPeriodStart
INTERSECT
SELECT * FROM yourTable WHERE InputBegin <= ExclusionPeriodEnd
但是,这可能不是很好。您将在 query1 中获取表的一部分,并将其与表的几乎所有其余部分相交。相反,您可以使用简单的方法,然后添加优化...
SELECT
*
FROM
yourTable
WHERE
InputStart <= ExclusionPeriodEnd
AND InputEnd >= ExclusionPeriodStart
WHERE 子句中的第一个条件可以通过范围查找来解决,然后扫描所有结果记录以测试第二个条件。那么,我们能否缩小需要扫描的范围(currently (start of table) -> (ExclusionPeriodEnd))
。
我们可以 如果我们知道一条额外的信息:任何一个范围的最大长度...
SELECT
*
FROM
yourTable
WHERE
InputStart <= ExclusionPeriodEnd
AND InputStart >= ExclusionPeriodStart - (maximumLength)
AND InputEnd >= ExclusionPeriodStart
现在前两个条件形成一个范围搜索,并提供一个小得多的数据集来扫描最后一个条件。
你怎么知道最大长度?您可以扫描整个表,但这是一种自我挫败的优化尝试。
相反,您可以索引一个计算字段;给出范围最大长度的计算。 SELECT MAX(calculatedField) FROM yourTable
然后避免扫描整个表。或者您可以使用触发器跟踪。这对 INSERTS 来说很好,但是当你有一个 DELETE 时有点混乱(如果你删除最长的范围,你是否再次扫描整个表以找到新的最长范围?可能不是,你可能会想保留旧的最大长度而是)。
【讨论】:
查询也可以在没有联合的情况下编写,如WHERE ExclusionPeriodStart <= InputEnd AND InputBegin <= ExclusionPeriodEnd
。我认为没有理由拆分为 2 个子查询和一个联合。
@ypercube - 这并没有给出相同的结果。你可以使用WHERE InputEnd < ExclusionPeriodStart OR InputStart > ExclusionPeriodEnd
(假设你的意思是我的第一个答案,在编辑之前)。但这会扫描整个表/索引。没有索引有帮助 - 为一个谓词的利益排序会给出关于第二个谓词的有效随机排序。通过分离查询,您可以获得两个范围搜索。所以目的是产生 SEEK 而不是 SCAN。【参考方案4】:
假设现有范围不重叠,那么beginRange
应该是一个(主或备用)键,检测新范围是否与某些现有范围重叠可以这样完成:
SELECT *
FROM YOUR_TABLE
WHERE beginRange = (
SELECT MAX(beginRange)
FROM YOUR_TABLE
WHERE beginRange < :inputEnd
)
AND :inputStart < endRange
如果新范围与某些现有范围重叠,则此查询返回“最高”范围。
如果没有重叠,则返回一个空结果集。
beginRange
键“下”的索引足以提高效率(我们只需要支持“MAX 扫描”即可)。
【讨论】:
以上是关于Oracle 更快的重叠检查的主要内容,如果未能解决你的问题,请参考以下文章