MySQL упорядкувати перед групою по


243

Тут можна знайти багато подібних питань, але я не думаю, що жоден відповідь на це питання адекватно.

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

Завдання в цьому випадку - отримати останню публікацію для кожного автора в базі даних.

Приклад запиту дає непридатні результати, оскільки його не завжди є остання публікація, яка повертається.

SELECT wp_posts.* FROM wp_posts
    WHERE wp_posts.post_status='publish'
    AND wp_posts.post_type='post'
    GROUP BY wp_posts.post_author           
    ORDER BY wp_posts.post_date DESC

Поточна прийнята відповідь є

SELECT
    wp_posts.*
FROM wp_posts
WHERE
    wp_posts.post_status='publish'
    AND wp_posts.post_type='post'
GROUP BY wp_posts.post_author
HAVING wp_posts.post_date = MAX(wp_posts.post_date) <- ONLY THE LAST POST FOR EACH AUTHOR
ORDER BY wp_posts.post_date DESC

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

Моє найкраще рішення - використовувати підзапит форми

SELECT wp_posts.* FROM 
(
    SELECT * 
    FROM wp_posts
    ORDER BY wp_posts.post_date DESC
) AS wp_posts
WHERE wp_posts.post_status='publish'
AND wp_posts.post_type='post'
GROUP BY wp_posts.post_author 

Тоді моє запитання просте: чи все-таки потрібно замовити рядки перед групуванням, не вдаючись до підзапиту?

Редагувати : Це питання було продовженням іншого питання, і специфіка моєї ситуації дещо відрізняється. Ви можете (і повинні) припустити, що також існує wp_posts.id, який є унікальним ідентифікатором для цієї посади.


2
Як ви згадували в коментарях до наведених відповідей, можливо, є кілька публікацій з тією ж міткою. Якщо так, наведіть приклад із даними та очікуваним результатом. І будь ласка, опишіть, чому ви очікуєте такого результату. post_authorі post_dateїх недостатньо, щоб отримати унікальний рядок, тому для отримання унікального ряду має бути більшеpost_author
Сер Руфо,

@SirRufo Ви маєте рацію, я додав для вас редагування.
Роб Форест

There are plenty of similar questions to be found on here but I don't think that any answer the question adequately.Ось для чого є щедрості.
Гонки легкості по орбіті

@LightnessRacesinOrbit, якщо в поточному питанні вже є прийнята відповідь, що, на мою думку, є неправильним, що б ви запропонували зробити?
Роб Форест

1
Цікаво, чому ви прийняли відповідь, яка використовує підзапит - коли ваше запитання чітко задає питання ... "" Чи все-таки потрібно замовити рядки перед групуванням, не вдаючись до підзапиту? "???
TV-C-15

Відповіді:


373

Використання ORDER BYв підзапиті - не найкраще рішення цієї проблеми.

Найкраще рішення для max(post_date)автора - використовувати підзапит для повернення максимальної дати, а потім приєднати його до таблиці як на, так post_authorі на максимальну дату.

Рішення повинно бути:

SELECT p1.* 
FROM wp_posts p1
INNER JOIN
(
    SELECT max(post_date) MaxPostDate, post_author
    FROM wp_posts
    WHERE post_status='publish'
       AND post_type='post'
    GROUP BY post_author
) p2
  ON p1.post_author = p2.post_author
  AND p1.post_date = p2.MaxPostDate
WHERE p1.post_status='publish'
  AND p1.post_type='post'
order by p1.post_date desc

Якщо у вас є такі вибіркові дані:

CREATE TABLE wp_posts
    (`id` int, `title` varchar(6), `post_date` datetime, `post_author` varchar(3))
;

INSERT INTO wp_posts
    (`id`, `title`, `post_date`, `post_author`)
VALUES
    (1, 'Title1', '2013-01-01 00:00:00', 'Jim'),
    (2, 'Title2', '2013-02-01 00:00:00', 'Jim')
;

Підзапит поверне максимальну дату та автора:

MaxPostDate | Author
2/1/2013    | Jim

