【问题标题】:MySQL: How to combine partitions of ranges into largest possible contiguous rangesMySQL:如何将范围的分区组合成最大可能的连续范围
【发布时间】:2018-09-05 17:44:21
【问题描述】:

我一直在尝试执行一个相当复杂的 SQL 查询(也许很简单?)来压缩包含重复信息的表。我在 SequelPro 中使用 MySQL 5.7.14。我是一个新手 SQL 用户,对连接、联合等有基本的了解。我认为这个需要一个带有一些分组依据的子查询,但我不知道如何做到最好。 下表说明了我正在尝试做的一个简单示例:

table

对于每个 col_1 重复条目,当 col_2 和 3 设置的范围(分别为范围的开始和结束)重叠时,我想压缩为单个条目。对于 col_4 和 5,应报告落在此范围内的条目中的最大值。对于上面的示例,在 col_1 中,a 有三个重叠范围,我想将其压缩到 col_1 的最小值和 col_2 的最大值,col_4 和 5 的最大值。对于 col_2 中的“b”,有两个范围(31-50, 12-15) 不重叠,因此它将按原样返回两行。对于 c,它将返回一行,范围为 100-300,col_4 和 col_5 的值分别为 3、2。此示例所需的完整结果如下所示:

query output

我应该补充一点,在某些地方有“空”值应该被视为零。 有人知道最好,最简单的方法吗? 提前谢谢你!

更新:我已尝试使用建议的范围设置查询,但出现错误。查询如下:

WITH a AS (SELECT range 
  , lower(col_2) AS startdate
  , max(upper(col_3)) OVER (ORDER BY range) AS `end`
   FROM   `combine`
   )
, b AS (
   SELECT *, lag(`end`) OVER (ORDER BY range) < `start` OR NULL AS step
   FROM   a
   )
, c AS (
   SELECT *, count(step) OVER (ORDER BY range) AS grp
   FROM   b
   )
SELECT daterange(min(`start`), max(`end`)) AS range
FROM   c
GROUP  BY grp
ORDER  BY 1;

我收到的错误是: 您的 SQL 语法有误;检查与您的 MySQL 服务器版本相对应的手册,以获取在 'a AS (SELECT range ,较低(col_2)作为开始日期 , max(upper(col_3)) OVE' 在第 1 行

【问题讨论】:

  • 我认为您无法在 1 个查询中完成。您可以使用光标(非常不高效),或者为什么不在代码中(在您的应用程序中)执行此逻辑?

标签: mysql


【解决方案1】:

这并不简单,但可以在一个查询中完成。

困难的部分是将一组间隔组合成最大可能的连续间隔。解决方案详见this post

要获得您想要的结果,您现在需要:

  1. 使用链接中给出的查询计算 col1 中每个值的最大可能连续间隔。

根据您的示例值,结果将是:

col_1 lower_bound upper_bound
a     20          60
b     12          15
b     31          50
c     100         300
  1. 将这些大间隔之一与your_table 中的每一行相关联。每行只能有一个这样的间隔,所以让我们INNER JOIN

    SELECT my_table.*, large_intervals.lower_bound, large_intervals.upper_bound FROM my_table INNER JOIN (my_awesome_query(your_table)) large_intervals ON large_intervals.col1 = my_table.col1 AND large_intervals.lower_bound <= my_table.col2 AND large_intervals.upper_bound >= my_table.col3

你会得到:

col1 col2 col3 col4 col5 lower_bound upper_bound
a    45   50   1    0    20          60
a    50   61   6    0    20          60
a    20   45   0    5    20          60
b    31   50   0    1    31          50
b    12   15   5    0    12          15
c    100  200  3    2    100         300
c    150  300  1    2    100         300
  1. 那么很简单,只需按 col1、lower_bound、upper bound 分组即可:

SELECT col1, lower_bound AS col2, upper_bound AS col3, MAX(col4) AS col4, MAX(col5) AS col5 FROM (query above) decorated_table GROUP BY col1, lower_bound, upper_bound

你会得到你想要的结果。

回到困难的部分:上面提到的帖子公开了 PostgreSQL 的解决方案。 MySQL 没有范围类型,但可以调整解决方案。例如,代替lower(range),直接使用下限col2。该解决方案还使用了窗口函数,即laglead,但MySQL 支持with the same syntax,所以这里没有问题。另请注意,他们使用COALESCE(upper(range), 'infinity') 来防范未绑定的范围。由于你的范围是有限的,你不需要关心这个,你可以直接使用上限,即col3。这是改编:

