MySQL “Agrupar por” e “Ordenar por”

96

Quero poder selecionar um monte de linhas de uma tabela de e-mails e agrupá-las pelo remetente. Minha consulta é semelhante a esta:

SELECT 
    `timestamp`, `fromEmail`, `subject`
FROM `incomingEmails` 
GROUP BY LOWER(`fromEmail`) 
ORDER BY `timestamp` DESC

A consulta funciona quase como eu desejo - ela seleciona registros agrupados por e-mail. O problema é que o assunto e o carimbo de data / hora não correspondem ao registro mais recente de um endereço de e-mail específico.

Por exemplo, pode retornar:

fromEmail: john@example.com, subject: hello
fromEmail: mark@example.com, subject: welcome

Quando os registros no banco de dados são:

fromEmail: john@example.com, subject: hello
fromEmail: john@example.com, subject: programming question
fromEmail: mark@example.com, subject: welcome

Se o assunto "questão de programação" for o mais recente, como posso fazer com que o MySQL selecione esse registro ao agrupar os e-mails?

John Kurlak
fonte

Respostas:

140

Uma solução simples é envolver a consulta em uma subseleção com a instrução ORDER primeiro e aplicar o GROUP BY depois :

SELECT * FROM ( 
    SELECT `timestamp`, `fromEmail`, `subject`
    FROM `incomingEmails` 
    ORDER BY `timestamp` DESC
) AS tmp_table GROUP BY LOWER(`fromEmail`)

Isso é semelhante a usar a junção, mas parece muito melhor.

Usar colunas não agregadas em um SELECT com uma cláusula GROUP BY não é padrão. O MySQL geralmente retorna os valores da primeira linha que encontra e descarta o resto. Qualquer cláusula ORDER BY só se aplicará ao valor da coluna retornado, não aos descartados.

ATUALIZAÇÃO IMPORTANTE A seleção de colunas não agregadas costuma funcionar na prática, mas não deve ser considerada confiável. De acordo com a documentação do MySQL "isto é útil principalmente quando todos os valores em cada coluna não agregada não nomeada no GROUP BY são os mesmos para cada grupo. O servidor é livre para escolher qualquer valor de cada grupo, a menos que sejam iguais, os valores escolhidos são indeterminados . "

A partir de 5.7.5 ONLY_FULL_GROUP_BY é habilitado por padrão, então colunas não agregadas causam erros de consulta (ER_WRONG_FIELD_WITH_GROUP)

Como @mikep aponta abaixo, a solução é usar ANY_VALUE () de 5.7 e acima

Consulte http://www.cafewebmaster.com/mysql-order-sort-group https://dev.mysql.com/doc/refman/5.6/en/group-by-handling.html https: //dev.mysql .com / doc / refman / 5.7 / en / group-by-handling.html https://dev.mysql.com/doc/refman/5.7/en/misc Miscellaneous-functions.html#function_any-value

b7kich
fonte
7
Eu vim com a mesma solução alguns anos atrás, e é uma ótima solução. parabéns para b7kich. No entanto, dois problemas aqui ... GROUP BY não faz distinção entre maiúsculas e minúsculas, então LOWER () é desnecessário e, segundo, $ userID parece ser uma variável diretamente do PHP, seu código pode ser vulnerável à injeção de sql se $ userID for fornecido pelo usuário e não forçado para ser um número inteiro.
velcrow
A ATUALIZAÇÃO IMPORTANTE também se aplica ao MariaDB: mariadb.com/kb/en/mariadb/…
Arthur Shipkowski
1
As of 5.7.5 ONLY_FULL_GROUP_BY is enabled by default, i.e. it's impossible to use non-aggregate columns.O modo SQL pode ser alterado durante o tempo de execução sem privilégios de administrador, por isso é muito fácil desabilitar ONLY_FULL_GROUP_BY. Por exemplo: SET SESSION sql_mode = '';. Demo: db-fiddle.com/f/esww483qFQXbXzJmkHZ8VT/3
mikep
1
Ou outra alternativa para o bypass ativado ONLY_FULL_GROUP_BY é usar ANY_VALUE (). Veja mais dev.mysql.com/doc/refman/8.0/en/…
mikep
42

Aqui está uma abordagem:

SELECT cur.textID, cur.fromEmail, cur.subject, 
     cur.timestamp, cur.read
FROM incomingEmails cur
LEFT JOIN incomingEmails next
    on cur.fromEmail = next.fromEmail
    and cur.timestamp < next.timestamp
WHERE next.timestamp is null
and cur.toUserID = '$userID' 
ORDER BY LOWER(cur.fromEmail)

Basicamente, você une a tabela a si mesma, procurando por linhas posteriores. Na cláusula where, você afirma que não pode haver linhas posteriores. Isso fornece apenas a linha mais recente.