Тоді, оскільки ви приєднаєтесь до таблиці до обох значень, ви повернете всі деталі цієї публікації.

Див. SQL Fiddle with Demo .

Щоб розширити свої коментарі щодо використання підзапиту для точного повернення цих даних.

MySQL не примушує вас до GROUP BYкожного стовпця, який ви включите до SELECTсписку. Як результат, якщо ви маєте лише GROUP BYодин стовпець, але повертаєте загалом 10 стовпців, немає гарантії, що інші значення стовпців, які належать до того, post_authorщо повертаються. Якщо стовпчик відсутній у GROUP BYMySQL, вибирає значення, яке потрібно повернути.

Використання підзапиту з функцією агрегату гарантує, що кожен раз повертається правильний автор та повідомлення

Як бічна примітка, хоча MySQL дозволяє використовувати ORDER BYпідзапит і дозволяє застосувати a GROUP BYдо не кожного стовпця у SELECTсписку, така поведінка заборонена в інших базах даних, включаючи SQL Server.


4
Я бачу, що ви там зробили, але це просто повертає дату, коли була зроблена остання публікація, а не весь рядок для цієї останньої публікації.
Роб Форест

1
@RobForrest саме це робить об'єднання. Ви повертаєте останню дату публікації у запиті за автором, а потім приєднуєтесь до своїх wp_postsобох стовпців, щоб отримати повний рядок.
Taryn

7
@RobForrest Для одного, коли ви застосовуєте GROUP BYлише один стовпець, немає гарантії, що значення в інших стовпцях будуть послідовно правильними. На жаль, MySQL дозволяє подібному типу SELECT / GROUPing статися з іншими продуктами. По-друге, синтаксис використання ORDER BYв підзапиті, дозволеного в MySQL, заборонено в інших продуктах бази даних, включаючи SQL Server. Ви повинні використовувати рішення, яке повертає належний результат щоразу, коли воно буде виконано.
Taryn

2
Для масштабування INDEX(post_author, post_date)важливим є склад .
Рік Джеймс

1
@ jtcotton63 Щоправда, але якщо ви розмістили post_idсвій внутрішній запит, то технічно ви також повинні згрупувати його, що, швидше за все, спотворить ваші результати.
Taryn

20

У вашому рішенні використовується розширення на пункт GROUP BY, яке дозволяє групувати за деякими полями (у даному випадку просто post_author):

GROUP BY wp_posts.post_author

та виберіть неагреговані стовпці:

SELECT wp_posts.*

які не вказані в групі за допомогою пункту або не використовуються в сукупній функції (MIN, MAX, COUNT тощо).

Правильне використання розширення до пункту GROUP BY

Це корисно, коли всі значення неагрегованих стовпців рівні для кожного рядка.

Наприклад, припустимо, у вас є стіл GardensFlowers( nameсаду, flowerякий росте в саду):

INSERT INTO GardensFlowers VALUES
('Central Park',       'Magnolia'),
('Hyde Park',          'Tulip'),
('Gardens By The Bay', 'Peony'),
('Gardens By The Bay', 'Cherry Blossom');

і ви хочете витягти всі квіти, які ростуть у саду, де росте кілька квітів. Тоді вам доведеться скористатися підзапитом, наприклад, ви можете використовувати цей:

SELECT GardensFlowers.*
FROM   GardensFlowers
WHERE  name IN (SELECT   name
                FROM     GardensFlowers
                GROUP BY name
                HAVING   COUNT(DISTINCT flower)>1);

Якщо вам потрібно витягти всі квіти, які є єдиними квітами в гардері, ви можете просто змінити умову HAVING на HAVING COUNT(DISTINCT flower)=1, але MySql також дозволяє вам використовувати це:

SELECT   GardensFlowers.*
FROM     GardensFlowers
GROUP BY name
HAVING   COUNT(DISTINCT flower)=1;

немає підзапиту, не стандартний SQL, але простіший.

Неправильне використання розширення до пункту GROUP BY

Але що станеться, якщо ВИБІРАТИ неагреговані стовпці, які не рівні для кожного ряду? Яке значення вибирає MySql для цього стовпця?

