【问题标题】:Find overlapping range in PL/SQL在 PL/SQL 中查找重叠范围
【发布时间】:2016-04-12 18:33:17
【问题描述】:

下面的示例数据

    id   start  end
    a     1      3
    a     5      6
    a     8      9
    b     2      4
    b     6      7
    b     9      10
    c     2      4
    c     6      7
    c     9      10

我正在尝试提出一个查询,该查询将返回 a、b 和 c 之间的所有重叠起始端(但可扩展到更多)。所以预期的数据将如下所示

    start  end
     2      3
     6      6
     9      9

我能想象到这一点的唯一方法是使用自定义聚合函数来跟踪当前有效间隔,然后在迭代阶段计算新的间隔。但是,在处理大型数据集时,我看不到这种方法实用。因此,如果某个聪明的头脑有一个我不知道的查询或一些先天功能,我将非常感谢您的帮助。

【问题讨论】:

  • 就个人而言,我认为这是一个很好的问题,不知道为什么有人会否决它。

标签: sql oracle plsql logic


【解决方案1】:

您可以使用聚合和join 来做到这一点。假设“a”和“b”没有内部重叠:

select greatest(ta.start, tb.start) as start,
       least(ta.end, tb.end) as end
from t ta join
     t tb
     on ta.start <= tb.end and ta.end >= tb.start and
        ta.id = 'a' and tb.id = 'b';

【讨论】:

  • 感谢您的客气话和您的解决方案。我将您的最后一个条件从 ta.id = tb.id 翻转为 并且效果很好。非常感谢!附言然而,我对此唯一担心的是,当处理非常大的数据集不会将表连接到自身上时会严重影响性能?
  • @user3758659 不应该是 ta.id
  • 对于超过 2 个不同 id 的情况,这似乎并不适用。
  • 在阅读了这里的一些 cmets 并为自己进行了测试后,我发现这确实不适用于增加 id 的数量。不过,我仍然非常感谢您的回答!
  • @user3758659 。 . .糟糕,我的 id 条件有误。
【解决方案2】:

这比 Gordon 的解决方案更难看也更复杂,但我认为它可以更好地提供预期的答案,并且应该扩展到使用更多 id:

WITH NUMS(N) AS (  --GENERATE NUMBERS N FROM THE SMALLEST START VALUE TO THE LARGEST END VALUE
  SELECT MIN("START") N FROM T
  UNION ALL
  SELECT N+1 FROM NUMS WHERE N < (SELECT MAX("END") FROM T)
),
SEQS(N,START_RANK,END_RANK) AS (
  SELECT N,
    CASE WHEN IS_START=1 THEN ROW_NUMBER() OVER (PARTITION BY IS_START ORDER BY N) ELSE 0 END START_RANK, --ASSIGN A RANK TO EACH RANGE START
    CASE WHEN IS_END=1 THEN ROW_NUMBER() OVER (PARTITION BY IS_END ORDER BY N) ELSE 0 END END_RANK --ASSIGN A RANK TO EACH RANGE END
  FROM (
          SELECT N,
              CASE WHEN NVL(LAG(N) OVER (ORDER BY N),N) + 1 <> N THEN 1 ELSE 0 END IS_START, --MARK N AS A RANGE START
              CASE WHEN NVL(LEAD(N) OVER (ORDER BY N),N) -1 <> N THEN 1 ELSE 0 END IS_END /* MARK N AS A RANGE END */ 
              FROM (
                SELECT DISTINCT N FROM ( --GET THE SET OF NUMBERS N THAT ARE INCLUDED IN ALL ID RANGES
                  SELECT NUMS.*,T.*,COUNT(*) OVER (PARTITION BY N) N_CNT,COUNT(DISTINCT "ID") OVER () ID_CNT 
                  FROM NUMS
                  JOIN T ON (NUMS.N >= T."START" AND NUMS.N <= T."END")
                  ) WHERE N_CNT=ID_CNT
              )
    ) WHERE IS_START + IS_END > 0
)
SELECT STARTS.N "START",ENDS.N "END" FROM SEQS STARTS
JOIN SEQS ENDS ON (STARTS.START_RANK=ENDS.END_RANK AND STARTS.N <= ENDS.N) ORDER BY "START"; --MATCH CORRESPONDING RANGE START/END VALUES 

首先我们生成最小起始值和最大结束值之间的所有数字。

然后我们通过将我们生成的数字连接到范围,并为每个“id”选择出现一次的每个数字“n”来找到包含在所有提供的“id”范围中的数字。

然后我们确定每个值“n”是开始还是结束一个范围。为了确定这一点,对于每个 N,我们说: 如果 N 的先前值不存在或不小于当前 N 1,则当前 N 开始一个范围。如果 N 的下一个值不存在或不比当前 N 大 1,则当前 N 结束一个范围。

接下来,我们为每个开始值和结束值分配一个“排名”,以便我们可以匹配它们。

最后,我们在排名匹配的地方(以及开始

编辑:经过一番搜索,我遇到了this question,它显示了一种更好的方法来查找开始/结束并将查询重构为:

WITH NUMS(N) AS (  --GENERATE NUMBERS N FROM THE SMALLEST START VALUE TO THE LARGEST END VALUE
  SELECT MIN("START") N FROM T
  UNION ALL
  SELECT N+1 FROM NUMS WHERE N < (SELECT MAX("END") FROM T)
)
SELECT MIN(N) "START",MAX(N) "END" FROM (
  SELECT N,ROW_NUMBER() OVER (ORDER BY N)-N GRP_ID
                FROM (
                  SELECT DISTINCT N FROM ( --GET THE SET OF NUMBERS N THAT ARE INCLUDED IN ALL ID RANGES
                    SELECT NUMS.*,T.*,COUNT(*) OVER (PARTITION BY N) N_CNT,COUNT(DISTINCT "ID") OVER () ID_CNT 
                    FROM NUMS
                    JOIN T ON (NUMS.N >= T."START" AND NUMS.N <= T."END")
                    ) WHERE N_CNT=ID_CNT
  )
) 
GROUP BY GRP_ID ORDER BY "START";

【讨论】:

  • 感谢迈克的回答,我已经测试了您的解决方案,它就像一个魅力。我会自己做一些分析,并对您的解决方案有一个很好的理解。非常感谢。
猜你喜欢
  • 2014-01-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-12-04
  • 2011-05-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多