¿Seleccionar primera fila en cada grupo GROUP BY?

1326

Como sugiere el título, me gustaría seleccionar la primera fila de cada conjunto de filas agrupadas con a GROUP BY.

Específicamente, si tengo una purchasestabla que se ve así:

SELECT * FROM purchases;

Mi salida:

id | cliente | total
--- + ---------- + ------
 1 | Joe | 5 5
 2 | Sally 3
 3 | Joe | 2
 4 | Sally 1

Me gustaría consultar idla mayor compra ( total) realizada por cada uno customer. Algo como esto:

SELECT FIRST(id), customer, FIRST(total)
FROM  purchases
GROUP BY customer
ORDER BY total DESC;

Rendimiento esperado:

PRIMERO (id) | cliente | PRIMERO (total)
---------- + ---------- + -------------
        1 | Joe | 5 5
        2 | Sally 3
David Wolever
fuente
Dado que solo está buscando cada uno más grande, ¿por qué no consultar MAX(total)?
phil294
44
@ phil294 que consulta max (total) no asociará ese total con el valor 'id' de la fila en la que ocurrió.
gwideman

Respuestas:

1117

En Oracle 9.2+ (no 8i + como se indicó originalmente), SQL Server 2005+, PostgreSQL 8.4+, DB2, Firebird 3.0+, Teradata, Sybase, Vertica:

WITH summary AS (
    SELECT p.id, 
           p.customer, 
           p.total, 
           ROW_NUMBER() OVER(PARTITION BY p.customer 
                                 ORDER BY p.total DESC) AS rk
      FROM PURCHASES p)
SELECT s.*
  FROM summary s
 WHERE s.rk = 1

Soportado por cualquier base de datos:

Pero necesitas agregar lógica para romper los lazos:

  SELECT MIN(x.id),  -- change to MAX if you want the highest
         x.customer, 
         x.total
    FROM PURCHASES x
    JOIN (SELECT p.customer,
                 MAX(total) AS max_total
            FROM PURCHASES p
        GROUP BY p.customer) y ON y.customer = x.customer
                              AND y.max_total = x.total
GROUP BY x.customer, x.total
Ponis OMG
fuente
2
Informix 12.x también admite funciones de ventana (aunque el CTE debe convertirse en una tabla derivada). Y Firebird 3.0 también admitirá las funciones de Windows
a_horse_with_no_name el
37
ROW_NUMBER() OVER(PARTITION BY [...])junto con algunas otras optimizaciones me ayudaron a reducir una consulta de 30 segundos a unos pocos milisegundos. ¡Gracias! (PostgreSQL 9.2)
Sam
8
Si hay varias compras con el mismo valor más alto totalpara un cliente, la primera consulta devuelve un ganador arbitrario (según los detalles de las implementaciones; ¡ idpuede cambiar para cada ejecución!). Normalmente (no siempre) desearía una fila por cliente, definida por criterios adicionales como "el que tiene el más pequeño id". Para solucionarlo, agregue ida la ORDER BYlista de row_number(). Luego obtienes el mismo resultado que con la segunda consulta, que es muy ineficiente para este caso. Además, necesitaría otra subconsulta para cada columna adicional.
Erwin Brandstetter
2
BigQuery de Google también admite el comando ROW_NUMBER () de la primera consulta. Funcionó como un encanto para nosotros
Praxiteles
2
Tenga en cuenta que la primera versión con la función de ventana funciona a partir de SQLite versión 3.25.0: sqlite.org/windowfunctions.html#history
brianz
1150

En PostgreSQL, esto suele ser más simple y rápido (más optimización de rendimiento a continuación):

SELECT DISTINCT ON (customer)
       id, customer, total
FROM   purchases
ORDER  BY customer, total DESC, id;

O más corto (si no es tan claro) con números ordinales de columnas de salida:

SELECT DISTINCT ON (2)
       id, customer, total
FROM   purchases
ORDER  BY 2, 3 DESC, 1;

Si totalpuede ser NULL (no hará daño de ninguna manera, pero querrá hacer coincidir los índices existentes ):

...
ORDER  BY customer, total DESC NULLS LAST, id;

