MySQL "Групувати за" та "Порядок за"


96

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

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

Просте рішення - обернути запит у підвибір із оператором ORDER спочатку і застосувати 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/uk/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/uk/miscellaneous-functions.html#function_any-value


7
Я запропонував те саме рішення кілька років тому, і це чудове рішення. слава b7kich. Тут є дві проблеми ... GROUP BY не враховує регістр, тому LOWER () непотрібний, а по-друге, $ userID здається змінною безпосередньо з PHP, ваш код може бути вразливим для sql-ін'єкції, якщо $ userID надається користувачем і не примусово бути цілим числом.
липучка

ВАЖЛИВЕ ОНОВЛЕННЯ також стосується 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,

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 ви вказуєте, що пізніших рядків бути не може. Це дає вам лише останній рядок.

Якщо може бути кілька електронних листів з однаковою міткою часу, цей запит потребує уточнення. Якщо в таблиці електронної пошти є стовпчик додаткового ідентифікатора, змініть ПРИЄДНАЙТЕ так:

LEFT JOIN incomingEmails next
    on cur.fromEmail = next.fromEmail
    and cur.id < next.id

Сказав, що textIDбуло неоднозначно = /
Джон Курлак

1
Потім видаліть амбузіт і додайте до нього префікс з назвою таблиці, наприклад cur.textID. Змінено і у відповіді.
Andomar

Це єдине рішення, яке можливо зробити з Dctrine 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

Зробіть групу BY після ЗАМОВЛЕННЯ, обернувши свій запит за допомогою GROUP BY:

SELECT t.* FROM (SELECT * FROM table ORDER BY time DESC) t GROUP BY t.from

1
Тож GROUP BY` автоматично вибирає останню time, чи найновішу time, або випадкову?
xrDDDD

1
Він вибирає найновіший час, оскільки ми впорядковуємось, time DESCа потім група забирає перший (останній).
11101101b

Тепер, якби я міг зробити JOINS на підвиборах у VIEWS, у mysql 5.1. Можливо, ця функція з’являється в новішому випуску.
IcarusNM

21

Відповідно до стандарту SQL, ви не можете використовувати неагреговані стовпці у списку вибору. MySQL дозволяє таке використання (якщо використовується режим ONLY_FULL_GROUP_BY), але результат не передбачуваний.

ONLY_FULL_GROUP_BY

Спочатку слід вибрати електронну пошту, MIN (прочитати), а потім, з другим запитом (або підзапитом) - Subject.


MIN (read) повертає мінімальне значення "read". Ймовірно, він замість цього шукає прапорець "прочитати" останнього електронного листа.
Andomar

2

Я боровся з обома цими підходами для більш складних запитів, ніж показані, тому що підхід до підзапитів був жахливо неефективним, незалежно від того, які індекси я ставлю, і тому, що я не зміг отримати зовнішнє самоз’єднання через Hibernate

Найкращий (і найпростіший) спосіб це зробити - згрупувати за чимось, що сконструйовано для вміщення конкатенації полів, які вам потрібні, а потім витягнути їх, використовуючи вирази в реченні SELECT. Якщо вам потрібно зробити MAX (), переконайтеся, що поле, яке ви хочете перейти до MAX (), завжди знаходиться на найбільш значущому кінці об'єднаної сутності.

Ключ до розуміння цього полягає в тому, що запит може мати сенс лише в тому випадку, якщо ці інші поля є інваріантними для будь-якої сутності, яка задовольняє Max (), тому з точки зору сортування інші частини конкатенації можна ігнорувати. Це пояснює, як це зробити в самому низу цього посилання. http://dev.mysql.com/doc/refman/5.0/uk/group-by-hidden-column.html

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.