【问题标题】:Simultaneously Aggregating Overlapping Ranges (A Rectangle Problem)同时聚合重叠范围(矩形问题)
【发布时间】:2020-04-12 12:20:15
【问题描述】:

我的问题

考虑一组具有两个区间的数据。例如,考虑学生的课程表。每条记录都有一个开始和结束日期,每个班级都有一个时期开始时间和一个时期结束时间。但是从某些记录重叠的意义上说,该时间表并未“规范化”。因此,如果您搜索包含学生给定日期和期间的记录,您可能会得到多个匹配项。

这是一个人为的例子。我将日期表示为整数以简化问题:

declare @schedule table (
    student char(3),
    fromDate int,
    toDate int,
    fromPeriod int,
    toPeriod int
)

insert @schedule values
    ('amy', 1, 7, 7, 9),
    ('amy', 3, 9, 5, 8), 
    ('amy', 10, 12, 1, 3), 
    ('ted', 1, 5, 11, 14),
    ('ted', 7, 11, 13, 16); 

Amy 的日期和期间范围要么重叠,要么相邻。如果我查询“date 5 period 7”,我会得到两个匹配项。我需要对这些进行重新设计,以便它们代表相同的“区域”,但不再重叠。

Ted 的经期重叠,但他的日期不重叠。这意味着没有真正的重叠,因此无需重新工作。

我的研究

我已经阅读了很多关于工作重叠间隔的帖子和一些文章。即:

我已经从 Itzik 的一篇名为“solutions-packing-date-and-time-intervals-puzzle”的博客中实现了一个,它对一个特定项目非常有效。我不认为这是一个稳定的链接,但我找到了它的副本here

但我很难将这些资源中的知识扩展到我手头的问题。这可能是我的局限。我很难跟踪他们。我研究了 Itzik 的解决方案并且已经理解了很多,但我确实记得有一个我无法理解。或者,这些解决方案可能只适用于奇异范围。

我的尝试

我通过将范围视为文字矩形对象解决了这个问题。有用。我什至在我自己的应用程序中制作了一个性能更高的版本。因此,我会将其作为解决方案发布,以防对遇到相同问题的任何人有用。

但它是如此冗长和复杂,并且有足够的怪癖(例如缓冲线、循环形状、使用浮点值、舍入问题),我不禁认为有更好的方法。我列出的资源的概念可以扩展到双重范围吗?或者某些 SRID 是否允许切割长度为零的矩形?

预期结果:

这个问题没有唯一的答案,因为您可以聚合范围并以不同的方式解构它们。但是为了尽量减少生成的矩形的数量,实际上只有两个可以接受的答案。从视觉上看,日期在 X 轴,周期在 Y 轴,重叠的范围可以像这样开始:

 +------------+
 |            |
 |    +------------+
 |    ||||||||     |  <- 2 overlapping rectangles
 +----|            |
      |            |
      +------------+

我们可以这样重做:

 +---+ +-----+
 |   | |     |
 |   | |     | +---+  <- 3 non-overlapping 
 |   | |     | |   |     vertically cut rectangles
 +---| |     | |   |
       |     | |   |
       +-----+ +---+

或者这样:

 +-----------+
 +-----------+

 +-----------------+  <- 3 non-overlapping 
 +-----------------+     horizontally cut rectangles

       +-----------+
       +-----------+

使用垂直切割,结果将如下所示:

+-------------------------------------------+
|student|fromDate|toDate|fromPeriod|toPeriod|
|-------------------------------------------|
|amy    |1       |2     |7         |9       |
|amy    |3       |7     |5         |9       |
|amy    |8       |9     |5         |8       |
|amy    |10      |12    |1         |3       |
|ted    |1       |5     |11        |14      |
|ted    |7       |11    |13        |16      |
+-------------------------------------------+

使用水平切割,结果如下所示:

+-------------------------------------------+
|student|fromDate|toDate|fromPeriod|toPeriod|
|-------------------------------------------|
|amy    |1       |7     |9         |9       |
|amy    |1       |9     |7         |8       |
|amy    |3       |9     |5         |6       |
|amy    |10      |12    |1         |3       |
|ted    |1       |5     |11        |14      |
|ted    |7       |11    |13        |16      |
+-------------------------------------------+