WITH a AS (
   SELECT
       col2,
       col3,
       col2 AS lower_bound, 
       MAX(col3) OVER (ORDER BY col2, col3) AS upper_bound
   FROM   combine
   )
, b AS (
   SELECT *, lag(upper_bound) OVER (ORDER BY col2, col3) < lower_bound OR NULL AS step
   FROM   a
   )
, c AS (
   SELECT *, count(step) OVER (ORDER BY col2, col3) AS grp
   FROM   b
   )
SELECT
    MIN(lower_bound) AS lower_bound,
    MAX(upper_bound) AS range
FROM   c
GROUP  BY grp
ORDER  BY 1;

这适用于单个组。如果你想通过 col1 获取范围,你可以像这样调整它:

WITH a AS (
   SELECT
       col1,
       col2,
       col3,
       col2 AS lower_bound, 
       MAX(col3) OVER (PARTITION BY col1 ORDER BY col2, col3) AS upper_bound
   FROM   combine
   )
, b AS (
   SELECT *, lag(upper_bound) OVER (PARTITION BY col1 ORDER BY col2, col3) < lower_bound OR NULL AS step
   FROM   a
   )
, c AS (
   SELECT *, count(step) OVER (PARTITION BY col1 ORDER BY col2, col3) AS grp
   FROM   b
   )
SELECT
    MIN(lower_bound) AS lower_bound,
    MAX(upper_bound) AS range
FROM   c
GROUP  BY col1, grp
ORDER  BY 1;

结合所有内容,我们得到以下结果(在您提供的示例上进行了测试),它完全返回了您期望的输出:

WITH a AS (
   SELECT
       col1,
       col2,
       col3,
       col2 AS lower_bound, 
       MAX(col3) OVER (PARTITION BY col1 ORDER BY col2, col3) AS upper_bound
   FROM   combine
   )
, b AS (
   SELECT *, lag(upper_bound) OVER (PARTITION BY col1 ORDER BY col2, col3) < lower_bound OR NULL AS step
   FROM   a
   )
, c AS (
   SELECT *, count(step) OVER (PARTITION BY col1 ORDER BY col2, col3) AS grp
   FROM   b
   )
, large_intervals AS (
    SELECT
        col1,
        MIN(lower_bound) AS lower_bound,
        MAX(upper_bound) AS upper_bound
    FROM   c
    GROUP  BY col1, grp
    ORDER  BY 1
    )
, combine_with_large_interval AS (
    SELECT
        combine.*,
        large_intervals.lower_bound,
        large_intervals.upper_bound
    FROM combine
    INNER JOIN large_intervals
        ON large_intervals.col1 = combine.col1
        AND large_intervals.lower_bound <= combine.col2
        AND large_intervals.upper_bound >= combine.col3
)
SELECT
    col1,
    lower_bound AS col2,
    upper_bound AS col3, 
    MAX(col4) AS col4, 
    MAX(col5) AS col5
FROM combine_with_large_interval
GROUP BY col1, lower_bound, upper_bound
ORDER BY col1, col2, col3;

瞧!

【讨论】:

  • 感谢您的回复马蒂厄!
  • 谢谢马蒂厄!您很清楚,我需要为每个 col_1 值组合一组间隔(定义一个范围),然后进行内部连接以将其链接到原始表。但是,我不确定如何执行链接中描述的第一步。它提供了四种不同的方法来做到这一点——我应该使用哪一种?我认为我的示例与前一篇文章中的示例有很大不同,因为我使用两列中的信息来构建此范围。使用的 sql 函数超出了我的知识范围,让我怀疑是否应该使用 SQL 或其他东西。如果您能更清楚地说明这一点,我将不胜感激!
  • 我编辑了答案,以便更清楚地说明如何使解决方案适应您的用例,我认为您现在可以开始了。你可以使用 MySQL,即使 Postgres 更灵活。您真的不需要了解帖子中提到的解决方案的复杂性,只需将名称替换为您的名称,并检查输出是否满足您的要求 :) 也许将算法应用在一张纸上以感受一下有效以及为什么。
  • 谢谢,我理解并调整了我对“获取范围”查询的输入,但我遇到了我不认识的错误。它们与我使用的查询一起位于我上面帖子的“更新”部分。如果您有任何建议,我将不胜感激
  • 你现在已经得到了完整的解决方案;)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-01-30
  • 1970-01-01
  • 2018-06-06
  • 1970-01-01
  • 2011-08-05
  • 1970-01-01
  • 2013-06-29
相关资源
最近更新 更多