在PostgreSQL中分组LIMIT:显示每个组的前N行?


183

我需要每个组的前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的前两行(按名称排序),即结果类似于:

 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。

Answers:


290

新解决方案(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;

8
这也适用于PostgreSQL 8.4(窗口功能以8.4开头)。
布鲁诺

5
太棒了!它完美地工作。我很好奇,有没有办法做到这一点group by
NurShomik '16

2
对于需要处理数百万行的人,并寻求真正高效的方法来做到这一点-最好的答案就是要走的路。只是不要忘记使用适当的索引来增加趣味性。
勤奋的按键

39

从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;

可能会更快,但是,当然,您应该专门针对数据和用例测试性能。


4
IMO非常神秘的解决方案,特别是那些名称,但很好。
villasv

1
如果您按列进行索引,那么使用LATERAL JOIN的解决方案可能会比使用窗口函数的解决方案(在某些情况下)明显更快t_inner.name
Artur Ra​​shitov

如果查询不包含自联接,则更易于理解。在这种情况下,distinct则不需要。最豪华的发布链接中显示了一个示例。
gillesB '18

杜德,这真令人沮丧。使用“ ROW_NUMBER”解决方案可获得120毫秒(而不是9秒)。谢谢!
勤奋的按键者

我们如何选择t_top的所有列。t表包含一个json列,当我选择distinct t_outer.section_id, t_top.*
suat

12

这是另一个解决方案(PostgreSQL <= 8.3)。

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

2
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的行。否则我喜欢你的方法。
Kouber Saparev 09年

谢谢,我刚与COALESCE一起使用了相同的解决方案,但是速度更快。:-)
Kouber Saparev 09年

实际上,最后一个JOIN子句可以简化为:... AND x.id <=(mlast).id,因为已经根据名称字段选择了ID,不是吗?
Kouber Saparev 09年

@Kouber:在您的示例中,name“ s”和id“ s”以相同的顺序排序,因此您不会看到它。以相反的顺序命名,您将看到这些查询产生不同的结果。
Quassnoi

2
        -- 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
        ;

CTE和Window函数是在同一版本中引入的,因此我看不到第一个解决方案的好处。
a_horse_with_no_name 2012年

这个职位已经三岁了。此外,可能仍然有一些缺少它们的实现(请轻推,不再赘述)。也可以将其视为老式查询构建中的练习。(尽管CTE并不是很老套)
wildplasser 2012年

该帖子被标记为“ postgresql”,引入CTE的PostgreSQL版本也引入了窗口功能。因此,我的评论(我确实看到它是旧的-和PG 8.3都没有)
a_horse_with_no_name 2012年

该帖子提到了8.3.5,我相信它们是在8.4中引入的。此外:最好了解替代方案恕我直言。
wildplasser 2012年

这就是我的意思:8.3既没有CTE也没有窗口功能。因此,第一个解决方案将无法在8.3版上运行
a_horse_with_no_name 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.