任何一个都可以接受。不过,为了保持它的确定性和易处理性,您需要选择一种策略并坚持下去。

【问题讨论】:

  • 请向我们展示您对样本数据的预期结果。
  • 这个问题没有一个答案,因为您可以聚合范围并以不同的方式重新组合它们。:所以您的结果实际上不是确定性的?这听起来像是一个不公平的要求......
  • @GMB,它很可能是确定性的。您可以编写一个算法,使用特定的输入,给出相同的预期输出。我只是说对于手头的目标可以接受不同的结果标准。
  • @GMB,重新设计了预期的结果。希望它能更好地解决您的问题。感谢您的反馈。

标签: sql-server merge range overlap rectangles


【解决方案1】:

数字表:

要解决我在帖子中指出的几何问题,您必须使用 SQL Server 几何数据类型。不幸的是,要在几何值内获取每个单独的形状或点,您必须按索引调用形状。数字表有助于解决此问题。所以我先这样做(将其替换为您的首选实现)。

create table #numbers (i int);

declare @i int = 1;
while @i <= 100 begin
    insert #numbers values (@i);
    set @i += 1;
end;

聚合范围:

第一个需要的任务是将数字范围转换为几何矩形。 Point 创建角点。 STUnionSTEnvelope 用于将这些变成 长方形。此外,由于我们希望范围在整数相邻时合并在一起,因此我们在几何转换之前将 'to' 字段加 1。

那么矩形必须联合起来,这样就没有重叠。这是由UnionAggregate 完成的。结果是一个rectilinearPolygons(方形)的几何对象。

几何对象仍然可以有多个直线多边形。所以这些被列出并作为单独的形状输出到rectilinears

with

    aggregateRectangles as (

        select      student, 
                    rectilinears = geometry::UnionAggregate(rectangle)
        from        @schedule s
        cross apply (select 
                        minPt = geometry::Point(s.fromDate, s.fromPeriod, 0),
                        maxPt = geometry::Point(s.toDate + 1, s.toPeriod + 1, 0)
                    ) extremePoints
        cross apply (select rectangle = minPt.STUnion(maxPt).STEnvelope()) enveloped
        group by    student 

    )

    select      ar.student, 
                r.rectilinear,
                mm.minY,
                mm.maxY
    into        #rectilinears
    from        aggregateRectangles ar
    join        #numbers n on n.i between 1 and ar.rectilinears.STNumGeometries()
    cross apply (select rectilinear = ar.rectilinears.STGeometryN(n.i)) r
    cross apply (select envelope = r.rectilinear.STEnvelope()) e
    cross apply (select 
                    minY = e.envelope.STPointN(1).STY, 
                    maxY = e.envelope.STPointN(3).STY 
                ) mm;

SideNote - 性能选项:

我没有在这里实现它。但是,如果您正在使用大数据,并且您上面的“直线”(复数)字段在许多分组之间共享(例如许多具有相同时间表的学生),则保存直线对象的 Well-known-text 版本(只需ToString())。在此之后,创建具有不同直线的第二个数据集,并对该压缩数据集执行剩余的几何操作。稍后将其加入学生级别。这在我的实际案例中显着提高了性能。

分解范围:

接下来,必须将这些直线分解回矩形。通过在每个点的 x 坐标处创建垂直线来创建拆分器。 y 轴可以很容易地选择,我只是为我自己的语义选择了 x。也可以同时选择两个轴,但这会导致记录过多。

不幸的是,如果拆分器的宽度为零,SQL Server 不会拆分形状(从理论上讲,这是不合适的,但我想您无法以 WKT 格式正确表示结果)。所以我们需要给分离器一个缓冲区,以便它们有一个区域。有STBuffer,虽然我遇到了麻烦,所以我只是手动创建了一个。

这样,矩形就被分割了。当它们被拆分时,它们仍然都驻留在同一个几何对象中,因此它们被枚举然后单独插入到#rectangles 表中。