Схоже, MySql завжди обирає ПЕРШЕ значення, з яким стикається.

Щоб переконатися, що перше значення, яке воно стикається, є саме потрібним вами значенням, вам потрібно застосувати a GROUP BYдо упорядкованого запиту, отже, необхідність використання підзапиту. Ви не можете зробити це інакше.

Зважаючи на припущення, що MySql завжди вибирає перший рядок, з яким він стикається, ви правильно сортуєте рядки перед групою BY. Але, на жаль, якщо ви уважно прочитаєте документацію, ви помітите, що це припущення не відповідає дійсності.

Вибираючи не агреговані стовпці, які не завжди є однаковими, MySql вільний вибирати будь-яке значення, тому отримане значення, яке воно фактично показує, є невизначеним .

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

Цей посилання (спасибі ypercube!) Фокус GROUP BY оптимізований показує ситуацію, коли один і той же запит повертає різні результати між MySql та MariaDB, ймовірно, через інший механізм оптимізації.

Отже, якщо ця хитрість спрацює, це просто питання удачі.

Загальноприйнятий відповідь на інше питання виглядає не так зі мною:

HAVING wp_posts.post_date = MAX(wp_posts.post_date)

wp_posts.post_dateце неагрегований стовпець, і його значення буде офіційно невизначене, але, швидше за все, це буде першим post_dateзіткненням. Але оскільки хитрість GROUP BY застосовується до невпорядкованої таблиці, не впевнено, хто з них post_dateзустрічається вперше .

Це, ймовірно, поверне публікації, які є єдиними публікаціями одного автора, але навіть це не завжди точно.

Можливе рішення

Я думаю, що це може бути можливим рішенням:

SELECT wp_posts.*
FROM   wp_posts
WHERE  id IN (
  SELECT max(id)
  FROM wp_posts
  WHERE (post_author, post_date) = (
    SELECT   post_author, max(post_date)
    FROM     wp_posts
    WHERE    wp_posts.post_status='publish'
             AND wp_posts.post_type='post'
    GROUP BY post_author
  ) AND wp_posts.post_status='publish'
    AND wp_posts.post_type='post'
  GROUP BY post_author
)

У внутрішньому запиті я повертаю максимальну дату публікації для кожного автора. Тоді я беру до уваги той факт, що той самий автор теоретично міг би одночасно мати дві публікації, тому я отримую лише максимальний ідентифікатор. А потім я повертаю всі рядки, які мають ці максимальні ідентифікатори. Це можна зробити швидше, використовуючи приєднання замість пункту IN.

(Якщо ви впевнені, що IDце лише зростає, і якщо це ID1 > ID2також означає post_date1 > post_date2, що запит можна зробити набагато простішим, але я не впевнений, чи так це).


Це extension to GROUP Byцікаве прочитання, дякую за це.
Роб Форест

2
Приклад, коли вона не вдається: оптимізовано фокус GROUP BY:
ypercubeᵀᴹ

Неагреговані стовпці у вибраних виразах із GROUP BY більше не працюють за замовчуванням з MySQL 5.7: stackoverflow.com/questions/34115174/… . Що IMHO набагато безпечніше і змушує деяких людей писати ефективніші запити.
rink.attendant.6

Чи не використовується ця відповідь підпитом? Чи не Оригінальний плакат запитує рішення, яке НЕ використовує підзапит?
TV-C-15

1
@ TV-C-15 проблема полягає в вдачі до підзапиту, і я пояснюю, чому вдатися до підзапиту не вийде. Навіть у прийнятій відповіді використовується підзапит, але він починає пояснювати, чому вдаватися - це погана ідея ( Використання ЗАМОВЛЕННЯ В підзапиті - не найкраще рішення цієї проблеми )
fthiella

9

Те, що ти збираєшся читати, є досить хитким, тому не намагайся цього вдома!

Загалом у SQL відповідь на ваше запитання - НІ , але через розслаблений режим GROUP BY(згаданий @bluefeet ), відповідь ТАК у MySQL.

