PostgreSQL 中的分组限制:显示每个组的前 N ​​行?

Posted

技术标签:

【中文标题】PostgreSQL 中的分组限制:显示每个组的前 N ​​行?【英文标题】:Grouped LIMIT in PostgreSQL: show the first N rows for each group? 【发布时间】:2010-11-10 14:38:03 【问题描述】:

我需要为每个组取前 N 行,按自定义列排序。

给定下表:

db=# SELECT * FROM xxx;
 id | section_id | name
----+------------+------
  1 |          1 | A
  2 |          1 | B
  3 |          1 | C
  4 |          1 | D
  5 |          2 | E
  6 |          2 | F
  7 |          3 | G
  8 |          2 | H
(8 rows)

我需要每个 section_id 的前 2 行(按 name 排序),即结果类似于:

 id | section_id | name
----+------------+------
  1 |          1 | A
  2 |          1 | B
  5 |          2 | E
  6 |          2 | F
  7 |          3 | G
(5 rows)

我使用的是 PostgreSQL 8.3.5。

【问题讨论】:

【参考方案1】:
SELECT  x.*
FROM    (
        SELECT  section_id,
                COALESCE
                (
                (
                SELECT  xi
                FROM    xxx xi
                WHERE   xi.section_id = xo.section_id
                ORDER BY
                        name, id
                OFFSET 1 LIMIT 1
                ),
                (
                SELECT  xi
                FROM    xxx xi
                WHERE   xi.section_id = xo.section_id
                ORDER BY 
                        name DESC, id DESC
                LIMIT 1
                )
                ) AS mlast
        FROM    (
                SELECT  DISTINCT section_id
                FROM    xxx
                ) xo
        ) xoo
JOIN    xxx x
ON      x.section_id = xoo.section_id
        AND (x.name, x.id) <= ((mlast).name, (mlast).id)

【讨论】:

该查询非常接近我需要的查询,只是它没有显示少于 2 行的部分,即不返回 ID=7 的行。否则我喜欢你的方法。 谢谢,我刚刚找到了与 COALESCE 相同的解决方案,但您更快。 :-) 其实最后一个 JOIN 子句可以简化为: ... AND x.id @Kouber:在您的示例中,nameid 按相同顺序排序,因此您不会看到它。将名称倒序排列,您将看到这些查询产生不同的结果。【参考方案2】:

这是另一个解决方案 (PostgreSQL

SELECT
  *
FROM
  xxx a
WHERE (
  SELECT
    COUNT(*)
  FROM
    xxx
  WHERE
    section_id = a.section_id
  AND
    name <= a.name
) <= 2

【讨论】:

【参考方案3】:

新解决方案(PostgreSQL 8.4)

SELECT
  * 
FROM (
  SELECT
    ROW_NUMBER() OVER (PARTITION BY section_id ORDER BY name) AS r,
    t.*
  FROM
    xxx t) x
WHERE
  x.r <= 2;

【讨论】:

这也适用于 PostgreSQL 8.4(窗口函数从 8.4 开始)。 太棒了!它完美无缺。不过我很好奇,有没有办法用group by 做到这一点? 对于那些使用数百万行并寻求真正高效的方法来做到这一点的人 - 最时髦的答案就是要走的路。只是不要忘记用适当的索引来增加趣味。 这在mysql 8.0.24中工作,快速将6M行关联的电子邮件地址压缩到每个类别中的五个(没有排序或标准,只需要五个电子邮件地址。少数类别(公司)有几千个地址。)【参考方案4】:
        -- ranking without WINDOW functions
-- EXPLAIN ANALYZE
WITH rnk AS (
        SELECT x1.id
        , COUNT(x2.id) AS rnk
        FROM xxx x1
        LEFT JOIN xxx x2 ON x1.section_id = x2.section_id AND x2.name <= x1.name
        GROUP BY x1.id
        )
SELECT this.*
FROM xxx this
JOIN rnk ON rnk.id = this.id
WHERE rnk.rnk <=2
ORDER BY this.section_id, rnk.rnk
        ;

        -- The same without using a CTE
-- EXPLAIN ANALYZE
SELECT this.*
FROM xxx this
JOIN ( SELECT x1.id
        , COUNT(x2.id) AS rnk
        FROM xxx x1
        LEFT JOIN xxx x2 ON x1.section_id = x2.section_id AND x2.name <= x1.name
        GROUP BY x1.id
        ) rnk
ON rnk.id = this.id
WHERE rnk.rnk <=2
ORDER BY this.section_id, rnk.rnk
        ;

【讨论】:

CTEs 和 Window 函数是在同一个版本中引入的,所以我看不到第一个解决方案的好处。 这个帖子已经三年了。此外,可能仍然存在缺少它们的实现(轻推轻推不再说)。它也可以被认为是老式查询构建的练习。 (尽管 CTE 不是很老套) 该帖子被标记为“postgresql”,并且引入 CTE 的 PostgreSQL 版本也引入了窗口函数。因此我的评论(我确实看到它很旧 - 而 PG 8.3 两者都没有) 帖子中提到了 8.3.5,我相信它们是在 8.4 中引入的。此外:了解替代方案也很好,恕我直言。 这正是我的意思:8.3 既没有 CTE,也没有窗口函数。所以第一个解决方案不适用于 8.3【参考方案5】:

从 v9.3 开始,您可以进行横向连接

select distinct t_outer.section_id, t_top.id, t_top.name from t t_outer
join lateral (
    select * from t t_inner
    where t_inner.section_id = t_outer.section_id
    order by t_inner.name
    limit 2
) t_top on true
order by t_outer.section_id;

它might be faster 但是,当然,您应该专门针对您的数据和用例测试性能。

【讨论】:

IMO 非常神秘的解决方案,特别是那些名字,但很好。 如果您有t_inner.name 列的索引,则此带有横向连接的解决方案可能明显快于带有窗口函数的上述解决方案(在某些情况下) 查询如果不包含自连接则更容易理解。在这种情况下,不需要distinct。发布的链接中显示了一个示例。 伙计,这太令人费解了。 “ROW_NUMBER”解决方案产生了 120 毫秒而不是 9 秒。谢谢! 如何选择t_top的所有列。 t 表包含一个 json 列,当我选择 distinct t_outer.section_id, t_top.* 时出现“无法识别类型 json postgres 的相等运算符”错误

以上是关于PostgreSQL 中的分组限制:显示每个组的前 N ​​行?的主要内容,如果未能解决你的问题,请参考以下文章

mysql中分组之后取每个组的前三个

sql数据库怎么实现分组并取每组的前1条语句,按日期排序?

获取每组的前 n 个结果 [重复]

如何限制jasper报告不要在每个组的新页面上启动?

oracle开展分组后,取出每组的前几条数据

在具有多个数字列的数据框中显示每个组的前 5 行