MySQL «Группировать по» и «Сортировать по»

97

Я хочу иметь возможность выбрать группу строк из таблицы электронных писем и сгруппировать их по отправителю. Мой запрос выглядит так:

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

Запрос почти работает так, как я хочу - он выбирает записи, сгруппированные по электронной почте. Проблема в том, что тема и временная метка не соответствуют самой последней записи для определенного адреса электронной почты.

Например, он может вернуть:

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

Когда записи в базе данных:

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

Если тема "программного вопроса" самая последняя, ​​как я могу заставить MySQL выбирать эту запись при группировании электронных писем?

Джон Курлак
источник

Ответы:

140

Простое решение , чтобы обернуть запрос в подзапрос с заявлением ПОРЯДКА первым и применение GROUP BY позже :

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

Это похоже на использование соединения, но выглядит намного лучше.

Использование неагрегированных столбцов в SELECT с предложением GROUP BY является нестандартным. MySQL обычно возвращает значения первой найденной строки и отбрасывает остальные. Любые предложения ORDER BY будут применяться только к возвращаемому значению столбца, а не к отброшенным.

ВАЖНОЕ ОБНОВЛЕНИЕ. Выбор неагрегированных столбцов используется на практике, но на него не следует полагаться. Согласно документации MySQL "это полезно в первую очередь, когда все значения в каждом неагрегированном столбце, не указанном в GROUP BY, одинаковы для каждой группы. Сервер может выбрать любое значение из каждой группы, поэтому, если они не совпадают, значения избранные являются неопределенными ".

Начиная с 5.7.5 ONLY_FULL_GROUP_BY включен по умолчанию, поэтому неагрегированные столбцы вызывают ошибки запроса (ER_WRONG_FIELD_WITH_GROUP)

Как указывает @mikep ниже, решение состоит в использовании ANY_VALUE () из 5.7 и выше

См. 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-handle.html https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_any-value

b7kich
источник
7
Я придумал такое же решение несколько лет назад, и это отличное решение. Престижность b7kich. Однако здесь есть две проблемы ... GROUP BY нечувствителен к регистру, поэтому LOWER () не требуется, а во-вторых, $ userID кажется переменной непосредственно из PHP, ваш код может быть уязвимым для SQL-инъекций, если $ userID предоставляется пользователем, а не принудительно быть целым числом.
velcrow
ВАЖНОЕ ОБНОВЛЕНИЕ также относится к MariaDB: mariadb.com/kb/en/mariadb/…
Артур Шипковски
1
As of 5.7.5 ONLY_FULL_GROUP_BY is enabled by default, i.e. it's impossible to use non-aggregate columns.Режим SQL можно изменить во время выполнения без прав администратора, поэтому отключить ONLY_FULL_GROUP_BY очень просто. Например: SET SESSION sql_mode = '';. Демо: db-fiddle.com/f/esww483qFQXbXzJmkHZ8VT/3
mikep 02
1
Или другой альтернативой включенному обходу ONLY_FULL_GROUP_BY является использование ANY_VALUE (). См. Подробнее dev.mysql.com/doc/refman/8.0/en/…
mikep
42

Вот один из подходов:

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)

Обычно вы присоединяетесь к таблице в поисках более поздних строк. В предложении where вы указываете, что не может быть более поздних строк. Это дает вам только последнюю строку.

Если может быть несколько электронных писем с одной и той же меткой времени, этот запрос потребует уточнения. Если в таблице электронной почты есть столбец инкрементного идентификатора, измените JOIN следующим образом:

LEFT JOIN incomingEmails next
    on cur.fromEmail = next.fromEmail
    and cur.id < next.id
