在 MySQL 中对重叠的数据范围进行分组

Posted

技术标签:

【中文标题】在 MySQL 中对重叠的数据范围进行分组【英文标题】:Group overlapping ranges of data in MySQL 【发布时间】:2020-09-09 19:11:58 【问题描述】:

有没有一种简单的方法可以避免使用游标来转换它:

+-------+------+-------+
| Group | From | Until |
+-------+------+-------+
| X     | 1    | 3     |
+-------+------+-------+
| X     | 2    | 4     |
+-------+------+-------+
| Y     | 5    | 7     |
+-------+------+-------+
| X     | 8    | 10    |
+-------+------+-------+
| Y     | 11   | 12    |
+-------+------+-------+
| Y     | 12   | 13    |
+-------+------+-------+

进入这个:

+-------+------+-------+
| Group | From | Until |
+-------+------+-------+
| X     | 1    | 4     |
+-------+------+-------+
| Y     | 5    | 7     |
+-------+------+-------+
| X     | 8    | 10    |
+-------+------+-------+
| Y     | 11   | 13    |
+-------+------+-------+

到目前为止,我已经尝试为每一行分配一个 ID,并按该 ID 分组,但如果不使用游标,我就无法接近。

【问题讨论】:

你用的是哪个mysql版本? 如果 X 和 Y 在同一范围内有效怎么办? X,1,2 和 X,2,3 应该加入 X,1,3 吗? X,1,2 和 X,3,4 呢,它们是分开的还是连接到 X,1,4 的? 【参考方案1】:
SELECT `Group`, `From`, `Until`
FROM ( SELECT `Group`, `From`, ROW_NUMBER() OVER (PARTITION BY `Group` ORDER BY `From`) rn
       FROM test t1
       WHERE NOT EXISTS ( SELECT NULL
                          FROM test t2
                          WHERE t1.`From` > t2.`From`
                            AND t1.`From` <= t2.`Until`
                            AND t1.`Group` = t2.`Group` ) ) t3
JOIN ( SELECT `Group`, `Until`, ROW_NUMBER() OVER (PARTITION BY `Group` ORDER BY `From`) rn
       FROM test t1
       WHERE NOT EXISTS ( SELECT NULL
                          FROM test t2
                          WHERE t1.`Until` >= t2.`From`
                            AND t1.`Until` < t2.`Until`
                            AND t1.`Group` = t2.`Group` ) ) t4 USING (`Group`, rn)

fiddle

必须适用于任何重叠类型(部分重叠、相邻、完全包含)。

如果 From 和/或 Until 为 NULL,则不起作用。



你能添加一个英文解释吗? – 是的

第一个子查询搜索连接的范围开始(参见小提琴 - 它单独执行) - 它在一个组中搜索 From 值,该组不在任何其他范围的中间/末端(允许起点相等)。

第二个子查询对连接范围 Until 执行相同的操作。

两者都额外枚举找到的值升序。

外部查询只是将每个范围的开始和结束连接成一行。

【讨论】:

@ysth 对于1-2、3-4、5-6,都应该加入1-6 ???这三个部分范围中有哪一对重叠或至少相邻?没有任何点同时属于 2 个或多个范围。 @ysth 但我的观点同样适用于 1-3、2-6、5-7 fiddle,组 'T'。 好吧,我想我误解了你在做什么。可以加个英文解释吗? @ysth 添加了一些解释。 那很优雅【参考方案2】:

如果您使用的是 MYSQL 版本 8+,那么您可以使用 row_number 来获得所需的结果:

Demo

  SELECT MIN(`FROM`) START, 
MAX(`UNTIL`) END, 
`GROUP` FROM (
SELECT A.*, 
ROW_NUMBER() OVER(ORDER BY `FROM`) RN_FROM,
ROW_NUMBER() OVER(PARTITION BY `GROUP` ORDER BY `UNTIL`) RN_UNTIL
FROM Table_lag A) X  
GROUP BY `GROUP`, (RN_FROM - RN_UNTIL) 
ORDER BY START;

【讨论】:

错误。 dbfiddle.uk/… 那不是数据,用户没有提供重叠数据,在您的情况下,您添加了重叠数据,即 103 和 106。如果您删除该查询,则可以正常工作。 用户提供的数据没有重叠前两行 (1,3) 和 (2,4) 重叠。 其重叠但不完整的子集,我的意思是没有提供子集数据。在您上面的行中是一个子集。 没错,样本数据没有包含。但我不认为这是不可能的。【参考方案3】:

您可以仅使用窗口函数执行此操作,使用一些间隙和岛技术。

这个想法是使用lag()和一个窗口sum()来构建一组具有相同组和重叠范围的连续记录。然后您可以聚合这些组:

select grp, min(c_from) c_from, max(c_until) c_until
from (
    select
        t.*,
        sum(lag_c_until < c_from) over(partition by grp order by c_from) mygrp
    from (
        select
            t.*,
            lag(c_until, 1, c_until) over(partition by grp order by c_from) lag_c_until
        from mytable t
    ) t
) t
group by grp, mygrp

