MySQL „Grupuj według” i „Zamów według”

97

Chcę móc wybrać kilka wierszy z tabeli e-maili i pogrupować je według nadawcy. Moje zapytanie wygląda następująco:

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

Zapytanie działa prawie tak, jak chcę - wybiera rekordy pogrupowane według wiadomości e-mail. Problem polega na tym, że temat i sygnatura czasowa nie odpowiadają najnowszemu rekordowi dla określonego adresu e-mail.

Na przykład może zwrócić:

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

Gdy rekordy w bazie danych to:

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

Jeśli temat „pytanie programistyczne” jest najnowszy, jak mogę zmusić MySQL do wybrania tego rekordu podczas grupowania wiadomości e-mail?

John Kurlak
źródło

Odpowiedzi:

140

Prostym rozwiązaniem jest zawinięcie zapytania do podselekcji z najpierw instrukcją ORDER, a później zastosowanie GROUP BY :

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

Jest to podobne do używania złączenia, ale wygląda o wiele ładniej.

Używanie niezagregowanych kolumn w SELECT z klauzulą ​​GROUP BY jest niestandardowe. MySQL zwróci wartości pierwszego znalezionego wiersza, a resztę odrzuci. Wszelkie klauzule ORDER BY będą miały zastosowanie tylko do zwróconej wartości kolumny, a nie do odrzuconych.

WAŻNA AKTUALIZACJA Wybieranie kolumn nieagregowanych używanych w praktyce, ale nie należy na nich polegać. Zgodnie z dokumentacją MySQL „jest to przydatne przede wszystkim wtedy, gdy wszystkie wartości w każdej niezagregowanej kolumnie, której nie podano w GROUP BY, są takie same dla każdej grupy. Serwer może wybrać dowolną wartość z każdej grupy, więc jeśli nie są one takie same, wartości wybrane są nieokreślone . "

Od 5.7.5 ONLY_FULL_GROUP_BY jest domyślnie włączone, więc kolumny nieagregowane powodują błędy zapytań (ER_WRONG_FIELD_WITH_GROUP)

Jak @mikep wskazuje poniżej, rozwiązaniem jest użycie ANY_VALUE () od 5.7 i nowszych

Zobacz 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 / pl / group-by-handling.html https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_any-value

b7kich
źródło
7
To samo rozwiązanie wymyśliłem kilka lat temu i jest to świetne rozwiązanie. kudos to b7kich. Jednak dwa problemy tutaj ... GROUP BY nie rozróżnia wielkości liter, więc LOWER () jest niepotrzebne, a po drugie, $ userID wydaje się być zmienną bezpośrednio z PHP, twój kod może być podatny na wstrzyknięcie sql, jeśli $ userID jest podany przez użytkownika i nie jest wymuszony być liczbą całkowitą.
velcrow
WAŻNA AKTUALIZACJA dotyczy również 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.Tryb SQL można zmienić w trakcie działania bez uprawnień administratora, więc bardzo łatwo jest wyłączyć ONLY_FULL_GROUP_BY. Na przykład: SET SESSION sql_mode = '';. Demo: db-fiddle.com/f/esww483qFQXbXzJmkHZ8VT/3
mikep
1
Lub inną alternatywą dla włączonego pomijania ONLY_FULL_GROUP_BY jest użycie ANY_VALUE (). Zobacz więcej dev.mysql.com/doc/refman/8.0/en/…
mikep
42

Oto jedno podejście:

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)

Zasadniczo dołączasz do stołu samodzielnie, wyszukując późniejsze wiersze. W klauzuli where stwierdzasz, że nie może być późniejszych wierszy. To daje tylko najnowszy wiersz.

Jeśli może istnieć wiele e-maili z tą samą sygnaturą czasową, to zapytanie wymaga doprecyzowania. Jeśli w tabeli e-maili znajduje się przyrostowa kolumna ID, zmień JOIN na przykład:

LEFT JOIN incomingEmails next
    on cur.fromEmail = next.fromEmail
    and cur.id < next.id
Andomar
źródło
Powiedział, że textIDto niejednoznaczne = /
John Kurlak
1
Następnie usuń ambuigity i dodaj do niego przedrostek nazwą tabeli, np. Cur.textID. Zmieniono również odpowiedź.
Andomar
Jest to jedyne rozwiązanie, które można zrobić z Doctrine DQL.
VisioN,
To nie działa, gdy tak dobrze próbujesz dołączyć do wielu kolumn. IE, gdy próbujesz znaleźć najnowszą wiadomość e-mail i najnowszą nazwę użytkownika i potrzebujesz wielu samodzielnych złączeń lewych, aby wykonać tę operację w jednym zapytaniu.
Loveen Dyall
Podczas pracy z przeszłymi i przyszłymi znacznikami czasu / datami, aby ograniczyć zestaw wyników do dat innych niż przyszłe, musisz dodać kolejny warunek do LEFT JOINkryteriówAND next.timestamp <= UNIX_TIMESTAMP()
fyrye
32