with

    createSplitters as (

        select      r.student,
                    rectilinear = geometry::STGeomFromText(r.rectilinear.ToString(), 0),
                    splitters = geometry::UnionAggregate(sp.splitter)
        from        #rectilinears r
        join        #numbers n on n.i between 1 and r.rectilinear.STNumPoints()
        cross apply (select 
                        x = r.rectilinear.STPointN(n.i).STX,
                        buffer = 0.001
                    ) px
        cross apply (select splitter =
                        geometry::Point(x - buffer, minY - buffer, 0).STUnion(
                            geometry::Point(x + buffer, maxY + buffer, 0)
                        ).STEnvelope()
                    ) sp
        group by    r.student,
                    r.rectilinear.ToString()

    )

    select      student,
                rectangle = rectangles.STGeometryN(n.i)
    into        #rectangles
    from        createSplitters sp
    cross apply (select 
                    rectangles = rectilinear.STDifference(sp.splitters)
                ) r
    join        #numbers n on n.i between 1 and r.rectangles.STNumGeometries();

解析范围:

这就是它的症结所在。剩下的只是从矩形中提取适当的值来给出范围。

为此,我们首先调用STEnvelope 以确保矩形仅由它们的角点表示。然后我们圆角点以撤消缓冲区的影响,以及浮点表示的任何问题。我们还从“to”字段中减去 1,以撤消我们在转换为几何点之前所做的操作。

select      student,
            fromDate = round(minPt.STX,0),
            toDate = round(maxPt.STX,0) - 1,
            fromPeriod = round(minPt.STY,0),
            toPeriod = round(maxPt.STY,0) - 1
into        #normalized
from        #rectangles r
cross apply (select 
                minPt = r.rectangle.STPointN(1), 
                maxPt = r.rectangle.STPointN(3)
            ) corners
order by    student, fromDate, fromPeriod;

可选 - 前后可视化:

我已经做到了这一点,所以我很好地给出了之前和之后结果的可视化表示。按 SSMS 中的“空间结果”选项卡,选择“学生”作为标签列,并在“未归一化”和“归一化”之间切换作为空间列。

Amy 的矩形之间的间隔起初看起来像是一个错误,但请记住,我们的“to”字段不仅代表记录在其中的数字,还代表整个小数部分,直到但不包括下一个整数。因此,例如,2 的 toDate 实际上是 2.99999 等的截止日期。

select      student,
            unnormalized = 
                geometry::Point(fromDate, fromPeriod, 0).STUnion(
                    geometry::Point(toDate, toPeriod, 0)
                ).STEnvelope(),
            normalized = null
from        @schedule s

union all
select      student,
            unnormalized = null,
            normalized = 
                geometry::Point(fromDate, fromPeriod, 0).STUnion(
                    geometry::Point(toDate, toPeriod, 0)
                ).STEnvelope()
from        #normalized;

【讨论】:

    【解决方案2】:

    这是一个非常有创意的解决方案,而且读起来很有趣!!

    一个相当简单的方法:

    with 
    
        a as (    
    
            select student, fromdate from @schedule union
            select student, todate+1 from @schedule    
    
        ),
    
        b as (
    
            select   *, 
                     todate = (
                         select   min(aa.fromdate) 
                         from a as aa 
                         where aa.student = a.student 
                         and aa.fromdate > a.fromdate
                     ) - 1 
            from     a
        )
    
        select    *
        from      b
        where     exists (
                      select   * 
                      from     @schedule as s 
                      where    s.student = b.student 
                      and      s.fromdate < b.todate 
                      and      s.todate > b.fromdate
                  );
    

    【讨论】:

    • 感谢 Iptr。但这只会合并日期范围而忽略期间范围。我需要同时合并日期和期间范围。如果您可以扩展您的工作方法,那就太棒了。
    • 我应该补充一点,我喜欢发布此内容,以便人们可以看到我正在尝试做的一维等价物。我相信这是“我的研究”下的链接中描述的方法之一。
    • 您可以通过将存在替换为交叉应用来访问期间(交叉应用是隐式存在),然后相应地调整期间限制。此外,获取 todate 的 LEAD() 而不是子查询可能会更高效。
    猜你喜欢
    • 1970-01-01
    • 2020-12-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-04-05
    • 2018-12-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多