debe aparecer en la cláusula GROUP BY o usarse en una función agregada

276

Tengo una tabla que se parece a esta llamada 'makerar'

 cname  | wmname |          avg           
--------+-------------+------------------------
 canada | zoro   |     2.0000000000000000
 spain  | luffy  | 1.00000000000000000000
 spain  | usopp  |     5.0000000000000000

Y quiero seleccionar el promedio máximo para cada cname.

SELECT cname, wmname, MAX(avg)  FROM makerar GROUP BY cname;

pero tendré un error

ERROR:  column "makerar.wmname" must appear in the GROUP BY clause or be used in an   aggregate function 
LINE 1: SELECT cname, wmname, MAX(avg)  FROM makerar GROUP BY cname;

entonces hago esto

SELECT cname, wmname, MAX(avg)  FROM makerar GROUP BY cname, wmname;

sin embargo, esto no dará los resultados previstos, y se muestra la salida incorrecta a continuación.

 cname  | wmname |          max           
--------+--------+------------------------
 canada | zoro   |     2.0000000000000000
 spain  | luffy  | 1.00000000000000000000
 spain  | usopp  |     5.0000000000000000

Los resultados reales deben ser

 cname  | wmname |          max           
--------+--------+------------------------
 canada | zoro   |     2.0000000000000000
 spain  | usopp  |     5.0000000000000000

¿Cómo puedo solucionar este problema?

Nota: Esta tabla es una VISTA creada a partir de una operación anterior.

Chico al azar
fuente
2
Relacionado: stackoverflow.com/q/18061285/398670
Craig Ringer
No entiendo. ¿Por qué se wmname="usopp"espera y no por ejemplo wmname="luffy"?
AndreKR

Respuestas:

226

Sí, este es un problema de agregación común. Antes de SQL3 (1999) , los campos seleccionados deben aparecer en la GROUP BYcláusula [*].

Para solucionar este problema, debe calcular el agregado en una subconsulta y luego unirlo consigo mismo para obtener las columnas adicionales que debe mostrar:

SELECT m.cname, m.wmname, t.mx
FROM (
    SELECT cname, MAX(avg) AS mx
    FROM makerar
    GROUP BY cname
    ) t JOIN makerar m ON m.cname = t.cname AND t.mx = m.avg
;

 cname  | wmname |          mx           
--------+--------+------------------------
 canada | zoro   |     2.0000000000000000
 spain  | usopp  |     5.0000000000000000

Pero también puede usar funciones de ventana, que se ven más simples:

SELECT cname, wmname, MAX(avg) OVER (PARTITION BY cname) AS mx
FROM makerar
;

Lo único con este método es que mostrará todos los registros (las funciones de la ventana no se agrupan). Pero mostrará el correcto (es decir, máximo al cnamenivel) MAXpara el país en cada fila, por lo que depende de usted:

 cname  | wmname |          mx           
--------+--------+------------------------
 canada | zoro   |     2.0000000000000000
 spain  | luffy  |     5.0000000000000000
 spain  | usopp  |     5.0000000000000000

La solución, posiblemente menos elegante, para mostrar las únicas (cname, wmname)tuplas que coinciden con el valor máximo, es:

SELECT DISTINCT /* distinct here matters, because maybe there are various tuples for the same max value */
    m.cname, m.wmname, t.avg AS mx
FROM (
    SELECT cname, wmname, avg, ROW_NUMBER() OVER (PARTITION BY avg DESC) AS rn 
    FROM makerar
) t JOIN makerar m ON m.cname = t.cname AND m.wmname = t.wmname AND t.rn = 1
;


 cname  | wmname |          mx           
--------+--------+------------------------
 canada | zoro   |     2.0000000000000000
 spain  | usopp  |     5.0000000000000000

[*]: Curiosamente, a pesar de que el tipo de especificación permite seleccionar campos no agrupados, a los motores principales parece no gustarles realmente. Oracle y SQLServer simplemente no permiten esto en absoluto. Mysql solía permitirlo de forma predeterminada, pero ahora desde 5.7 el administrador debe habilitar esta opción ( ONLY_FULL_GROUP_BY) manualmente en la configuración del servidor para que esta característica sea compatible ...

Sebas
fuente
1
Gracias, la sintaxis es corect, pero debes comparar los valores de mx y avg al unirte
RandomGuy
1
Sí, su sintaxis es correcta y elimina duplicados, sin embargo, necesita m.avg = t.mx al final (después de escribir JOING) para obtener los resultados
deseados
1
@Sebas Se puede hacer sin unirse MAX(vea la respuesta de @ypercube, también hay otra solución en mi respuesta), pero no de la forma en que lo hace. Verifique la salida esperada.
zero323
1
@Sebas Su solución solo agrega una columna (la MAX avgpor cname) pero no restringe las filas del resultado (como lo desea el OP). Ver los resultados reales debe ser párrafo en la pregunta.
ypercubeᵀᴹ
1
Volviendo fuera ONLY_FULL_GROUP_BY en MySQL 5.7 no activa el modo en que SQL previsto por la norma cuando las columnas pueden ser omitidas de la group by(o hace que MySQL se comportan como Postgres). Simplemente vuelve al comportamiento anterior donde MySQL devuelve resultados aleatorios (= "indeterminados").
a_horse_with_no_name
126

En Postgres, también puedes usar la DISTINCT ON (expression)sintaxis especial :

SELECT DISTINCT ON (cname) 
    cname, wmname, avg
FROM 
    makerar 
ORDER BY 
    cname, avg DESC ;
