【问题标题】:Determine if a range is completely covered by a set of ranges确定一个范围是否完全被一组范​​围覆盖
【发布时间】:2019-02-22 08:03:44
【问题描述】:

如何检查一个范围是否被一组范围完全覆盖。在以下示例中:

WITH ranges(id, a, b) AS (
    SELECT 1,  0, 40 UNION
    SELECT 2, 40, 60 UNION
    SELECT 3, 80, 100 UNION
    SELECT 4, 10, 30
), tests(id, a, b) AS (
    SELECT 1, 10, 90 UNION
    SELECT 2, 10, 60
)
SELECT *
FROM tests
WHERE -- ?
  • 我想选择10, 60,因为它都被0, 4040, 60(和10, 30)覆盖了
  • 我想排除10, 90,因为它暴露在60, 80之间

假设a 是包容性的,b 是独占性的,即值40 属于[40, 60) 而不是[0, 40)。范围可以包含间隙和各种重叠。

实际问题涉及日期+时间数据,但日期只是数字。我正在使用 SQL Server,但首选通用解决方案。

【问题讨论】:

  • 看起来您在这里使用的是封闭间隔。请注意,尤其是对于日期时间数据,应首选半封闭区间。计算端点更容易,您不太可能跳过应该包含的数据等。

标签: sql sql-server date range gaps-and-islands


【解决方案1】:

如已接受的答案中所述,解决方案是将重叠范围合并在一起,然后确定测试范围是否存在于合并范围之一内。

除了连接和/或递归之外,您还可以使用 sorting approach 和窗口函数来合并重叠范围:

WITH ranges(id, a, b) AS (
    SELECT 1,  0, 40 UNION
    SELECT 2, 40, 60 UNION
    SELECT 3, 80, 100 UNION
    SELECT 4, 10, 30
), tests(id, a, b) AS (
    SELECT 1, 10, 90 UNION
    SELECT 2, 10, 60
), ranges_chg AS (
    SELECT *, CASE WHEN MAX(b) OVER (ORDER BY a ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) >= a THEN 0 ELSE 1 END AS chg
    FROM ranges
), ranges_grp AS(
    SELECT *, SUM(chg) OVER (ORDER BY a) AS grp
    FROM ranges_chg
), merged_ranges AS (
    SELECT MIN(a) AS a, MAX(b) AS b
    FROM ranges_grp
    GROUP BY grp
)
SELECT *
FROM tests
WHERE EXISTS (
    SELECT 1
    FROM merged_ranges
    WHERE merged_ranges.a <= tests.a AND tests.b <= merged_ranges.b
)

结果和Fiddle

| id | a  | b  |
|----|----|----|
| 2  | 10 | 60 |

range_grpCTE 中的数据会让您了解它的工作原理:

| id | a  | b   | max b over... | chg | grp |
|----|----|-----|---------------|-----|-----|
| 1  | 0  | 40  | NULL          | 1   | 1   |
| 4  | 10 | 30  | 40            | 0   | 1   |
| 2  | 40 | 60  | 40            | 0   | 1   |
| 3  | 80 | 100 | 60            | 1   | 2   |