Припустимо, у вас індекс BTREE (post_status, post_type, post_author, post_date). Як виглядає індекс під капотом?

(post_status = 'опублікувати', post_type = 'повідомлення', post_author = 'користувач A', post_date = '2012-12-01') (post_status = 'опублікувати', post_type = 'повідомлення', post_author = 'користувач A', post_date = '2012-12-31') (post_status = 'опублікувати', post_type = 'повідомлення', post_author = 'користувач B', post_date = '2012-10-01') (post_status = 'опублікувати', post_type = ' публікація ', post_author =' користувач B ', post_date =' 2012-12-01 ')

Тобто дані сортуються по всіх цих полях у порядку зростання.

Коли ви робите GROUP BYза замовчуванням, він сортує дані за полем групування ( post_authorу нашому випадку; post_status, post_type потрібніWHERE пунктом), і якщо є відповідний індекс, він бере дані для кожного першого запису у порядку зростання. Тобто запит отримає наступне (перша публікація для кожного користувача):

(post_status = 'опублікувати', post_type = 'повідомлення', post_author = 'користувач A', post_date = '2012-12-01') (post_status = 'опублікувати', post_type = 'повідомлення', post_author = 'користувач B', post_date = '2012-10-01')

Але GROUP BYв MySQL дозволяє чітко вказати замовлення. І коли ви подаєте запит post_userу порядку зменшення, він пройде через наш індекс у протилежному порядку, все ще приймаючи перший запис для кожної групи, який насправді є останнім.

Це є

...
WHERE wp_posts.post_status='publish' AND wp_posts.post_type='post'
GROUP BY wp_posts.post_author DESC

дасть нам

(post_status = 'опублікувати', post_type = 'повідомлення', post_author = 'користувач B', post_date = '2012-12-01') (post_status = 'опублікувати', post_type = 'повідомлення', post_author = 'користувач A', post_date = '2012-12-31')

Тепер, коли ви замовляєте результати групування за post_date, ви отримуєте потрібні дані.

SELECT wp_posts.*
FROM wp_posts
WHERE wp_posts.post_status='publish' AND wp_posts.post_type='post'
GROUP BY wp_posts.post_author DESC
ORDER BY wp_posts.post_date DESC;

Примітка :

Це не те, що я рекомендував би для цього конкретного запиту. У цьому випадку я використовував би трохи змінену версію того, що пропонує @bluefeet . Але ця методика може бути дуже корисною. Подивіться мою відповідь тут: Отримання останнього запису в кожній групі

Підводні камені : Недоліками підходу є те, що

  • результат запиту залежить від індексу, що суперечить духу SQL (індекси повинні лише пришвидшити запити);
  • індекс нічого не знає про його вплив на запит (ви чи хтось інший у майбутньому може виявити індекс занадто трудомістким і змінити його якось, порушивши результати запиту, а не лише його ефективність)
  • якщо ви не розумієте, як працює запит, швидше за все, ви забудете пояснення через місяць, і запит збентежить вас і ваших колег.

Перевагою є продуктивність у важких випадках. У цьому випадку ефективність запиту повинна бути такою ж, як і в запиті @ bluefeet, через кількість даних, що беруть участь у сортуванні (всі дані завантажуються у тимчасову таблицю і потім сортуються; btw, його запит також вимагає (post_status, post_type, post_author, post_date)індексу) .

Що я б запропонував :

Як я вже сказав, ці запити змушують MySQL витрачати час на сортування потенційно величезної кількості даних у тимчасовій таблиці. У випадку, якщо вам потрібна пейджінг (тобто задіяно LIMIT), більшість даних навіть скидаються. Що я б робив, це мінімізувати кількість відсортованих даних: це сортування та обмеження мінімуму даних у підзапиті, а потім приєднання до всієї таблиці.

SELECT * 
FROM wp_posts
INNER JOIN
(
  SELECT max(post_date) post_date, post_author
  FROM wp_posts
  WHERE post_status='publish' AND post_type='post'
  GROUP BY post_author
  ORDER BY post_date DESC
  -- LIMIT GOES HERE
) p2 USING (post_author, post_date)
WHERE post_status='publish' AND post_type='post';