ypercubeᵀᴹ
fuente
55
No funcionará como se espera si uno quiere ordenar columnas como promedio
amenzhinsky
@amenzhinsky ¿Qué quieres decir? Si se quiere ordenar el conjunto de resultados con un orden diferente al de BY cname?
ypercubeᵀᴹ
@ypercube, en realidad psql ordena primero y luego aplica DISTINCT. En caso de ordenar por promedio, obtendremos diferentes resultados para cada fila valores mínimos y máximos dependiendo de la dirección de clasificación
amenzhinsky
3
Por supuesto. Si no ejecuta la consulta que publiqué, obtendrá resultados diferentes. Eso no es lo mismo que "no funcionará como se esperaba" ...
ypercubeᵀᴹ
1
@Batfan thnx. Tenga en cuenta que si bien esto es bastante bueno, compacto y fácil de escribir, a menudo no es la forma más eficiente para este tipo de consultas.
ypercubeᵀᴹ
27

El problema con la especificación de campos no agrupados y no agregados en group byselecciones es que el motor no tiene forma de saber qué campo de registro debería devolver en este caso. ¿Es primero? ¿Es el último? Por lo general, no hay registros que correspondan naturalmente al resultado agregado ( miny maxson excepciones).

Sin embargo, existe una solución alternativa: agregue también el campo requerido. En posgres, esto debería funcionar:

SELECT cname, (array_agg(wmname ORDER BY avg DESC))[1], MAX(avg)
FROM makerar GROUP BY cname;

Tenga en cuenta que esto crea una matriz de todos los wnames, ordenados por avg, y devuelve el primer elemento (las matrices en postgres están basadas en 1).

e-neko
fuente
Buen punto. Aunque parece posible que la base de datos pueda hacer una unión externa para vincular los campos no agregados de cada fila al resultado agregado al que contribuyó la fila. A menudo he sentido curiosidad por qué no tienen una opción para eso. Aunque podría simplemente ignorar esta opción :)
Ben Simmons
16
SELECT t1.cname, t1.wmname, t2.max
FROM makerar t1 JOIN (
    SELECT cname, MAX(avg) max
    FROM makerar
    GROUP BY cname ) t2
ON t1.cname = t2.cname AND t1.avg = t2.max;

Usando la rank() función de ventana :

SELECT cname, wmname, avg
FROM (
    SELECT cname, wmname, avg, rank() 
    OVER (PARTITION BY cname ORDER BY avg DESC)
    FROM makerar) t
WHERE rank = 1;

Nota

Cualquiera de los dos conservará múltiples valores máximos por grupo. Si desea un solo registro por grupo, incluso si hay más de un registro con un promedio igual al máximo, debe verificar la respuesta de @ ypercube.

cero323
fuente
16

Para mí, no se trata de un "problema de agregación común", sino de una consulta SQL incorrecta. La única respuesta correcta para "seleccionar el promedio máximo para cada cname ..." es

SELECT cname, MAX(avg) FROM makerar GROUP BY cname;

El resultado será:

 cname  |      MAX(avg)
--------+---------------------
 canada | 2.0000000000000000
 spain  | 5.0000000000000000

Este resultado en general responde a la pregunta "¿Cuál es el mejor resultado para cada grupo?" . Vemos que el mejor resultado para España es 5 y para Canadá el mejor resultado es 2. Es cierto, y no hay error. Si también necesitamos mostrar wmname , debemos responder la pregunta: "¿Cuál es la REGLA para elegir wmname del conjunto resultante?" Cambiemos un poco los datos de entrada para aclarar el error:

  cname | wmname |        avg           
--------+--------+-----------------------
 spain  | zoro   |  1.0000000000000000
 spain  | luffy  |  5.0000000000000000
 spain  | usopp  |  5.0000000000000000

¿Qué resultado esperas al ejecutar esta consulta SELECT cname, wmname, MAX(avg) FROM makerar GROUP BY cname;? ¿Debería ser spain+luffyo spain+usopp? ¿Por qué? En la consulta no se determina cómo elegir "mejor" wmname si varios son adecuados, por lo que el resultado tampoco está determinado. Es por eso que el intérprete de SQL devuelve un error: la consulta no es correcta.

En otras palabras, no hay una respuesta correcta a la pregunta "¿Quién es el mejor del spaingrupo?" . Luffy no es mejor que usopp, porque usopp tiene el mismo "puntaje".

ox160d05d
fuente
Esta solución también funcionó para mí. Tuve problemas de consulta porque mi ORM también incluía la clave primaria asociada, lo que resultó en la siguiente consulta incorrecta : SELECT cname, id, MAX(avg) FROM makerar GROUP BY cname;que dio este error engañoso.
Roberto
1

Esto parece funcionar también

SELECT *
FROM makerar m1
WHERE m1.avg = (SELECT MAX(avg)
                FROM makerar m2
                WHERE m1.cname = m2.cname
               )
daintym0sh
fuente
0

Recientemente me encontré con este problema, al intentar contar usando case when, y descubrí que cambiar el orden de las declaraciones whichy countsoluciona el problema:

SELECT date(dateday) as pick_day,
COUNT(CASE WHEN (apples = 'TRUE' OR oranges 'TRUE') THEN fruit END)  AS fruit_counter

FROM pickings

GROUP BY 1

En lugar de usar, en este último, donde obtuve errores de que las manzanas y las naranjas deberían aparecer en funciones agregadas

CASE WHEN ((apples = 'TRUE' OR oranges 'TRUE') THEN COUNT(*) END) END AS fruit_counter
Rachel Windzberg
fuente
1
La whichdeclaración?
Hillary Sanders