【讨论】:

    【解决方案2】:

    这是解决方案的一般形式。我们的想法是执行以下操作:

    • 获取范围内所有点的列表。这是所有范围的开始和范围的结束。
    • 检查其中是否有任何不在范围内。

    这是基于以下操作:不在范围内的点将是任一表中包含的数字之一:

    with tc as (
          select t.test, r.candidate
          from tests t join
               (select r.a as candidate from ranges union all
                select r.b from ranges
               ) r
               on r.candiate >= t.a and r.candidate < t.b
          union all
          select t.test, t.a
          from tests t
          union all
          select t.test, t.b
          from tests t
         )
    select distinct tc.test
    from tc
    where not exists (select 1
                      from ranges r
                      where tc.candidate >= r.a and tc.candidate < r.b
                     );
    

    因为范围包括第一项,所以您实际上不必检查它。所以候选名单可以减少:

    with tc as (
          select t.test, r.candidate
          from tests t join
               (select r.b as candidate from ranges
               ) r
               on r.candidate >= t.a and r.r < t.b
          union all
          select t.test, t.a
          from tests t
          union all
          select t.test, t.b
          from tests t
         )
    

    【讨论】:

    • 我非常努力地修复了您的查询工作,但我做不到。
    • @SalmanA 。 . .你有什么问题? SQL Fiddle(或相关网站会有所帮助)。
    • 查询中的列名/表名都是错误的(例如r.r)。我尝试了 a 和 b 的所有组合,但查询最多返回所有行。
    • @SalmanA 。 . .该列应称为candidate。我不应该在编写查询的过程中更改事物的名称。
    【解决方案3】:

    这是一个类似于 Thorsten 的递归解决方案。只是提供另一个例子。

    WITH ranges(id, a, b) AS (
        SELECT 1,  0, 40 UNION
        SELECT 2, 40, 60 UNION
        SELECT 3, 80, 100 UNION
        SELECT 4, 10, 30 
    ), tests(id, a, b) AS
    (   
            SELECT 1 as id, 10 as a, 90 as b
            UNION
            SELECT 2, 10, 60
    ), rangeFinder(a, b, ra, rfb) AS
    (
        SELECT a, b, 0 AS ra, 0 AS rfb 
        FROM ranges AS r
        UNION ALL
        SELECT rangeFinder.a, ranges.b, ranges.a, rangeFinder.b 
        FROM ranges 
        JOIN rangeFinder
            ON ranges.b > rangeFinder.b
            AND ranges.a <=rangeFinder.b
    ), islands(a, b) AS
    (
        SELECT a, b 
        FROM rangeFinder
        WHERE a NOT IN (SELECT ra FROM rangeFinder)
            AND b NOT IN (SELECT rfb FROM rangeFinder)
    )
    SELECT t.id, t.a, t.b FROM 
    tests t
    JOIN islands i
    ON t.a >= i.a
    AND t.b <= i.b
    

    在这里演示:http://rextester.com/HDQ52126

    【讨论】:

    • 我喜欢您用于查询的名称。 rangeFinder 是递归 cte 的好名字。
    • 我实际上首先使用了realRanges,但后来决定使用islands 似乎有冲突。
    • 好主意。通常是我们选择的名称使我们的查询或多或少具有可读性。
    【解决方案4】:

    您需要一个递归查询来查找实际范围(在您的情况下为 0 到 60 和 80 到 100)。我们将从给定的范围开始,并寻找扩展这些范围的范围。最后我们坚持使用最大的范围(例如,范围 10 到 30 可以扩展到 0 到 40,然后再扩展到 0 到 60,所以我们保持最宽的范围 0 到 60)。

    with wider_ranges(a, b, grp) as
    (
      select a, b, id from ranges
      union all
      select
        case when r1.a < r2.a then r1.a else r2.a end,
        case when r1.b > r2.b then r1.b else r2.b end,
        r1.grp
      from wider_ranges r1
      join ranges r2 on (r2.a < r1.a and r2.b >= r1.a)
                     or (r2.b > r1.b and r2.a <= r1.b)
    )
    , real_ranges(a, b) as
    (
      select distinct min(a), max(b)
      from wider_ranges
      group by grp
    )
    select * 
    from tests
    where exists
    (
      select *
      from real_ranges
      where tests.a >= real_ranges.a and tests.b <= real_ranges.b
    );
    

    Rextester 演示:http://rextester.com/BDJA16583

    根据要求,这适用于 SQL Server,但它是标准 SQL,因此它应该适用于几乎所有具有递归查询的 DBMS。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-09-03
      • 1970-01-01
      • 2020-09-12
      • 1970-01-01
      • 2014-10-24
      相关资源
      最近更新 更多