Gruppiertes LIMIT in PostgreSQL: Zeigen Sie die ersten N Zeilen für jede Gruppe an?


177

Ich muss die ersten N Zeilen für jede Gruppe nehmen, sortiert nach benutzerdefinierten Spalten.

Angesichts der folgenden Tabelle:

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)

Ich benötige die ersten 2 Zeilen (sortiert nach Namen ) für jede section_id , dh ein Ergebnis ähnlich dem:

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

Ich benutze PostgreSQL 8.3.5.

Antworten:


278

Neue Lösung (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
Dies funktioniert auch mit PostgreSQL 8.4 (Fensterfunktionen beginnen mit 8.4).
Bruno

2
Lehrbuch Antwort zu tun gruppierten Limit
Piggybox

3
Genial! Es funktioniert einwandfrei. Ich bin allerdings neugierig, gibt es eine Möglichkeit, dies zu tun group by?
NurShomik

1
Für diejenigen, die mit Millionen Zeilen arbeiten und nach einem wirklich performanten Weg suchen, dies zu tun, ist die Antwort von poshest der richtige Weg. Vergessen Sie nicht, Ti mit der richtigen Indizierung aufzupeppen.
Fleißiger Schlüsselpresser

36

Seit v9.3 können Sie eine seitliche Verbindung herstellen

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;

Es könnte schneller sein, aber natürlich sollten Sie die Leistung speziell für Ihre Daten und Ihren Anwendungsfall testen.


4
Sehr kryptische Lösung IMO, speziell mit diesen Namen, aber eine gute.
Villasv

1
Diese Lösung mit LATERAL JOIN ist möglicherweise erheblich schneller als eine Lösung mit Fensterfunktion (in einigen Fällen), wenn Sie einen Index nach t_inner.nameSpalten haben
Artur Rashitov

Die Abfrage ist leichter zu verstehen, wenn sie keinen Self-Join enthält. In diesem Fall distinctwird nicht benötigt. Ein Beispiel zeigt der vornehmste Link.
GillesB

Alter, das ist umwerfend. 120 ms statt 9 Sekunden ergaben mit der Lösung "ROW_NUMBER". Danke dir!
Fleißiger Schlüsselpresser

Wie können wir alle Spalten von t_top auswählen? Die t-Tabelle enthält eine json-Spalte und ich erhalte den Fehler "Gleichstellungsoperator für Typ json postgres konnte nicht identifiziert werden", wenn ichdistinct t_outer.section_id, t_top.*
suat

12

Hier ist eine andere Lösung (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)

Die Abfrage kommt der von mir benötigten sehr nahe, außer dass keine Abschnitte mit weniger als 2 Zeilen angezeigt werden, dh die Zeile mit der ID = 7 wird nicht zurückgegeben. Ansonsten gefällt mir dein Ansatz.
Kouber Saparev

Vielen Dank, ich bin gerade mit COALESCE zur gleichen Lösung gekommen, aber Sie waren schneller. :-)
Kouber Saparev

Tatsächlich könnte die letzte JOIN-Unterklausel vereinfacht werden zu: ... AND x.id <= (mlast) .id, da die ID bereits gemäß dem Namensfeld ausgewählt wurde, nein?
Kouber Saparev

@Kouber: In deinem Beispiel sind die name's und id' s in derselben Reihenfolge sortiert, sodass du sie nicht siehst. Machen Sie die Namen in umgekehrter Reihenfolge und Sie werden sehen, dass diese Abfragen unterschiedliche Ergebnisse liefern.
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
        ;

CTEs und Fensterfunktionen wurden mit derselben Version eingeführt, daher sehe ich den Nutzen der ersten Lösung nicht.
a_horse_with_no_name

Die Post ist drei Jahre alt. Außerdem kann es immer noch Implementierungen geben, denen diese fehlen (Nudge Nudge sagt nichts mehr). Es könnte auch als Übung im altmodischen Querybuilding angesehen werden. (obwohl CTEs nicht sehr altmodisch sind)
Wildplasser

Der Beitrag ist mit "postgresql" gekennzeichnet, und die PostgreSQL-Version, in der CTEs eingeführt wurden, führte auch Fensterfunktionen ein. Daher mein Kommentar (ich habe gesehen, dass es so alt ist - und PG 8.3 hatte auch keinen)
a_horse_with_no_name

Der Beitrag erwähnt 8.3.5, und ich glaube, sie wurden in 8.4 eingeführt. Außerdem: Es ist auch gut, über alternative Szenarien Bescheid zu wissen, IMHO.
Wildplasser

Genau das meine ich: 8.3 hatte weder CTEs noch Fensterfunktionen. Die erste Lösung funktioniert also nicht mit 8.3
a_horse_with_no_name
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.