LÍMITE agrupado en PostgreSQL: ¿mostrar las primeras N filas para cada grupo?

179

Necesito tomar las primeras N filas para cada grupo, ordenadas por columna personalizada.

Dada la siguiente tabla:

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)

Necesito las primeras 2 filas (ordenadas por nombre ) para cada section_id , es decir, un resultado similar a:

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

Estoy usando PostgreSQL 8.3.5.

Kouber Saparev
fuente

Respuestas:

279

Nueva solución (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
fuente
8
Esto también funciona con PostgreSQL 8.4 (las funciones de ventana comienzan con 8.4).
Bruno
2
Respuesta de libro de texto para hacer límite agrupado
piggybox
44
¡Increíble! Funciona a la perfección. Sin embargo, tengo curiosidad, ¿hay alguna manera de hacer esto group by?
NurShomik
1
Para aquellos que trabajan con millones de filas y buscan una forma realmente eficiente de hacerlo, la respuesta más correcta es el camino a seguir. Simplemente no olvide condimentarlo con una indexación adecuada.
Diligent Key Presser
37

Desde la v9.3 puedes hacer una unión 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;

Se podría ser más rápido , pero, por supuesto, usted debe probar el rendimiento específicamente en sus datos y casos de uso.

poshest
fuente
44
Solución muy críptica IMO, especialmente con esos nombres, pero una buena.
villasv
1
Esta solución con LATERAL JOIN podría ser significativamente más rápida que la anterior con función de ventana (en algunos casos) si tiene un índice por t_inner.namecolumna
Artur Rashitov
La consulta es más fácil de entender si no contiene la autounión. En ese caso distinctno es necesario. Se muestra un ejemplo en el enlace publicado más recientemente.
gillesB
Amigo, esto es alucinante. 120 ms en lugar de 9 segundos produjeron la solución "ROW_NUMBER". ¡Gracias!
Diligent Key Presser
¿Cómo podemos seleccionar todas las columnas de t_top? La tabla t contiene una columna json y obtengo el error "no se pudo identificar el operador de igualdad para el tipo json postgres" cuando seleccionodistinct t_outer.section_id, t_top.*
suat
12

Aquí hay otra solución (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
fuente
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
fuente
La consulta está muy cerca de la que necesito, excepto que no muestra secciones con menos de 2 filas, es decir, la fila con ID = 7 no se devuelve. De lo contrario, me gusta tu enfoque.
Kouber Saparev
Gracias, acabo de llegar a la misma solución con COALESCE, pero fuiste más rápido. :-)
Kouber Saparev
En realidad, la última subcláusula JOIN podría simplificarse a: ... AND x.id <= (mlast) .id ya que la ID ya se ha elegido de acuerdo con el campo de nombre, ¿no?
Kouber Saparev
@Kouber: en su ejemplo, las name'sy id' se ordenan en el mismo orden, por lo que no lo verá. Haga los nombres en orden inverso y verá que estas consultas producen 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
fuente
Las funciones CTE y Window se introdujeron con la misma versión, por lo que no veo el beneficio de la primera solución.
a_horse_with_no_name
El post tiene tres años. Además, todavía puede haber implementaciones que les faltan (empujar, empujar, no decir más). También podría considerarse un ejercicio en la construcción de consultas antiguas. (aunque los CTE no son muy antiguos)
wildplasser
La publicación está etiquetada como "postgresql" y la versión PostgreSQL que introdujo CTE también introdujo funciones de ventanas. De ahí mi comentario (vi que es tan antiguo, y PG 8.3 no tenía ninguno)
a_horse_with_no_name
La publicación menciona 8.3.5, y creo que se introdujeron en 8.4. Además: también es bueno saber sobre escenarios alternativos, en mi humilde opinión.
wildplasser
Eso es exactamente lo que quiero decir: 8.3 no tenía CTE ni funciones de ventanas. Entonces, la primera solución no funcionará en 8.3
a_horse_with_no_name