Jak już wskazano w odpowiedzi, bieżąca odpowiedź jest błędna, ponieważ GROUP BY arbitralnie wybiera rekord z okna.

Jeśli ktoś używa MySQL 5.6 lub MySQL 5.7 z ONLY_FULL_GROUP_BY, poprawne (deterministyczne) zapytanie to:

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

Aby zapytanie działało sprawnie, wymagane jest odpowiednie indeksowanie.

Zwróć uwagę, że dla uproszczenia usunąłem rozszerzenie LOWER(), które w większości przypadków nie będzie używane.

Marcus
źródło
2
To powinna być prawidłowa odpowiedź. Właśnie odkryłem związany z tym błąd w mojej witrynie. W order bypodselekcji w innych odpowiedziach nie ma żadnego efektu.
Jette,
1
OMG, niech to zaakceptowana odpowiedź. Ta zaakceptowana zmarnowała 5 godzin mojego czasu :(
Richard Kersey
29

Wykonaj GROUP BY po ORDER BY, opakowując zapytanie GROUP BY w następujący sposób:

SELECT t.* FROM (SELECT * FROM table ORDER BY time DESC) t GROUP BY t.from
11101101b
źródło
1
Więc GROUP BY` automatycznie wybiera najnowsze time, najnowsze timeczy losowe?
xrDDDD
1
Wybiera najnowszy czas, ponieważ składamy zamówienie, time DESCa następnie grupa według zajmuje pierwszy (ostatni).
11101101b
Gdybym tylko mógł wykonać JOINS na sub-selekcjach w VIEWS, w mysql 5.1. Może ta funkcja pojawi się w nowszej wersji.
IcarusNM
21

Zgodnie ze standardem SQL nie można używać kolumn nieagregowanych na liście wyboru. MySQL pozwala na takie użycie (bez użycia trybu ONLY_FULL_GROUP_BY), ale wynik nie jest przewidywalny.

ONLY_FULL_GROUP_BY

Najpierw należy wybrać fromEmail, MIN (czytaj), a następnie, przy drugim zapytaniu (lub podzapytaniu) - Temat.

noonex
źródło
MIN (odczyt) zwróci minimalną wartość „odczyt”. Prawdopodobnie szuka zamiast tego flagi „przeczytaj” najnowszej wiadomości e-mail.
Andomar
2

Zmagałem się z oboma tymi podejściami w przypadku bardziej złożonych zapytań niż te pokazane, ponieważ podejście do podzapytań było strasznie nieefektywne bez względu na to, jakie indeksy założyłem, i ponieważ nie mogłem uzyskać zewnętrznego sprzężenia samoczynnego przez Hibernate

Najlepszym (i najłatwiejszym) sposobem na to jest grupowanie według czegoś, co jest tak skonstruowane, że zawiera konkatenację wymaganych pól, a następnie wyciąganie ich za pomocą wyrażeń w klauzuli SELECT. Jeśli musisz wykonać MAX (), upewnij się, że pole, które chcesz MAX (), znajduje się zawsze na najbardziej znaczącym końcu łączonej encji.

Kluczem do zrozumienia tego jest to, że zapytanie może mieć sens tylko wtedy, gdy te inne pola są niezmienne dla dowolnej jednostki, która spełnia funkcję Max (), więc pod względem sortowania można zignorować inne elementy konkatenacji. Wyjaśnia, jak to zrobić, na samym dole tego łącza. http://dev.mysql.com/doc/refman/5.0/en/group-by-hidden-columns.html

Jeśli możesz uzyskać zdarzenie wstawiania / aktualizacji (takie jak wyzwalacz), aby wstępnie obliczyć konkatenację pól, możesz je zindeksować, a zapytanie będzie tak szybkie, jakby grupa obejmowała tylko pole, które faktycznie chciałeś MAX ( ). Możesz nawet użyć go, aby uzyskać maksymalnie wiele pól. Używam go do wykonywania zapytań dotyczących drzew wielowymiarowych wyrażonych jako zbiory zagnieżdżone.

Mike N
źródło