Андомар
источник
Сказал, что textIDэто неоднозначно = /
John Kurlak
1
Затем удалите двусмысленность и добавьте к ней префикс имени таблицы, например cur.textID. Изменился и ответ.
Andomar
Это единственное решение, которое можно сделать с помощью Doctrine DQL.
VisioN,
Это не работает, когда вы так хорошо пытаетесь самостоятельно объединиться для нескольких столбцов. IE, когда вы пытаетесь найти последний адрес электронной почты и последнее имя пользователя, и вам требуется несколько самостоятельных левых соединений для выполнения этой операции в одном запросе.
Ловин Дьялл,
При работе с прошлыми и будущими временными метками / датами, чтобы ограничить набор результатов не будущими датами, вам нужно добавить еще одно условие к LEFT JOINкритериямAND next.timestamp <= UNIX_TIMESTAMP()
fyrye
32

Как уже указывалось в ответе, текущий ответ неверен, потому что GROUP BY произвольно выбирает запись из окна.

Если вы используете MySQL 5.6 или MySQL 5.7 с ONLY_FULL_GROUP_BY, правильный (детерминированный) запрос:

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

Для эффективного выполнения запроса требуется правильная индексация.

Обратите внимание, что для упрощения я удалил LOWER(), который в большинстве случаев не будет использоваться.

Маркус
источник
2
Это должен быть правильный ответ. Я только что обнаружил на своем сайте ошибку, связанную с этим. В order byподзапросе в других ответах вообще не действует.
Jette
1
OMG, пожалуйста, сделайте это принятым ответом. Принятый потратил 5 часов моего времени :(
Ричард Керси,
29

Сделайте GROUP BY после ORDER BY, заключив свой запрос в GROUP BY следующим образом:

SELECT t.* FROM (SELECT * FROM table ORDER BY time DESC) t GROUP BY t.from
11101101b
источник
1
Значит, GROUP BY` автоматически выбирает самые свежие time, самые новые timeили случайные?
xrDDDD
1
Он выбирает самое новое время, потому что мы упорядочиваем по, time DESCа затем группа по берет первое (самое позднее).
11101101b 05
Если бы я только мог выполнять JOINS для вложенных выделений в VIEWS, в mysql 5.1. Возможно, эта функция появится в более новой версии.
IcarusNM
21

Согласно стандарту SQL вы не можете использовать неагрегированные столбцы в списке выбора. MySQL допускает такое использование (если не используется режим ONLY_FULL_GROUP_BY), но результат непредсказуем.

ONLY_FULL_GROUP_BY

Сначала следует выбрать fromEmail, MIN (чтение), а затем со вторым запросом (или подзапросом) - Subject.

noonex
источник
MIN (чтение) вернет минимальное значение «прочитано». Вместо этого он, вероятно, ищет флаг «прочитано» в последнем письме.
Andomar
2

Я боролся с обоими этими подходами для более сложных запросов, чем показанные, потому что подход с подзапросом был ужасно неэффективным независимо от того, какие индексы я использовал, и потому что я не мог получить внешнее самосоединение через Hibernate.

Лучший (и самый простой) способ сделать это - сгруппировать по чему-то, что сконструировано так, чтобы содержать конкатенацию требуемых полей, а затем вытащить их, используя выражения в предложении SELECT. Если вам нужно выполнить MAX (), убедитесь, что поле, над которым вы хотите выполнить MAX (), всегда находится на самом значительном конце объединенного объекта.

Ключ к пониманию этого заключается в том, что запрос может иметь смысл только в том случае, если эти другие поля инвариантны для любого объекта, который удовлетворяет Max (), поэтому с точки зрения сортировки другие части конкатенации можно игнорировать. Он объясняет, как это сделать, в самом низу этой ссылки. http://dev.mysql.com/doc/refman/5.0/en/group-by-hidden-columns.html

Если вы можете получить событие вставки / обновления (например, триггер) для предварительного вычисления конкатенации полей, вы можете его проиндексировать, и запрос будет таким же быстрым, как если бы группа по занимала только поле, которое вы действительно хотели MAX ( ). Вы даже можете использовать его, чтобы получить максимум нескольких полей. Я использую его для выполнения запросов к многомерным деревьям, выраженным в виде вложенных наборов.

Майк Н
источник