LIMIT agrupado no PostgreSQL: mostra as primeiras N linhas para cada grupo?

179

Preciso pegar as primeiras N linhas para cada grupo, ordenadas por coluna personalizada.

Dada a seguinte tabela:

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)

Preciso das 2 primeiras linhas (ordenadas por nome ) para cada section_id , ou seja, um resultado semelhante a:

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

Estou usando o PostgreSQL 8.3.5.

Kouber Saparev
fonte

Respostas:

279

Nova solução (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;
Dave
fonte
8
Isso funciona com o PostgreSQL 8.4 também (as funções da janela começam com 8.4).
de Bruno
2
Textbook resposta para fazer limite agrupados
piggybox
4
Impressionante! Funciona perfeitamente. Estou curioso, existe uma maneira de fazer isso group by?
21916 NurShomik
1
Para quem trabalha com milhões de linhas e procura uma maneira realmente eficiente de fazer isso - a resposta mais chique é o caminho a percorrer. Só não se esqueça de apimentá-lo com a indexação adequada.
Diligent Key Presser
37

Desde a v9.3, você pode fazer uma junção lateral

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;

Pode ser mais rápido , mas, é claro, você deve testar o desempenho especificamente em seus dados e caso de uso.

chique
fonte
4
Solução muito enigmática IMO, especialmente com esses nomes, mas que é boa.
villasv
1
Esta solução com LATERAL Cadastre poderia ser significativamente mais rápido do que acima um com função de janela (em alguns casos) se você tem índice por t_inner.namecoluna
Artur Rashitov
A consulta é mais fácil de entender se ela não contiver a associação automática. Nesse caso, distinctnão é necessário. Um exemplo é mostrado no link posthest postado.
precisa saber é o seguinte
Cara, isso é assustador. 120 ms em vez de 9 s resultaram na solução "ROW_NUMBER". Obrigado!
Diligent Key Presser
Como podemos selecionar todas as colunas de t_top. A tabela t contém uma coluna json e eu recebo o erro "não foi possível identificar o operador de igualdade para o tipo json postgres" ao selecionardistinct t_outer.section_id, t_top.*
suat
12

Aqui está outra solução (PostgreSQL <= 8.3).

SELECT
  *
FROM
  xxx a
WHERE (
  SELECT
    COUNT(*)
  FROM
    xxx
  WHERE
    section_id = a.section_id
  AND
    name <= a.name
) <= 2
Kouber Saparev
fonte
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
fonte
A consulta está muito próxima da que eu preciso, exceto que ela não está mostrando seções com menos de 2 linhas, ou seja, a linha com ID = 7 não é retornada. Caso contrário, eu gosto da sua abordagem.
Kouber Saparev
Obrigado, acabei de chegar à mesma solução com o COALESCE, mas você foi mais rápido. :
Kouber Saparev
Na verdade, a última sub-cláusula JOIN pode ser simplificada para: ... AND x.id <= (mlast) .id como o ID já foi escolhido de acordo com o campo de nome, não?
Kouber Saparev
@ Kouber: no seu exemplo, os name'e id' são classificados na mesma ordem, para que você não o veja. Faça os nomes na ordem inversa e você verá que essas consultas geram resultados diferentes.
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
fonte
As funções CTEs e Window foram introduzidas com a mesma versão, portanto não vejo o benefício da primeira solução.
A_horse_with_no_name
O post tem três anos. Além disso, ainda pode haver implementações que não as possuem (o empurrão não diz mais nada). Também poderia ser considerado um exercício na construção de consultas com estilo antigo. (embora CTEs não são muito old-fashoned)
wildplasser
A postagem é marcada com "postgresql" e a versão do PostgreSQL que introduziu CTEs também introduziu funções de janelas. Daí o meu comentário (eu vi é que o velho - e PG 8.3 tinha nem)
a_horse_with_no_name
O post menciona 8.3.5, e acredito que eles foram introduzidos na 8.4. Além disso: também é bom saber sobre cenários alternativos, IMHO.
wildplasser
É exatamente isso que eu quero dizer: 8.3 não tinham CTEs nem funções de janelas. Assim, a primeira solução não irá funcionar em 8.3
a_horse_with_no_name