Puntos principales

  • DISTINCT ONes una extensión PostgreSQL del estándar (donde solo se define DISTINCTen toda la SELECTlista).

  • Enumere cualquier número de expresiones en la DISTINCT ONcláusula, el valor de fila combinado define duplicados. El manual:

    Obviamente, dos filas se consideran distintas si difieren en al menos un valor de columna. Los valores nulos se consideran iguales en esta comparación.

    El énfasis en negrita es mío.

  • DISTINCT ONse puede combinar con ORDER BY. Las expresiones iniciales en ORDER BYdeben estar en el conjunto de expresiones en DISTINCT ON, pero puede reorganizar el orden libremente. Ejemplo. Puede agregar expresiones adicionalesORDER BY para elegir una fila particular de cada grupo de pares. O, como dice el manual :

    Las DISTINCT ONexpresiones deben coincidir con las ORDER BY expresiones más a la izquierda . La ORDER BYcláusula normalmente contendrá expresiones adicionales que determinan la precedencia deseada de las filas dentro de cada DISTINCT ONgrupo.

    Agregué idcomo último elemento para romper los lazos:
    "Elija la fila con el más pequeño idde cada grupo que comparta el más alto total".

    Para ordenar los resultados de una manera que no está de acuerdo con el orden de clasificación que determina el primero por grupo, puede anidar la consulta anterior en una consulta externa con otra ORDER BY. Ejemplo.

  • Si totalpuede ser NULL, lo más probable es que desee la fila con el mayor valor no nulo. Añadir NULLS LASTcomo demostrado. Ver:

  • La SELECTlista no está limitado por las expresiones en DISTINCT ONo ORDER BYen cualquier forma. (No es necesario en el caso simple anterior):

    • No tiene que incluir ninguna de las expresiones en DISTINCT ONo ORDER BY.

    • Usted puede incluir cualquier otra expresión en la SELECTlista. Esto es instrumental para reemplazar consultas mucho más complejas con subconsultas y funciones de agregado / ventana.

  • Probé con Postgres versiones 8.3 - 12. Pero la característica ha estado allí al menos desde la versión 7.1, así que básicamente siempre.

Índice

El índice perfecto para la consulta anterior sería un índice de varias columnas que abarque las tres columnas en secuencia coincidente y con un orden de clasificación coincidente:

CREATE INDEX purchases_3c_idx ON purchases (customer, total DESC, id);

Puede ser muy especializado. Pero úselo si el rendimiento de lectura para la consulta particular es crucial. Si tiene DESC NULLS LASTen la consulta, use la misma en el índice para que el orden de clasificación coincida y el índice sea aplicable.

Efectividad / Optimización del rendimiento

Considere el costo y el beneficio antes de crear índices personalizados para cada consulta. El potencial del índice anterior depende en gran medida de la distribución de datos .

El índice se usa porque entrega datos previamente ordenados. En Postgres 9.2 o posterior, la consulta también puede beneficiarse de una exploración de índice solo si el índice es más pequeño que la tabla subyacente. Sin embargo, el índice debe ser escaneado en su totalidad.

Punto de referencia

Tenía un punto de referencia simple aquí que ya no está actualizado. Lo reemplacé con un punto de referencia detallado en esta respuesta por separado .

Erwin Brandstetter
fuente
28
Esta es una gran respuesta para la mayoría de los tamaños de bases de datos, pero quiero señalar que a medida que se acerca ~ millones de filas se DISTINCT ONvuelve extremadamente lento. La implementación siempre ordena toda la tabla y la explora en busca de duplicados, ignorando todos los índices (incluso si ha creado el índice de varias columnas requerido). Consulte explicatextended.com/2009/05/03/postgresql-optimizing-distinct para una posible solución.
Meekohi
14
Usar ordinales para "acortar el código" es una idea terrible. ¿Qué tal dejar los nombres de las columnas para que sean legibles?
KOTJMF
13
@KOTJMF: le sugiero que vaya con su preferencia personal entonces. Demuestro ambas opciones para educar. La abreviatura de sintaxis puede ser útil para expresiones largas en la SELECTlista.
Erwin Brandstetter
1
@jangorecki: El punto de referencia original es de 2011, ya no tengo la configuración. Pero ya era hora de ejecutar pruebas con la página 9.4 y la página 9.5 de todos modos. Ver detalles en la respuesta agregada. . ¿Podría agregar un comentario con el resultado de su instalación a continuación?
Erwin Brandstetter
2
@PirateApp: No desde lo alto de mi cabeza. DISTINCT ONsolo es bueno para obtener una fila por grupo de pares.
Erwin Brandstetter
134

