LIMITE groupée dans PostgreSQL: afficher les N premières lignes de chaque groupe?

183

Je dois prendre les N premières lignes pour chaque groupe, triées par colonne personnalisée.

Compte tenu du tableau suivant:

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)

J'ai besoin des 2 premières lignes (classées par nom ) pour chaque section_id , c'est à dire un résultat similaire à:

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

J'utilise PostgreSQL 8.3.5.

Kouber Saparev
la source

Réponses:

290

Nouvelle solution (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
la source
8
Cela fonctionne également avec PostgreSQL 8.4 (les fonctions de fenêtre commencent par 8.4).
Bruno
5
Impressionnant! Cela fonctionne parfaitement. Je suis curieux cependant, y a-t-il un moyen de faire cela group by?
NurShomik
2
Pour ceux qui travaillent avec des millions de lignes et recherchent un moyen vraiment performant de le faire, la réponse la plus chic est la voie à suivre. N'oubliez pas de pimenter le ti avec une indexation appropriée.
Diligent Key Presser
39

Depuis la v9.3, vous pouvez faire une jointure latérale

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;

Cela peut être plus rapide mais, bien sûr, vous devez tester les performances spécifiquement sur vos données et votre cas d'utilisation.

chic
la source
4
Solution très cryptique IMO, spécialement avec ces noms, mais une bonne.
villasv
1
Cette solution avec LATERAL JOIN pourrait être beaucoup plus rapide que celle au-dessus de celle avec fonction fenêtrée (dans certains cas) si vous avez un index par t_inner.namecolonne
Artur Rashitov
La requête est plus facile à comprendre si elle ne contient pas la jointure automatique. Dans ce cas, ce distinctn'est pas nécessaire. Un exemple est montré dans le lien posté le plus chic.
gillesB
Mec, c'est époustouflant. 120 ms au lieu de 9 s ont donné avec la solution "ROW_NUMBER". Merci!
Diligent Key Presser
Comment pouvons-nous sélectionner toutes les colonnes de t_top. La table t contient une colonne json et j'obtiens l'erreur "impossible d'identifier l'opérateur d'égalité pour le type json postgres" lorsque je sélectionnedistinct t_outer.section_id, t_top.*
suat
12

Voici une autre solution (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
la source
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
la source
La requête est très proche de celle dont j'ai besoin, sauf qu'elle n'affiche pas les sections de moins de 2 lignes, c'est-à-dire que la ligne avec ID = 7 n'est pas retournée. Sinon j'aime votre approche.
Kouber Saparev le
Merci, je viens de trouver la même solution avec COALESCE, mais vous étiez plus rapide. :-)
Kouber Saparev
En fait, la dernière sous-clause JOIN pourrait être simplifiée à: ... AND x.id <= (mlast) .id car l'ID a déjà été choisi en fonction du champ de nom, non?
Kouber Saparev
@Kouber: dans votre exemple, les name's et id' sont triés dans le même ordre, donc vous ne le verrez pas. Faites les noms dans l'ordre inverse et vous verrez que ces requêtes donnent des résultats différents.
Quassnoi le
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
la source
Les fonctions CTE et Window ont été introduites avec la même version, donc je ne vois pas l'avantage de la première solution.
a_horse_with_no_name
Le poste a trois ans. De plus, il se peut qu'il y ait encore des implémentations qui en manquent (nudge nudge dis pas plus). Cela pourrait également être considéré comme un exercice de construction de requêtes à l'ancienne. (bien que les CTE ne soient pas très anciens)
wildplasser
Le message est étiqueté "postgresql" et la version PostgreSQL qui a introduit les CTE a également introduit des fonctions de fenêtrage. D'où mon commentaire (j'ai vu que c'était si vieux - et PG 8.3 n'avait ni l'un ni l'autre)
a_horse_with_no_name
Le message mentionne 8.3.5, et je crois qu'ils ont été introduits en 8.4. En outre: il est également bon de connaître les scénarios alternatifs, à mon humble avis.
wildplasser
C'est exactement ce que je veux dire: 8.3 n'avait ni CTE ni fonctions de fenêtrage. Donc, la première solution ne fonctionnera pas sur 8.3
a_horse_with_no_name