您选择的列名与 SQL 关键字(groupfrom)冲突,因此我将它们重命名为grpc_fromc_until

Demo on DB Fiddle - 感谢 ysth 首先创建小提琴:

grp | c_来自 | c_until :-- | -----: | ------: X | 1 | 4 是 | 5 | 7 X | 8 | 10 是 | 11 | 13

【讨论】:

我只是从他们对 Atif 答案的评论中提取了 Akina 的小提琴,并添加了一个测试用例。 我将您的查询添加到我的小提琴中,调整表/列名称以匹配,但结果不佳。也许我调整了一些错误的东西。 dbfiddle.uk/… 我一定是做错了什么。只需将 Z 数据从我的小提琴添加到您的工作..除了您没有将 Z 113-114 合并到 Z 100-112 组中。 dbfiddle.uk/… 我以为只是 sum() 中的表达式需要添加 -1,但这并不能解决问题。但我对整件事持怀疑态度;您依赖于滞后时间,但不依赖于直到。并且即使您同时按 from 和 until 订购,您也可以有一个像 1-5、2-3、6-7 这样的系列,其中一个滞后无济于事。我真的认为它需要递归 嗯。我错了。您可以在没有 cte 或窗口函数的情况下执行此操作。更新了我的答案【参考方案4】:

我会为此使用递归 CTE:

with recursive intervals (`Group`, `From`, `Until`) as (
    select distinct t1.Group, t1.From, t1.Until
    from Table_lag t1
    where not exists (
        select 1
        from Table_lag t2
        where t1.Group=t2.Group
        and t1.From between t2.From and t2.Until+1
        and (t1.From,t1.Until) <> (t2.From,t2.Until)
    )
    union all
    select t1.Group, t1.From, t2.Until
    from intervals t1
    join Table_lag t2
        on t2.Group=t1.Group
        and t2.From between t1.From and t1.Until+1
        and t2.Until > t1.Until
)
select `Group`, `From`, max(`Until`) as Until
from intervals
group by `Group`, `From`
order by `From`, `Group`;

锚表达式 (select .. where not exists (...)) 找到所有组 & from 不会与之前的一些 from 组合(因此在我们的最终输出中每一行都有一行):

然后递归查询为我们的每一行添加合并间隔的行。

然后按组和从(那些是可怕的列名)分组以获得最大的 每个起始组/从的间隔。

https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=9efa508504b80e44b73c952572394b76

或者,您可以使用一组简单的连接和子查询来完成此操作,而无需 CTE 或窗口函数:

select
    interval_start_range.grp,
    interval_start_range.start,
    max(merged.finish) finish
from (
    select
        interval_start.grp,
        interval_start.start,
        min(later_interval_start.start) next_start
    from (
        select distinct t1.grp, t1.start, t1.finish
        from Table_lag t1
        where not exists (
            select 1
            from Table_lag t2
            where t1.grp=t2.grp
            and t1.start between t2.start and t2.finish+1
            and (t1.start,t1.finish) <> (t2.start,t2.finish)
        )
    ) interval_start
    left join (
        select distinct t1.grp, t1.start, t1.finish
        from Table_lag t1
        where not exists (
            select 1
            from Table_lag t2
            where t1.grp=t2.grp
            and t1.start between t2.start and t2.finish+1
            and (t1.start,t1.finish) <> (t2.start,t2.finish)
        )
    ) later_interval_start
        on interval_start.grp=later_interval_start.grp
        and interval_start.start < later_interval_start.start
    group by interval_start.grp, interval_start.start
) as interval_start_range
join Table_lag merged
    on merged.grp=interval_start_range.grp
    and merged.start >= interval_start_range.start
    and (interval_start_range.next_start is null or merged.start < interval_start_range.next_start)
group by interval_start_range.grp, interval_start_range.start
order by interval_start_range.start, interval_start_range.grp

(我已将此处的列重命名为不需要反引号。)

这里有一个选择来获取我们将报告的可报告间隔的所有开始,加入另一个类似的选择(您可以使用 CTE 来避免冗余)以找到同一组的以下可报告间隔的开始(如果有的话)。这包含在一个子查询中,以获取组、起始值和以下可报告间隔的起始值。然后它只需要加入在该范围内开始的所有其他记录并选择最大的结束值。

https://dbfiddle.uk/?rdbms=mysql_5.5&fiddle=151cc933489c299f7beefa99e1959549

【讨论】:

以上是关于在 MySQL 中对重叠的数据范围进行分组的主要内容,如果未能解决你的问题,请参考以下文章

如何在 MDX 中对同一维度进行分组和过滤

在 SQL 中对有序数据中的子集进行分组

如何从单个表中对多行进行分组并获取给定范围内的所有记录

如何在 MySQL 中对具有不同平均值的三个变量进行分组?

在 MySQL 5.7 中对“孤岛”进行分组

根据重叠时段对数据进行分组或求和