Punto de referencia

Prueba de los candidatos más interesantes con PostgreSQL 9.4 y 9.5 con una mesa a mitad de camino realista de 200k filas en purchasesy 10k distintacustomer_id ( avg. 20 filas por cliente ).

Para Postgres 9.5 realicé una segunda prueba con efectivamente 86446 clientes distintos. Consulte a continuación ( promedio de 2.3 filas por cliente ).

Preparar

Mesa principal

CREATE TABLE purchases (
  id          serial
, customer_id int  -- REFERENCES customer
, total       int  -- could be amount of money in Cent
, some_column text -- to make the row bigger, more realistic
);

Uso una serial(restricción PK añadida a continuación) y un número entero, customer_idya que es una configuración más típica. También se agrega some_columnpara compensar típicamente más columnas.

Datos ficticios, PK, índice: una tabla típica también tiene algunas tuplas muertas:

INSERT INTO purchases (customer_id, total, some_column)    -- insert 200k rows
SELECT (random() * 10000)::int             AS customer_id  -- 10k customers
     , (random() * random() * 100000)::int AS total     
     , 'note: ' || repeat('x', (random()^2 * random() * random() * 500)::int)
FROM   generate_series(1,200000) g;

ALTER TABLE purchases ADD CONSTRAINT purchases_id_pkey PRIMARY KEY (id);

DELETE FROM purchases WHERE random() > 0.9; -- some dead rows

INSERT INTO purchases (customer_id, total, some_column)
SELECT (random() * 10000)::int             AS customer_id  -- 10k customers
     , (random() * random() * 100000)::int AS total     
     , 'note: ' || repeat('x', (random()^2 * random() * random() * 500)::int)
FROM   generate_series(1,20000) g;  -- add 20k to make it ~ 200k

CREATE INDEX purchases_3c_idx ON purchases (customer_id, total DESC, id);

VACUUM ANALYZE purchases;

customer tabla - para consulta superior

CREATE TABLE customer AS
SELECT customer_id, 'customer_' || customer_id AS customer
FROM   purchases
GROUP  BY 1
ORDER  BY 1;

ALTER TABLE customer ADD CONSTRAINT customer_customer_id_pkey PRIMARY KEY (customer_id);

VACUUM ANALYZE customer;

En mi segunda prueba para 9.5 utilicé la misma configuración, pero random() * 100000para generar customer_idpara obtener solo unas pocas filas por customer_id.

Tamaños de objetos para mesa purchases

Generado con esta consulta .

               what                | bytes/ct | bytes_pretty | bytes_per_row
-----------------------------------+----------+--------------+---------------
 core_relation_size                | 20496384 | 20 MB        |           102
 visibility_map                    |        0 | 0 bytes      |             0
 free_space_map                    |    24576 | 24 kB        |             0
 table_size_incl_toast             | 20529152 | 20 MB        |           102
 indexes_size                      | 10977280 | 10 MB        |            54
 total_size_incl_toast_and_indexes | 31506432 | 30 MB        |           157
 live_rows_in_text_representation  | 13729802 | 13 MB        |            68
 ------------------------------    |          |              |
 row_count                         |   200045 |              |
 live_tuples                       |   200045 |              |
 dead_tuples                       |    19955 |              |

Consultas

1. row_number()en CTE, ( ver otra respuesta )

WITH cte AS (
   SELECT id, customer_id, total
        , row_number() OVER(PARTITION BY customer_id ORDER BY total DESC) AS rn
   FROM   purchases
   )
SELECT id, customer_id, total
FROM   cte
WHERE  rn = 1;

2. row_number()en subconsulta (mi optimización)

SELECT id, customer_id, total
FROM   (
   SELECT id, customer_id, total
        , row_number() OVER(PARTITION BY customer_id ORDER BY total DESC) AS rn
   FROM   purchases
   ) sub
WHERE  rn = 1;

3. DISTINCT ON( ver otra respuesta )

SELECT DISTINCT ON (customer_id)
       id, customer_id, total
FROM   purchases
ORDER  BY customer_id, total DESC, id;

4. rCTE con LATERALsubconsulta ( ver aquí )