Той самий запит, використовуючи описаний вище підхід:

SELECT *
FROM (
  SELECT post_id
  FROM wp_posts
  WHERE post_status='publish' AND post_type='post'
  GROUP BY post_author DESC
  ORDER BY post_date DESC
  -- LIMIT GOES HERE
) as ids
JOIN wp_posts USING (post_id);

Усі ці запити з планами їх виконання на SQLFiddle .


Це цікава техніка, куди ви їдете туди. Дві речі: ти кажеш, не намагайся цього вдома, які потенційні підводні камені? по-друге, ви згадуєте дещо змінену версію відповіді синього фута, що це було б?
Роб Форест

Дякую за це, цікаво бачити, як хтось атакує проблему по-іншому. Оскільки мій набір даних ніде не знаходиться біля ваших рядків 18M +, я не думаю, що продуктивність не є настільки важливою, як ремонтопридатність, тому я думаю, що ваші пізніші варіанти, ймовірно, більше підходять. Мені подобається ідея обмеження на внутрішній стороні підпиту.
Роб Форест

8

Спробуйте це. Просто отримайте список останніх дат публікації від кожного автора . Це воно

SELECT wp_posts.* FROM wp_posts WHERE wp_posts.post_status='publish'
AND wp_posts.post_type='post' AND wp_posts.post_date IN(SELECT MAX(wp_posts.post_date) FROM wp_posts GROUP BY wp_posts.post_author) 

@Rob Forrest, перевіри моє рішення. Це вирішує ваше питання, сподіваємось!
sanchitkhanna26

1
Вибачте, я не думаю, що це спрацює. Наприклад, якщо і автор 1, і автор 2 публікують щось 02.02.13, а потім автор 2 публікації щось нове 08.02.2013, усі 3 публікації будуть повернуті. Так, поле для дати включає час, тому ситуація є менш вірогідною, але це аж ніяк не гарантується на досить великому наборі даних.
Роб Форест

+1 для використання post_date IN (select max(...) ...). Це ефективніше, ніж робити групу в підборі
Seaux

просто для уточнення, що оптимальніше лише, якщо індексовано post_author.
Seaux

1
IN ( SELECT ... )набагато менш ефективний, ніж еквівалент JOIN.
Рік Джеймс

3

Ні. Немає сенсу замовляти записи перед групуванням, оскільки групування буде мутувати набір результатів. Спосіб запиту є кращим способом. Якщо це відбувається занадто повільно, вам доведеться змінити дизайн таблиці, наприклад, зберігаючи ідентифікатор останнього допису для кожного автора в окремій таблиці або ввести булеву колонку із зазначенням для кожного автора, який з його дописів є останнім один.


Зауважте, як би ви відповіли на коментарі Bluefeet про те, що цей тип запиту не є правильним синтаксисом SQL і тому не переноситься на платформах баз даних? Також існує побоювання, що немає гарантії, що це дасть правильні результати кожного разу.
Роб Форест

2

Просто використовуйте функцію max та групову функцію

    select max(taskhistory.id) as id from taskhistory
            group by taskhistory.taskid
            order by taskhistory.datum desc

3
Що робити, якщо той, хто має найвищий ідентифікатор, не є останнім часом? Прикладом цього може бути те, що автор тривалий час займав свою чернетку перед публікацією.
Роб Форест

0

Тільки для резюме, стандартне рішення використовує некорельований підпит і виглядає так:

SELECT x.*
  FROM my_table x
  JOIN (SELECT grouping_criteria,MAX(ranking_criterion) max_n FROM my_table GROUP BY grouping_criteria) y
    ON y.grouping_criteria = x.grouping_criteria
   AND y.max_n = x.ranking_criterion;

Якщо ви використовуєте стародавню версію MySQL або досить невеликий набір даних, то ви можете використовувати наступний метод:

SELECT x.*
  FROM my_table x
  LEFT
  JOIN my_table y
    ON y.joining_criteria = x.joining_criteria
   AND y.ranking_criteria < x.ranking_criteria
 WHERE y.some_non_null_column IS NULL;  

