deve aparecer na cláusula GROUP BY ou ser usado em uma função agregada

276

Eu tenho uma tabela que se parece com esse chamador 'makerar'

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

E eu quero selecionar a média máxima para cada cname.

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

mas vou receber um erro,

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;

então eu faço isso

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

no entanto, isso não fornecerá os resultados pretendidos e a saída incorreta abaixo é mostrada.

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

Os resultados reais devem ser

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

Como posso resolver esse problema?

Nota: Esta tabela é uma VIEW criada a partir de uma operação anterior.

Cara aleatório
fonte
2
Relacionados: stackoverflow.com/q/18061285/398670
Craig Ringer
Eu não entendo Por que é wmname="usopp"esperado e não por exemplo wmname="luffy"?
AndreKR 7/04

Respostas:

226

Sim, este é um problema de agregação comum. Antes do SQL3 (1999) , os campos selecionados devem aparecer na GROUP BYcláusula [*].

Para solucionar esse problema, você deve calcular o agregado em uma subconsulta e depois associá-lo a si mesmo para obter as colunas adicionais que você precisa 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

Mas você também pode usar as funções da janela, que parecem mais simples:

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

A única coisa com esse método é que ele mostrará todos os registros (as funções da janela não agrupam). Mas ele mostrará o correto (ou seja, no cnamenível máximo ) MAXpara o país em cada linha, então é você quem decide:

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

A solução, sem dúvida menos elegante, para mostrar as únicas (cname, wmname)tuplas que correspondem ao valor máximo, é:

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, mesmo que o tipo de especificação permita selecionar campos não agrupados, os principais mecanismos parecem não gostar muito dele. Oracle e SQLServer simplesmente não permitem isso. O Mysql costumava permitir isso por padrão, mas agora desde 5.7 o administrador precisa habilitar esta opção ( ONLY_FULL_GROUP_BY) manualmente na configuração do servidor para que esse recurso seja suportado ...

Sebas
fonte
1
Obrigado, a sintaxe está correta, mas você deve comparar os valores de mx e avg ao ingressar
RandomGuy
1
Sim sua sintaxe está correta e elimina duplicatas no entanto você precisa m.avg = t.mx no final (depois que você escreveu joing) para obter os resultados destina
RandomGuy
1
@Sebas Isso pode ser feito sem participar MAX(veja a resposta por @ypercube, também há outra solução na minha resposta), mas não da maneira que você faz. Verifique a saída esperada.
Zero323
1
@Sebas Sua solução adiciona apenas uma coluna (o MAX avgpor cname), mas não restringe as linhas do resultado (como o OP deseja). Veja os resultados reais devem ser parágrafos na pergunta.
precisa saber é o seguinte
1
Desativar o ONLY_FULL_GROUP_BY MySQL 5.7 não ativa a maneira como o padrão SQL especifica quando as colunas podem ser omitidas do group by(ou faz o MySQL se comportar como o Postgres). Ele apenas reverte para o antigo comportamento, onde o MySQL retorna resultados aleatórios (= "indeterminados").
a_horse_with_no_name
126

No Postgres, você também pode usar a DISTINCT ON (expression)sintaxe especial :

SELECT DISTINCT ON (cname) 
    cname, wmname, avg
FROM 
    makerar 
ORDER BY 
    cname, avg DESC ;
ypercubeᵀᴹ
fonte
5
Não vai funcionar como se espera quando se quer classificar colunas como o AVG
amenzhinsky
@amenzhinsky O que você quer dizer? Se alguém deseja que o conjunto de resultados seja classificado com uma ordem diferente de BY cname?
ypercubeᵀᴹ
@ypercube, na verdade o psql classifica primeiro e depois aplica DISTINCT. No caso da classificação por avg vamos obter resultados diferentes para cada linha valores mínimos e máximos dependendo da direção do tipo
amenzhinsky
3
Claro. Se você não executar a consulta que publiquei, obterá resultados diferentes! Isso não é o mesmo que "não vai funcionar como esperado" ...
ypercubeᵀᴹ
1
@Batfan thnx. Observe que, embora isso seja bastante interessante, compacto e fácil de escrever, geralmente não é a maneira mais eficiente para esse tipo de consulta.
precisa saber é o seguinte
27

O problema com a especificação de campos não agrupados e não agregados em group byselects é que o mecanismo não tem como saber qual campo de registro ele deve retornar nesse caso. É o primeiro? É o último? Geralmente, não há registro que corresponda naturalmente ao resultado agregado ( mine maxsão exceções).

No entanto, existe uma solução alternativa: agregue também o campo obrigatório. No posgres, isso deve funcionar:

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

Note que isso cria uma matriz de todos os wnames, ordenados por avg, e retorna o primeiro elemento (as matrizes no postgres são baseadas em 1).

e-neko
fonte
Bom ponto. Embora pareça possível que o banco de dados possa fazer uma junção externa para vincular os campos não agregados de cada linha ao resultado agregado para o qual a linha contribuiu. Muitas vezes fiquei curioso por que eles não têm uma opção para isso. Embora eu poderia simplesmente ser ignorante desta opção :)
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 a rank() função de janela :

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

Qualquer um deles preservará vários valores máximos por grupo. Se você deseja apenas um registro único por grupo, mesmo que haja mais de um registro com avg igual a max, verifique a resposta do @ ypercube.

zero323
fonte
16

Para mim, não se trata de um "problema de agregação comum", mas apenas de uma consulta SQL incorreta. A resposta correta e única para "selecione a média máxima de cada nome de domínio ..." é

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

O resultado será:

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

Esse resultado, em geral, responde à pergunta "Qual é o melhor resultado para cada grupo?" . Vemos que o melhor resultado para a Espanha é 5 e para o Canadá o melhor resultado é 2. É verdade e não há erro. Se precisarmos exibir o wmname também, teremos que responder à pergunta: "Qual é a REGRA para escolher o wmname do conjunto resultante?" Vamos mudar um pouco os dados de entrada para esclarecer o erro:

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

Qual resultado você espera ao executar esta consulta SELECT cname, wmname, MAX(avg) FROM makerar GROUP BY cname;:? Deveria ser spain+luffyou spain+usopp? Por quê? Não é determinado na consulta como escolher "melhor" wmname se vários forem adequados, portanto, o resultado também não é determinado. É por isso que o interpretador SQL retorna um erro - a consulta não está correta.

Em outras palavras, não há resposta correta para a pergunta "Quem é o melhor em spaingrupo?" . Luffy não é melhor que usopp, porque usopp tem a mesma "pontuação".

ox160d05d
fonte
Esta solução funcionou para mim também. Eu tive problemas de consulta porque meu ORM também incluiu a chave primária associada, resultando na seguinte consulta incorreta :, SELECT cname, id, MAX(avg) FROM makerar GROUP BY cname;que deu esse erro enganoso.
Roberto
1

Isso parece funcionar bem

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

Recentemente, encontrei esse problema ao tentar contar usando case whene descobri que alterar a ordem das instruções whiche countcorrige o 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

Em vez de usar - neste último, onde obtive erros que maçãs e laranjas devem aparecer em funções agregadas

CASE WHEN ((apples = 'TRUE' OR oranges 'TRUE') THEN COUNT(*) END) END AS fruit_counter
Rachel Windzberg
fonte
1
A whichafirmação?
Hillary Sanders