【问题标题】:Window functions and more "local" aggregation窗口函数和更多“本地”聚合
【发布时间】:2024-01-22 07:19:01
【问题描述】:

假设我有这张桌子:

select * from window_test;

 k | v
---+---
 a | 1
 a | 2
 b | 3
 a | 4

最终我想得到:

 k | min_v | max_v
---+-------+-------
 a | 1     | 2
 b | 3     | 3
 a | 4     | 4

但我会很高兴得到这个(因为我可以很容易地用 distinct 过滤它):

 k | min_v | max_v
---+-------+-------
 a | 1     | 2
 a | 1     | 2
 b | 3     | 3
 a | 4     | 4

是否可以使用 PostgreSQL 9.1+ 窗口函数来实现这一点?我试图了解是否可以让它在此示例中对k=a 的第一次和最后一次出现使用单独的分区(按v 排序)。

【问题讨论】:

  • 第一:你想“折叠”完整的范围吗? IE。当您将(a,5), (a,6), (a,7) 添加到您的数据集时,您会期待什么?第二:v总是完美加1还是有差距?
  • @A.H. v 并没有完全增加(事实上,它是一个时间戳)。是的,我想折叠完整的范围,并希望 (a, 4, 5) 作为插入 (a, 5) 后的最后一个元组。
  • @KonradGarus,提供的任何解决方案都适合您吗?

标签: postgresql common-table-expression window-functions


【解决方案1】:

这将使用示例数据返回您想要的结果。不确定它是否适用于现实世界的数据:

select k, 
       min(v) over (partition by group_nr) as min_v,
       max(v) over (partition by group_nr) as max_v
from (
    select *,
           sum(group_flag) over (order by v,k) as group_nr
    from (
    select *,
           case
              when lag(k) over (order by v) = k then null
              else 1
            end as group_flag
    from window_test
    ) t1
) t2
order by min_v;

不过我忽略了DISTINCT

【讨论】:

  • 谢谢大家。我喜欢这两种解决方案,但觉得这个更优雅。
  • 那里做得很好,但它对我不起作用(postgresql 9.3),直到我在order by min_v 之前添加了group by k, group_nr 句子,并替换了min(v) over...max(v) over... 窗口函数简单聚合 min(v)max(v) 函数的主要选择。谢谢,这真的帮助了我:)
【解决方案2】:

编辑:我想出了以下查询——根本没有窗口函数:

WITH RECURSIVE tree AS (
  SELECT k, v, ''::text as next_k, 0 as next_v, 0 AS level FROM window_test
  UNION ALL
  SELECT c.k, c.v, t.k, t.v + level, t.level + 1
    FROM tree t JOIN window_test c ON c.k = t.k AND c.v + 1 = t.v),
partitions AS (
  SELECT t.k, t.v, t.next_k,
         coalesce(nullif(t.next_v, 0), t.v) AS next_v, t.level
    FROM tree t
   WHERE NOT EXISTS (SELECT 1 FROM tree WHERE next_k = t.k AND next_v = t.v))
SELECT min(k) AS k, v AS min_v, max(next_v) AS max_v
  FROM partitions p
 GROUP BY v
 ORDER BY 2;

我现在提供了 2 个有效的查询,希望其中一个适合您。

SQL Fiddle 用于此变体。


实现此目的的另一种方法是使用支持序列。

  1. 创建支持序列:

    CREATE SEQUENCE wt_rank START WITH 1;
    
  2. 查询:

    WITH source AS (
      SELECT k, v,
             coalesce(lag(k) OVER (ORDER BY v), k) AS prev_k
        FROM window_test
        CROSS JOIN (SELECT setval('wt_rank', 1)) AS ri),
    ranking AS (
      SELECT k, v, prev_k,
             CASE WHEN k = prev_k THEN currval('wt_rank')
                  ELSE nextval('wt_rank') END AS rank
        FROM source)
    SELECT r.k, min(s.v) AS min_v, max(s.v) AS max_v
        FROM ranking r
        JOIN source s ON r.v = s.v
       GROUP BY r.rank, r.k
       ORDER BY 2;
    

【讨论】:

  • 谢谢。实际上,v 是排序列。如果我在末尾添加 (a, 5) 元组,则此解决方案会中断(换句话说,如果有几个集群,每个集群不止一行)。
【解决方案3】:

如果不需要窗户、隔板或聚结,这对您来说不合适吗?它只是使用传统的 SQL 技巧通过自连接查找最近的元组,并稍作区别:

SELECT k, min(v), max(v) FROM (
    SELECT k, v, v + min(d) lim FROM (
        SELECT x.*, y.k n, y.v - x.v d FROM window_test x
        LEFT JOIN window_test y ON x.k <> y.k AND y.v - x.v > 0) 
    z GROUP BY k, v, n)
w GROUP BY k, lim ORDER BY 2;

我认为这可能是一个更“相关”的解决方案,但我不确定它的效率。

【讨论】:

  • 带有窗口函数的解决方案通常更快。您的查询需要通过window_test 表两次(因为自联接)。使用 &lt;&gt; 的连接通常也无法扩展。另外:您正在对联接的结果进行分组,这意味着到那时该组需要使用窗口函数处理更多行(只需要处理一次表中的行)。但是窗口函数有什么问题呢?每个现代 DBMS 都支持它们。
  • 同意。当然,没有什么反对窗口函数的——我向 OP 提供了这个解决方案,因为它可能会感兴趣,因为它只使用关系代数中的运算符,因此可能被认为是“更纯粹的”(我个人认为它在概念上也更清晰)。但是,我不会在十亿行关系上使用它...