Сгруппированный LIMIT в PostgreSQL: показать первые N строк для каждой группы?

179

Мне нужно взять первые 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)

Мне нужны первые 2 строки (упорядоченные по имени ) для каждого 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.

Кубер Сапарев
источник

Ответы:

279

Новое решение (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).
Бруно
2
Ответ учебника, чтобы сделать групповой лимит
piggybox
4
Потрясающие! Работает без нареканий. Мне любопытно, есть ли способ сделать это group by?
НурШомик
1
Для тех, кто работает с миллионами строк и ищет действительно эффективный способ сделать это - ответ Пешеста - это путь. Просто не забудьте придать остроту правильной индексации.
Прилежный
37

Начиная с 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столбцам
Артур Рашитов
Запрос легче понять, если он не содержит самосоединения. В таком случае distinctне нужно. Пример показан в размещенной ссылке.
gillesB
Чувак, это умопомрачительно. 120мс вместо 9сек с решением "ROW_NUMBER". Спасибо!
Добросовестный Key Прижимная
Как мы можем выбрать все столбцы t_top. Таблица t содержит столбец json, и я получаю сообщение об ошибке «Не удалось идентифицировать оператор равенства для типа json postgres»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)
Quassnoi
источник
Запрос очень близок к тому, который мне нужен, за исключением того, что он не показывает разделы с менее чем 2 строками, то есть строка с ID = 7 не возвращается. В противном случае мне нравится ваш подход.
Кубер Сапарев
Спасибо, я только что пришел к тому же решению с COALESCE, но вы были быстрее. :-)
Кубер Сапарев
На самом деле последний подпункт JOIN можно упростить до: ... И x.id <= (mlast) .id, так как идентификатор уже выбран в соответствии с полем имени, нет?
Кубер Сапарев
@ 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
        ;
wildplasser
источник
CTE и оконные функции были представлены в одной и той же версии, поэтому я не вижу преимуществ первого решения.
a_horse_with_no_name
Посту три года. Кроме того, все еще могут быть реализации, в которых их нет (подтолкни, скажи больше). Это также можно считать упражнением в старомодном построении запросов. (хотя CTE не очень старомодны)
wildplasser
Сообщение помечено как «postgresql», а версия PostgreSQL, в которой представлены CTE, также представила функции управления окнами. Отсюда мой комментарий (я видел, что он такой старый - и PG 8.3 не имел ни того, ни другого)
a_horse_with_no_name
В посте упоминается 8.3.5, и я считаю, что они были введены в 8.4. Кроме того: также полезно знать об альтернативных сценариях, ИМХО.
wildplasser
Это именно то, что я имею в виду: в 8.3 не было ни CTE, ни оконных функций. Таким образом, первое решение не будет работать на 8.3
a_horse_with_no_name