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 purchases
tabla 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 id
la 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
sql
sqlite
postgresql
group-by
greatest-n-per-group
David Wolever
fuente
fuente
MAX(total)
?Respuestas:
En Oracle 9.2+ (no 8i + como se indicó originalmente), SQL Server 2005+, PostgreSQL 8.4+, DB2, Firebird 3.0+, Teradata, Sybase, Vertica:
Soportado por cualquier base de datos:
Pero necesitas agregar lógica para romper los lazos:
fuente
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)total
para un cliente, la primera consulta devuelve un ganador arbitrario (según los detalles de las implementaciones; ¡id
puede 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ñoid
". Para solucionarlo, agregueid
a laORDER BY
lista derow_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.En PostgreSQL, esto suele ser más simple y rápido (más optimización de rendimiento a continuación):
O más corto (si no es tan claro) con números ordinales de columnas de salida:
Si
total
puede ser NULL (no hará daño de ninguna manera, pero querrá hacer coincidir los índices existentes ):Puntos principales
DISTINCT ON
es una extensión PostgreSQL del estándar (donde solo se defineDISTINCT
en toda laSELECT
lista).Enumere cualquier número de expresiones en la
DISTINCT ON
cláusula, el valor de fila combinado define duplicados. El manual:El énfasis en negrita es mío.
DISTINCT ON
se puede combinar conORDER BY
. Las expresiones iniciales enORDER BY
deben estar en el conjunto de expresiones enDISTINCT 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 :Agregué
id
como último elemento para romper los lazos:"Elija la fila con el más pequeño
id
de cada grupo que comparta el más altototal
".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
total
puede ser NULL, lo más probable es que desee la fila con el mayor valor no nulo. AñadirNULLS LAST
como demostrado. Ver:La
SELECT
lista no está limitado por las expresiones enDISTINCT ON
oORDER BY
en cualquier forma. (No es necesario en el caso simple anterior):No tiene que incluir ninguna de las expresiones en
DISTINCT ON
oORDER BY
.Usted puede incluir cualquier otra expresión en la
SELECT
lista. 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:
Puede ser muy especializado. Pero úselo si el rendimiento de lectura para la consulta particular es crucial. Si tiene
DESC NULLS LAST
en 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.
Para pocas filas por cliente (alta cardinalidad en la columna
customer
), esto es muy eficiente. Más aún si necesita salida ordenada de todos modos. El beneficio se reduce con un número creciente de filas por cliente.Idealmente, tiene suficiente
work_mem
para procesar el paso de clasificación involucrado en la RAM y no derramarlo en el disco. Pero, en general, establecerwork_mem
demasiado alto puede tener efectos adversos. ConsidereSET LOCAL
para consultas excepcionalmente grandes. Encuentra cuánto necesitas conEXPLAIN ANALYZE
. La mención de " Disco: " en el paso de clasificación indica la necesidad de más:Para muchas filas por cliente (baja cardinalidad en la columna
customer
), un escaneo de índice suelto (también conocido como "escaneo de omisión") sería (mucho) más eficiente, pero eso no está implementado hasta Postgres 12. (Una implementación para escaneos de solo índice está en desarrollo para Postgres 13. Ver aquí y aquí .)Por ahora, hay técnicas de consulta más rápidas para sustituir esto. En particular, si tiene una mesa separada con clientes únicos, que es el caso de uso típico. Pero también si no lo haces:
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 .
fuente
DISTINCT ON
vuelve 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.SELECT
lista.DISTINCT ON
solo es bueno para obtener una fila por grupo de pares.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
purchases
y 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
Uso una
serial
(restricción PK añadida a continuación) y un número entero,customer_id
ya que es una configuración más típica. También se agregasome_column
para compensar típicamente más columnas.Datos ficticios, PK, índice: una tabla típica también tiene algunas tuplas muertas:
customer
tabla - para consulta superiorEn mi segunda prueba para 9.5 utilicé la misma configuración, pero
random() * 100000
para generarcustomer_id
para obtener solo unas pocas filas porcustomer_id
.Tamaños de objetos para mesa
purchases
Generado con esta consulta .
Consultas
1.
row_number()
en CTE, ( ver otra respuesta )2.
row_number()
en subconsulta (mi optimización)3.
DISTINCT ON
( ver otra respuesta )4. rCTE con
LATERAL
subconsulta ( ver aquí )5.
customer
mesa conLATERAL
( ver aquí )6.
array_agg()
conORDER BY
( ver otra respuesta )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
B. Lo mismo con Postgres 9.5
C. Igual que B., pero con ~ 2.3 filas por
customer_id
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
):Seleccione toda la tabla, resultados en 5958 filas en este caso.
Condición de uso que
WHERE customer BETWEEN x AND y
resulta en 1000 filas.Seleccione un solo cliente con
WHERE customer = x
.La misma prueba se repite con el índice descrito en la otra respuesta
fuente
2. row_number()
y5. customer table with LATERAL
, ¿qué asegura que la identificación sea la más pequeña?customer_id
la fila con el más altototal
. Es una coincidencia engañosa en los datos de prueba de la pregunta queid
en las filas seleccionadas también es el más pequeño porcustomer_id
.Esto es comun mayor-n-por-grupoproblema, 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 .
fuente
DISTINCT ON
versión es mucho más corta, más simple y generalmente funciona mejor en Postgres que las alternativas con autoLEFT JOIN
o semi-anti-uniónNOT EXISTS
. También está "bien probado".En Postgres puedes usar
array_agg
así:Esto le dará
id
la mayor compra de cada cliente.Algunas cosas a tener en cuenta:
array_agg
es una función agregada, por lo que funciona conGROUP BY
.array_agg
le 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.array_agg
de manera similar para su tercera columna de salida, peromax(total)
es más simple.DISTINCT ON
, el uso learray_agg
permite conservar suGROUP BY
, en caso de que lo desee por otros motivos.fuente
La solución no es muy eficiente como señala Erwin, debido a la presencia de SubQ
fuente
Lo uso de esta manera (solo postgresql): https://wiki.postgresql.org/wiki/First/last_%28aggregate%29
Entonces su ejemplo debería funcionar casi como está:
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:
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:
Entonces, el ejemplo equivalente, con ordenar sería algo como:
Por supuesto, puede ordenar y filtrar según lo considere adecuado dentro del agregado; Es una sintaxis muy poderosa.
fuente
La consulta:
¡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
O
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:
nos aseguramos de que todos los registros que tienen otro registro con el total más alto para el mismo usuario se unan:
Eso nos ayudará a filtrar el total más alto para cada compra sin necesidad de agrupación:
Y esa es la respuesta que necesitamos.
fuente
Solución muy rápida
y realmente muy rápido si la tabla está indexada por id:
fuente
En SQL Server puedes hacer esto:
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
fuente
Utilice la
ARRAY_AGG
función para PostgreSQL , U-SQL , IBM DB2 y Google BigQuery SQL :fuente
En PostgreSQL, otra posibilidad es usar la
first_value
función de ventana en combinación conSELECT DISTINCT
:Creé un compuesto
(id, total)
, por lo que ambos valores son devueltos por el mismo agregado. Por supuesto, siempre puede solicitarfirst_value()
dos veces.fuente
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:
Esta consulta es razonablemente rápida, especialmente cuando hay un índice compuesto como (cliente, total) en la tabla de compras.
Observación:
t1, t2 son alias de subconsulta que podrían eliminarse según la base de datos.
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.id
etc. La sintaxis USING funciona en SQLite, MySQL y PostgreSQL.fuente
Snowflake / Teradata admite una
QUALIFY
cláusula que funciona comoHAVING
para funciones con ventana:fuente
Si desea seleccionar cualquier fila (por alguna condición específica) del conjunto de filas agregadas.
Si desea utilizar otra
sum/avg
función de agregación ( ) además demax/min
. Por lo tanto, no puede usar la pista conDISTINCT ON
Puede usar la siguiente subconsulta:
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 filaPero si quieres hacer esas cosas, probablemente estés buscando funciones de ventana
fuente
Para SQl Server, la forma más eficiente es:
y no olvides crear un índice agrupado para columnas usadas
fuente