Коли ви говорите стародавню версію, на якій версії MySQL це запускається? І, вибачте, ні, набір даних досить великий у моєму прикладі.
Роб Форест

Він працюватиме (повільно) на будь-якій версії. Старіші версії не можуть використовувати підзапити.
Полуниця

Так, метод №2 (версія, яку я спробував звідси ), не працюватиме на великому наборі даних (мільйони рядків), викидає втрачену помилку підключення . Спосіб №1 займає ~ 15 секунд для виконання запиту. Спочатку я хотів уникати використання вкладених запитів, але це змусило мене переглянути. Дякую!
aexl

@TheSexiestManinJamaica Так. Не так багато змінилося за 3,5 роки. Якщо припустити, що запит сам по собі ефективний, то час, який запит потребує на виконання, значною мірою залежить від розміру набору даних, розташування індексів та наявного обладнання.
Полуниця

-1

** Підзапроси можуть погано вплинути на продуктивність при використанні з великими наборами даних **

Оригінальний запит

SELECT wp_posts.*
FROM   wp_posts
WHERE  wp_posts.post_status = 'publish'
       AND wp_posts.post_type = 'post'
GROUP  BY wp_posts.post_author
ORDER  BY wp_posts.post_date DESC; 

Змінений запит

SELECT p.post_status,
       p.post_type,
       Max(p.post_date),
       p.post_author
FROM   wp_posts P
WHERE  p.post_status = "publish"
       AND p.post_type = "post"
GROUP  BY p.post_author
ORDER  BY p.post_date; 

Тому що я використовую maxв select clause==> max(p.post_date)можна уникнути підборів запитів і замовлення стовпцем "Макс" після групи по.


1
Це дійсно повертає найновіший post_date на автора, але немає гарантії, що решта повернених даних стосується публікації з останньою публікацією post_date.
Роб Форест

@RobForrest -> я не розумію, чому? добре розробити свою відповідь і просто викинути претензії. Наскільки я розумію, дані гарантовано пов'язані, оскільки я використовую там, де застереження фільтрують відповідні дані.
guykaplan

1
По мірі ви абсолютно правильні, кожне з 4 вибраних вами полів буде стосуватися цього максимуму post_date, але це не відповідає на запитання, яке було задано. Наприклад, якщо ви додали post_id або вміст публікації, ці стовпці не будуть гарантовані з тієї ж записи, що і максимальна дата. Щоб отримати ваш запит вище, щоб повернути решту деталей публікації, вам доведеться запустити другий запит. Якщо питання стосувалося пошуку дати останньої публікації, то так, вам відповісти було б добре.
Роб Форест

@guykaplan, Запити не повільні. Розмір набору даних не має значення. Це залежить від того, як ви його використовуєте. Дивіться percona.com/blog/2010/03/18/when-the-subselect-runs-faster
Pacerier

@Pacerier: стаття дійсно показує, як можна отримати вигоду від продуктивності за допомогою підзапитів, але я хотів би бачити, як ви конвертуєте даний сценарій для кращої роботи. і розмір даних важливий, і знову ж таки в даній статті, яку ви опублікували, ви припускаєте, що існує лише одна таблиця. розмір даних не за розміром рядка, а за розміром складності. сказавши, що, якщо ви працюєте з дійсно великою таблицею (не так багато таблиць), запит може бути набагато кращим.
guykaplan

-4

По-перше, не використовуйте * у виборі, впливає на їхню ефективність та перешкоджає використанню групи за дорученням та замовленням. Спробуйте цей запит:

SELECT wp_posts.post_author, wp_posts.post_date as pdate FROM wp_posts
WHERE wp_posts.post_status='publish'
AND wp_posts.post_type='post'
GROUP BY wp_posts.post_author           
ORDER BY pdate DESC

Якщо ви не вкажете таблицю в ORDER BY, а лише псевдонім, вони впорядкують результат вибору.


Ігноруйте обрані *, вони в цьому прикладі для стислості. Ваша відповідь точно така ж, як і перший приклад, який я дав.
Роб Форест

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