Se houver vários e-mails com o mesmo carimbo de data / hora, essa consulta precisará ser refinada. Se houver uma coluna de ID incremental na tabela de e-mail, altere o JOIN como:

LEFT JOIN incomingEmails next
    on cur.fromEmail = next.fromEmail
    and cur.id < next.id
Andomar
fonte
Disse que textIDera ambíguo = /
John Kurlak
1
Em seguida, remova a ambiguidade e prefixe-a com o nome da tabela, como cur.textID. A resposta também mudou.
Andomar
Esta é a única solução que é possível fazer com Doctrine DQL.
VisioN
Isso não funciona quando você está tentando unir a si mesmo para várias colunas tão bem. IE, quando você está tentando encontrar o e-mail e o nome de usuário mais recentes e precisa de várias junções à esquerda para realizar essa operação em uma única consulta.
Loveen Dyall
Ao trabalhar com carimbos de data / hora passados ​​e futuros, para limitar o conjunto de resultados a datas não futuras, você precisa adicionar outra condição aos LEFT JOINcritériosAND next.timestamp <= UNIX_TIMESTAMP()
fyrye
32

Como já apontado em uma resposta, a resposta atual está errada, pois o GROUP BY seleciona arbitrariamente o registro da janela.

Se alguém estiver usando o MySQL 5.6 ou MySQL 5.7 com ONLY_FULL_GROUP_BY, a consulta correta (determinística) é:

SELECT incomingEmails.*
  FROM (
    SELECT fromEmail, MAX(timestamp) `timestamp`
    FROM incomingEmails
    GROUP BY fromEmail
  ) filtered_incomingEmails
  JOIN incomingEmails USING (fromEmail, timestamp)
GROUP BY fromEmail, timestamp

Para que a consulta seja executada com eficiência, é necessária uma indexação adequada.

Observe que, para fins de simplificação, removi o LOWER(), que na maioria dos casos, não será usado.

Marcus
fonte
2
Esta deve ser a resposta correta. Acabei de descobrir um bug no meu site relacionado a isso. O order byin subselect nas outras respostas não tem efeito algum.
Jette
1
OMG, por favor, faça esta a resposta aceita. O aceito desperdiçou 5 horas do meu tempo :(
Richard Kersey
29

Faça um GROUP BY após ORDER BY envolvendo sua consulta com o GROUP BY assim:

SELECT t.* FROM (SELECT * FROM table ORDER BY time DESC) t GROUP BY t.from
11101101b
fonte
1
Então o GROUP BY` seleciona automaticamente o mais recente time, ou o mais novo time, ou aleatoriamente?
xrDDDD
1
Ele seleciona a hora mais recente porque estamos ordenando por time DESCe, em seguida, o agrupamento por pega a primeira (mais recente).
11101101b
Agora, se eu pudesse fazer JOINS em sub-seleções em VIEWS, no mysql 5.1. Talvez esse recurso venha em uma versão mais recente.
IcarusNM
21

De acordo com o padrão SQL, você não pode usar colunas não agregadas na lista de seleção. O MySQL permite tal uso (uless modo ONLY_FULL_GROUP_BY usado) mas o resultado não é previsível.

ONLY_FULL_GROUP_BY

Você deve primeiro selecionar fromEmail, MIN (ler) e, em seguida, com a segunda consulta (ou subconsulta) - Assunto.

noonex
fonte
MIN (leitura) retornaria o valor mínimo de "leitura". Ele provavelmente está procurando pelo sinalizador "ler" do último e-mail.
Andomar
2

Lutei com ambas as abordagens para consultas mais complexas do que as mostradas, porque a abordagem de subconsulta era terrivelmente ineficiente, não importa quais índices eu colocasse, e porque não consegui obter a auto-junção externa por meio do Hibernate

A melhor (e mais fácil) maneira de fazer isso é agrupar por algo que é construído para conter uma concatenação dos campos de que você precisa e, em seguida, extraí-los usando expressões na cláusula SELECT. Se você precisar fazer um MAX (), certifique-se de que o campo do qual deseja MAX () esteja sempre na extremidade mais significativa da entidade concatenada.

A chave para entender isso é que a consulta só pode fazer sentido se esses outros campos forem invariáveis ​​para qualquer entidade que satisfaça Max (), portanto, em termos de classificação, as outras partes da concatenação podem ser ignoradas. Ele explica como fazer isso na parte inferior deste link. http://dev.mysql.com/doc/refman/5.0/en/group-by-hidden-columns.html

Se você puder obter um evento de inserção / atualização (como um gatilho) para pré-calcular a concatenação dos campos, você pode indexá-lo e a consulta será tão rápida como se o agrupamento por estivesse sobre apenas o campo que você realmente deseja MAX ( ) Você pode até mesmo usá-lo para obter o máximo de vários campos. Eu o uso para fazer consultas em árvores multidimensionais expressas como conjuntos aninhados.

Mike N
fonte