WITH RECURSIVE cte AS (
   (  -- parentheses required
   SELECT id, customer_id, total
   FROM   purchases
   ORDER  BY customer_id, total DESC
   LIMIT  1
   )
   UNION ALL
   SELECT u.*
   FROM   cte c
   ,      LATERAL (
      SELECT id, customer_id, total
      FROM   purchases
      WHERE  customer_id > c.customer_id  -- lateral reference
      ORDER  BY customer_id, total DESC
      LIMIT  1
      ) u
   )
SELECT id, customer_id, total
FROM   cte
ORDER  BY customer_id;

5. customermesa con LATERAL( ver aquí )

SELECT l.*
FROM   customer c
,      LATERAL (
   SELECT id, customer_id, total
   FROM   purchases
   WHERE  customer_id = c.customer_id  -- lateral reference
   ORDER  BY total DESC
   LIMIT  1
   ) l;

6. array_agg()con ORDER BY( ver otra respuesta )

SELECT (array_agg(id ORDER BY total DESC))[1] AS id
     , customer_id
     , max(total) AS total
FROM   purchases
GROUP  BY customer_id;

Resultados

Tiempo de ejecución para las consultas anteriores con EXPLAIN ANALYZE(y todas las opciones desactivadas ), lo mejor de 5 ejecuciones .

Todas las consultas utilizaron un Escaneo de solo índice en purchases2_3c_idx(entre otros pasos). Algunos de ellos solo por el tamaño más pequeño del índice, otros más efectivamente.

A. Postgres 9.4 con 200k filas y ~ 20 por customer_id

1. 273.274 ms  
2. 194.572 ms  
3. 111.067 ms  
4.  92.922 ms  
5.  37.679 ms  -- winner
6. 189.495 ms

B. Lo mismo con Postgres 9.5

1. 288.006 ms
2. 223.032 ms  
3. 107.074 ms  
4.  78.032 ms  
5.  33.944 ms  -- winner
6. 211.540 ms  

C. Igual que B., pero con ~ 2.3 filas por customer_id

1. 381.573 ms
2. 311.976 ms
3. 124.074 ms  -- winner
4. 710.631 ms
5. 311.976 ms
6. 421.679 ms

Puntos de referencia relacionados

Aquí hay una nueva prueba "ogr" con 10 millones de filas y 60 mil "clientes" únicos en Postgres 11.5 (actual a partir de septiembre de 2019). Los resultados todavía están en línea con lo que hemos visto hasta ahora:

Punto de referencia original (obsoleto) de 2011

Ejecuté tres pruebas con PostgreSQL 9.1 en una tabla de la vida real de 65579 filas e índices btree de una sola columna en cada una de las tres columnas involucradas y tomé el mejor tiempo de ejecución de 5 ejecuciones.
Comparando la primera consulta de @OMGPonies ( A) con la solución anteriorDISTINCT ON ( B):

  1. Seleccione toda la tabla, resultados en 5958 filas en este caso.

    A: 567.218 ms
    B: 386.673 ms
  2. Condición de uso que WHERE customer BETWEEN x AND yresulta en 1000 filas.

    A: 249.136 ms
    B:  55.111 ms
  3. Seleccione un solo cliente con WHERE customer = x.

    A:   0.143 ms
    B:   0.072 ms

La misma prueba se repite con el índice descrito en la otra respuesta

CREATE INDEX purchases_3c_idx ON purchases (customer, total DESC, id);

1A: 277.953 ms  
1B: 193.547 ms

2A: 249.796 ms -- special index not used  
2B:  28.679 ms

3A:   0.120 ms  
3B:   0.048 ms
Erwin Brandstetter
fuente
55
Gracias por un gran punto de referencia. Me preguntaba si consultar datos de eventos donde tiene una marca de tiempo en lugar del total se beneficiaría del nuevo índice BRIN. Potencialmente, esto puede acelerar las consultas temporales.
Jangorecki
3
@jangorecki: cualquier tabla enorme con datos ordenados físicamente puede beneficiarse de un índice BRIN.
Erwin Brandstetter
@ErwinBrandstetter En los ejemplos 2. row_number()y 5. customer table with LATERAL, ¿qué asegura que la identificación sea la más pequeña?
Artem Novikov el
@ArtemNovikov: Nada. El objetivo es recuperar, por customer_id la fila con el más alto total. Es una coincidencia engañosa en los datos de prueba de la pregunta que iden las filas seleccionadas también es el más pequeño por customer_id.
Erwin Brandstetter
1
@ArtemNovikov: para permitir escaneos de solo índice.
Erwin Brandstetter
55

