Zgrupowany LIMIT w PostgreSQL: pokazać pierwsze N ​​wierszy dla każdej grupy?

179

Muszę wziąć pierwsze N ​​wierszy dla każdej grupy, uporządkowane według niestandardowej kolumny.

Biorąc pod uwagę poniższą tabelę:

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)

Potrzebuję pierwszych 2 wierszy (uporządkowanych według nazwy ) dla każdego section_id , czyli wyniku podobnego do:

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

Używam PostgreSQL 8.3.5.

Kouber Saparev
źródło

Odpowiedzi:

279

Nowe rozwiązanie (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
źródło
8
Działa to również z PostgreSQL 8.4 (funkcje okna zaczynają się od 8.4).
Bruno
2
Podręcznik odpowiedź do zgrupowanego limitu
piggybox
4
Niesamowite! Działa bez zarzutu. Jestem jednak ciekawy, czy jest na to sposób group by?
NurShomik
1
Dla tych, którzy pracują z milionami wierszy i szukają naprawdę wydajnego sposobu na zrobienie tego - odpowiedzią poshest jest droga do zrobienia. Po prostu nie zapomnij doprawić ti odpowiednim indeksowaniem.
Sumienny przycisk dociskowy
37

Od wersji 9.3 możesz wykonać połączenie boczne

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;

To to być szybsze, ale oczywiście powinieneś przetestować wydajność specjalnie na danych i przypadku użycia.

poshest
źródło
4
Bardzo tajemnicze rozwiązanie IMO, szczególnie z tymi nazwami, ale dobre.
villasv
1
To rozwiązanie z LATERAL JOIN może być znacznie szybsze niż poprzednie z funkcją okienkową (w niektórych przypadkach), jeśli masz indeks po t_inner.namekolumnie
Artur Rashitov
Zapytanie jest łatwiejsze do zrozumienia, jeśli nie zawiera samosprzężenia. W takim przypadku distinctnie jest potrzebne. Przykład jest pokazany w opublikowanym linku poshest.
gillesB
Stary, to niewiarygodne. 120 ms zamiast 9 sekund przy rozwiązaniu „ROW_NUMBER”. Dziękuję Ci!
Diligent Key Presser
Jak możemy wybrać wszystkie kolumny t_top. Tabela t zawiera kolumnę json i otrzymuję błąd „nie można zidentyfikować operatora równości dla typu json postgres” po wybraniudistinct t_outer.section_id, t_top.*
suat
12

Oto inne rozwiązanie (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
źródło
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
źródło
Zapytanie jest bardzo zbliżone do tego, którego potrzebuję, z wyjątkiem tego, że nie pokazuje sekcji z mniej niż 2 wierszami, tj. Wiersz o identyfikatorze = 7 nie jest zwracany. W przeciwnym razie podoba mi się twoje podejście.
Kouber Saparev
Dziękuję, właśnie doszedłem do tego samego rozwiązania z COALESCE, ale byłeś szybszy. :-)
Kouber Saparev
Właściwie ostatnią klauzulę JOIN można uprościć do: ... AND x.id <= (mlast) .id, ponieważ ID zostało już wybrane zgodnie z polem nazwy, nie?
Kouber Saparev
@Kouber: w Twoim przykładzie litery name„i id” są sortowane w tej samej kolejności, więc ich nie zobaczysz. Utwórz nazwy w odwrotnej kolejności, a zobaczysz, że te zapytania dają różne wyniki.
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
źródło
Funkcje CTE i Window zostały wprowadzone w tej samej wersji, więc nie widzę korzyści z pierwszego rozwiązania.
a_horse_with_no_name
Wpis ma trzy lata. Poza tym nadal mogą istnieć implementacje, które ich nie mają (szturchnij, nie mów więcej). Można to również uznać za ćwiczenie w staromodnym budowaniu zapytań. (choć CTE nie są bardzo staromodne)
wildplasser
Post jest oznaczony jako „postgresql”, a wersja PostgreSQL, która wprowadziła CTE, również wprowadziła funkcje okienkowe. Stąd mój komentarz (widziałem, że jest taki stary - a PG 8.3 nie miał żadnego)
a_horse_with_no_name
W poście wspomniano o 8.3.5 i uważam, że zostały one wprowadzone w 8.4. Poza tym: dobrze jest również wiedzieć o alternatywnych scenariuszach, IMHO.
wildplasser
Dokładnie to mam na myśli: 8.3 nie miało CTE ani funkcji okienkowych. Więc pierwsze rozwiązanie nie będzie działać w 8.3
a_horse_with_no_name