Esto es comun problema, que ya tiene soluciones bien probadas y altamente optimizadas . Personalmente, prefiero la solución de unión izquierda de Bill Karwin (la publicación original con muchas otras soluciones ).

¡Tenga en cuenta que sorprendentemente se pueden encontrar muchas soluciones a este problema común en una de las fuentes más oficiales, el manual de MySQL ! Vea ejemplos de consultas comunes :: Las filas que sostienen el máximo grupal de una determinada columna .

TMS
fuente
22
¿Cómo es el manual MySQL de alguna manera "oficial" para las preguntas de Postgres / SQLite (sin mencionar SQL)? Además, para ser claros, la DISTINCT ONversión es mucho más corta, más simple y generalmente funciona mejor en Postgres que las alternativas con auto LEFT JOINo semi-anti-unión NOT EXISTS. También está "bien probado".
Erwin Brandstetter
3
Además de lo que escribió Erwin, diría que usar una función de ventana (que es la funcionalidad común de SQL hoy en día) es casi siempre más rápido que usar una combinación con una tabla derivada
a_horse_with_no_name
66
Grandes referencias No sabía que esto se llamaba el mayor problema de n por grupo. Gracias.
David Mann
La pregunta no se refiere a la mayor n por grupo, sino a la primera n.
reinierpost
1
En un caso de dos campos de orden que probé, "la solución de combinación izquierda de Bill Karwin" dio un bajo rendimiento. Vea mi comentario a continuación stackoverflow.com/a/8749095/684229
Johnny Wong el
30

En Postgres puedes usar array_aggasí:

SELECT  customer,
        (array_agg(id ORDER BY total DESC))[1],
        max(total)
FROM purchases
GROUP BY customer

Esto le dará idla mayor compra de cada cliente.

Algunas cosas a tener en cuenta:

  • array_agges una función agregada, por lo que funciona con GROUP BY.
  • array_aggle permite especificar un ámbito de orden solo para sí mismo, por lo que no limita la estructura de toda la consulta. También hay una sintaxis sobre cómo ordenar los NULL, si necesita hacer algo diferente de lo predeterminado.
  • Una vez que construimos la matriz, tomamos el primer elemento. (Las matrices de Postgres están indexadas en 1, no indexadas en 0).
  • Podría usarlo array_aggde manera similar para su tercera columna de salida, pero max(total)es más simple.
  • A diferencia DISTINCT ON, el uso le array_aggpermite conservar su GROUP BY, en caso de que lo desee por otros motivos.
Paul A Jungwirth
fuente
14

La solución no es muy eficiente como señala Erwin, debido a la presencia de SubQ

select * from purchases p1 where total in
(select max(total) from purchases where p1.customer=customer) order by total desc;
usuario2407394
fuente
Gracias, sí, de acuerdo con usted, la unión entre subq y consulta externa en realidad lleva más tiempo. "In" no será un problema aquí ya que el subq dará como resultado solo una fila. Por cierto, ¿a qué error de sintaxis estás apuntando?
user2407394
ohh .. utilizado para "Teradata" .. editado now..however romper los lazos no se requiere aquí, ya que la necesidad de encontrar total más alto para cada cliente ..
user2407394
¿Sabe que obtiene varias filas para un solo cliente en caso de empate? Si eso es deseable depende de los requisitos exactos. Normalmente no lo es. Para la pregunta en cuestión, el título es bastante claro.
Erwin Brandstetter
Esto no está claro en la pregunta, si el mismo cliente tiene una compra = Máx. Para 2 identificadores diferentes, creo que deberíamos mostrar ambos.
user2407394
10

Lo uso de esta manera (solo postgresql): https://wiki.postgresql.org/wiki/First/last_%28aggregate%29

-- Create a function that always returns the first non-NULL item
CREATE OR REPLACE FUNCTION public.first_agg ( anyelement, anyelement )
RETURNS anyelement LANGUAGE sql IMMUTABLE STRICT AS $$
        SELECT $1;
$$;

-- And then wrap an aggregate around it
CREATE AGGREGATE public.first (
        sfunc    = public.first_agg,
        basetype = anyelement,
        stype    = anyelement
);

-- Create a function that always returns the last non-NULL item
CREATE OR REPLACE FUNCTION public.last_agg ( anyelement, anyelement )
RETURNS anyelement LANGUAGE sql IMMUTABLE STRICT AS $$
        SELECT $2;
$$;

-- And then wrap an aggregate around it
CREATE AGGREGATE public.last (
        sfunc    = public.last_agg,
        basetype = anyelement,
        stype    = anyelement
);

Entonces su ejemplo debería funcionar casi como está:

SELECT FIRST(id), customer, FIRST(total)
FROM  purchases
GROUP BY customer
ORDER BY FIRST(total) DESC;

CUEVA: ignora las filas NULL


Edición 1 - Use la extensión postgres en su lugar

Ahora lo uso de esta manera: http://pgxn.org/dist/first_last_agg/

Para instalar en ubuntu 14.04:

apt-get install postgresql-server-dev-9.3 git build-essential -y
git clone git://github.com/wulczer/first_last_agg.git
cd first_last_app
make && sudo make install
psql -c 'create extension first_last_agg'

Es una extensión de postgres que te brinda las primeras y últimas funciones; aparentemente más rápido que el anterior.


Edición 2 - Ordenar y filtrar

Si usa funciones agregadas (como estas), puede ordenar los resultados, sin la necesidad de tener los datos ya ordenados:

http://www.postgresql.org/docs/current/static/sql-expressions.html#SYNTAX-AGGREGATES

Entonces, el ejemplo equivalente, con ordenar sería algo como:

SELECT first(id order by id), customer, first(total order by id)
  FROM purchases
 GROUP BY customer
 ORDER BY first(total);

Por supuesto, puede ordenar y filtrar según lo considere adecuado dentro del agregado; Es una sintaxis muy poderosa.

matiu
fuente
Usando este enfoque de función personalizada también. Suficientemente universal y simple. ¿Por qué complicar las cosas? ¿Es esta solución significativamente menos eficaz que otras?
Sergey Shcherbakov
9

La consulta:

SELECT purchases.*
FROM purchases
LEFT JOIN purchases as p 
ON 
  p.customer = purchases.customer 
  AND 
  purchases.total < p.total
WHERE p.total IS NULL

¡CÓMO FUNCIONA! (He estado allí)

Queremos asegurarnos de que solo tenemos el total más alto para cada compra.


Algunas cosas teóricas (omita esta parte si solo quiere entender la consulta)

Deje que Total sea una función T (cliente, id) donde devuelve un valor dado el nombre y el id. Para demostrar que el total dado (T (cliente, id)) es el más alto, tenemos que demostrar que queremos demostrar

  • ∀x T (cliente, id)> T (cliente, x) (este total es más alto que todos los demás totales para ese cliente)

O

  • ¬∃x T (cliente, id) <T (cliente, x) (no existe un total más alto para ese cliente)

El primer enfoque necesitará que obtengamos todos los registros para ese nombre que realmente no me gusta.

El segundo necesitará una forma inteligente de decir que no puede haber un registro más alto que este.


Regresar a SQL

Si nos fuimos, la tabla se une con el nombre y el total es menor que la tabla unida:

      LEFT JOIN purchases as p 
      ON 
      p.customer = purchases.customer 
      AND 
      purchases.total < p.total

nos aseguramos de que todos los registros que tienen otro registro con el total más alto para el mismo usuario se unan:

purchases.id, purchases.customer, purchases.total, p.id, p.customer, p.total
1           , Tom           , 200             , 2   , Tom   , 300
2           , Tom           , 300
3           , Bob           , 400             , 4   , Bob   , 500
4           , Bob           , 500
5           , Alice         , 600             , 6   , Alice   , 700
6           , Alice         , 700

Eso nos ayudará a filtrar el total más alto para cada compra sin necesidad de agrupación:

WHERE p.total IS NULL

purchases.id, purchases.name, purchases.total, p.id, p.name, p.total
2           , Tom           , 300
4           , Bob           , 500
6           , Alice         , 700

Y esa es la respuesta que necesitamos.

khaled_gomaa
fuente
8

Solución muy rápida

SELECT a.* 
FROM
    purchases a 
    JOIN ( 
        SELECT customer, min( id ) as id 
        FROM purchases 
        GROUP BY customer 
    ) b USING ( id );

y realmente muy rápido si la tabla está indexada por id:

create index purchases_id on purchases (id);
Alejandro Salamanca Mazuelo
fuente
La cláusula USING es muy estándar. Es solo que algunos sistemas de bases de datos menores no lo tienen.
Holger Jakobs
2
Esto no encuentra la compra de los clientes con mayor total
Johnny Wong
7

En SQL Server puedes hacer esto:

SELECT *
FROM (
SELECT ROW_NUMBER()
OVER(PARTITION BY customer
ORDER BY total DESC) AS StRank, *
FROM Purchases) n
WHERE StRank = 1

Explicación: Aquí Agrupar por se realiza en base al cliente y luego ordenarlo por total, luego cada grupo recibe el número de serie como StRank y estamos sacando primero 1 cliente cuyo StRank es 1

Diwas Poudel
fuente
¡Gracias! Esto funcionó perfectamente y fue muy fácil de entender e implementar.
ruohola 01 de
4

En PostgreSQL, otra posibilidad es usar la first_valuefunción de ventana en combinación con SELECT DISTINCT:

select distinct customer_id,
                first_value(row(id, total)) over(partition by customer_id order by total desc, id)
from            purchases;

Creé un compuesto (id, total), por lo que ambos valores son devueltos por el mismo agregado. Por supuesto, siempre puede solicitar first_value()dos veces.

pbillen
fuente
3

La solución aceptada de OMG Ponies "Compatible con cualquier base de datos" tiene una buena velocidad de mi prueba.

Aquí proporciono un mismo enfoque, pero una solución más completa y limpia para cualquier base de datos. Se consideran los empates (suponga que desea obtener solo una fila para cada cliente, incluso múltiples registros para el total máximo por cliente), y se seleccionarán otros campos de compra (por ejemplo, adquirir_pago_id) para las filas coincidentes reales en la tabla de compras.

Soportado por cualquier base de datos:

select * from purchase
join (
    select min(id) as id from purchase
    join (
        select customer, max(total) as total from purchase
        group by customer
    ) t1 using (customer, total)
    group by customer
) t2 using (id)
order by customer

Esta consulta es razonablemente rápida, especialmente cuando hay un índice compuesto como (cliente, total) en la tabla de compras.

Observación:

  1. t1, t2 son alias de subconsulta que podrían eliminarse según la base de datos.

  2. Advertencia : la using (...)cláusula actualmente no es compatible con MS-SQL y Oracle db a partir de esta edición en enero de 2017. Debe expandirlo usted mismo, por ejemplo, on t2.id = purchase.idetc. La sintaxis USING funciona en SQLite, MySQL y PostgreSQL.

Johnny Wong
fuente
2

Snowflake / Teradata admite una QUALIFYcláusula que funciona como HAVINGpara funciones con ventana:

SELECT id, customer, total
FROM PURCHASES
QUALIFY ROW_NUMBER() OVER(PARTITION BY p.customer ORDER BY p.total DESC) = 1
Lukasz Szozda
fuente
1
  • Si desea seleccionar cualquier fila (por alguna condición específica) del conjunto de filas agregadas.

  • Si desea utilizar otra sum/avgfunción de agregación ( ) además de max/min. Por lo tanto, no puede usar la pista conDISTINCT ON

Puede usar la siguiente subconsulta:

SELECT  
    (  
       SELECT **id** FROM t2   
       WHERE id = ANY ( ARRAY_AGG( tf.id ) ) AND amount = MAX( tf.amount )   
    ) id,  
    name,   
    MAX(amount) ma,  
    SUM( ratio )  
FROM t2  tf  
GROUP BY name

Usted puede reemplazar amount = MAX( tf.amount ) con cualquier condición que desee con una restricción: esta subconsulta no debe devolver más de una fila

Pero si quieres hacer esas cosas, probablemente estés buscando funciones de ventana

Eugen Konkov
fuente
1

Para SQl Server, la forma más eficiente es:

with
ids as ( --condition for split table into groups
    select i from (values (9),(12),(17),(18),(19),(20),(22),(21),(23),(10)) as v(i) 
) 
,src as ( 
    select * from yourTable where  <condition> --use this as filter for other conditions
)
,joined as (
    select tops.* from ids 
    cross apply --it`s like for each rows
    (
        select top(1) * 
        from src
        where CommodityId = ids.i 
    ) as tops
)
select * from joined

y no olvides crear un índice agrupado para columnas usadas

BazSTR
fuente