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 с демонстрацией .

Чтобы расширить мои комментарии об использовании подзапроса для точного возврата этих данных.

MySQL не принуждает вас к GROUP BYкаждому столбцу, который вы включаете в SELECTсписок. В результате, если вы только GROUP BYодин столбец, но возвращаете в общей сложности 10 столбцов, нет никакой гарантии, что значения других столбцов, которые принадлежат post_author, возвращаются. Если столбец отсутствует в GROUP BYMySQL, выбирает, какое значение следует вернуть.

Использование подзапроса с функцией агрегата гарантирует, что каждый раз будет возвращаться правильный автор и сообщение.

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

Тарын
источник
4
Я вижу, что вы там сделали, но это просто возвращает дату создания самого последнего сообщения, а не всю строку для этого самого последнего сообщения.
Роб Форрест
1
@RobForrest это то, что делает объединение. Вы возвращаете самую последнюю дату публикации в подзапросе по автору, а затем присоединяетесь к вашей wp_postsв обоих столбцах, чтобы получить полную строку.
Тарын
7
@RobForrest Для одного, когда вы применяете GROUP BYтолько к одному столбцу, нет гарантии, что значения в других столбцах будут последовательно правильными. К сожалению, MySQL допускает такой тип SELECT / GROUPing, что другие продукты этого не делают. Во-вторых, синтаксис использования ORDER BYв подзапросе, хотя и разрешен в MySQL, не разрешен в других продуктах баз данных, включая SQL Server. Вы должны использовать решение, которое будет возвращать правильный результат при каждом его выполнении.
Тарын
2
Для масштабирования INDEX(post_author, post_date)важно соединение .
Рик Джеймс
1
@ jtcotton63 Верно, но если вы добавите post_idсвой внутренний запрос, то технически вы должны также сгруппировать его, что, скорее всего, исказит ваши результаты.
Тарын
20

Ваше решение использует расширение предложения GROUP BY , которое позволяет группировать по некоторым полям (в данном случае просто post_author):

GROUP BY wp_posts.post_author

и выберите неагрегированные столбцы:

SELECT wp_posts.*

которые не перечислены в предложении group by или которые не используются в статистической функции (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 всегда выбирает первую встреченную строку, вы корректно сортируете строки перед GROUP 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, что запрос можно сделать намного проще, но я не уверен, что это так).

fthiella
источник
Это 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 проблема связана с повторным использованием подзапроса, и я объясняю, почему повторное использование подзапроса не будет работать. Даже принятый ответ использует подзапрос, но он начинает объяснять, почему прибегание является плохой идеей ( Использование ORDER BY в подзапросе не лучшее решение этой проблемы )
fthiella
9

То, что вы собираетесь прочитать, довольно хакерское, так что не пытайтесь делать это дома!

В целом, в SQL ответом на ваш вопрос является НЕТ , но из-за смягченного режима GROUP BY(упомянутого @bluefeet ) ответ - ДА в MySQL.

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

(post_status = 'publish', post_type = 'post', post_author = 'user A', post_date = '2012-12-01') (post_status = 'publish', post_type = 'post', post_author = 'user A', post_date = '2012-12-31') (post_status = 'publish', post_type = 'post', post_author = 'user B', post_date = '2012-10-01') (post_status = 'publish', post_type = ' post ', post_author =' пользователь B ', post_date =' 2012-12-01 ')

То есть данные сортируются по всем этим полям в порядке возрастания.

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

(post_status = 'publish', post_type = 'post', post_author = 'user A', post_date = '2012-12-01') (post_status = 'publish', post_type = 'post', post_author = 'user 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 = 'publish', post_type = 'post', post_author = 'user B', post_date = '2012-12-01') (post_status = 'publish', post_type = 'post', post_author = 'user 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;

NB :

Это не то, что я бы порекомендовал для этого конкретного запроса. В этом случае я бы использовал слегка измененную версию того, что предлагает @bluefeet . Но эта техника может быть очень полезной. Посмотрите на мой ответ здесь: получение последней записи в каждой группе

Подводные камни : недостатки подхода в том, что

  • результат запроса зависит от индекса, что противоречит духу SQL (индексы должны только ускорять запросы);
  • index ничего не знает о его влиянии на запрос (вы или кто-то еще в будущем можете счесть индекс слишком ресурсоемким и каким-то образом изменить его, нарушив результаты запроса, а не только его производительность)
  • Если вы не понимаете, как работает запрос, скорее всего, вы забудете объяснение через месяц, и запрос запутает вас и ваших коллег.

Преимущество - производительность в тяжелых случаях. В этом случае производительность запроса должна быть такой же, как и в запросе @ bluefeet, из-за объема данных, участвующих в сортировке (все данные загружаются во временную таблицу и затем сортируются; кстати, его запрос также требует (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 .

newtover
источник
Это интересная техника, которую вы собираетесь использовать. Две вещи: вы говорите, не пробуйте это дома, каковы потенциальные подводные камни? во-вторых, вы упомянули слегка измененную версию ответа bluefeet, что бы это было?
Роб Форрест
Спасибо за это, интересно видеть, что кто-то нападает на проблему по-другому. Поскольку мой набор данных далеко не соответствует вашим строкам 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) 
sanchitkhanna26
источник
@Rob Forrest, проверь мое решение. Надеюсь, это решит ваш вопрос!
sanchitkhanna26
1
Извините, я не думаю, что это сработает. Например, если автор 1 и автор 2 публикуют что-то 01.02.13, а затем автор 2 публикует что-то новое 08.02.13, будут возвращены все 3 сообщения. Да, поле datetime включает время, поэтому ситуация менее вероятна, но это ни в коем случае не гарантируется для достаточно большого набора данных.
Роб Форрест
+1 за использование post_date IN (select max(...) ...). Это более эффективно , чем делает группу, в суб выбора, см dev.mysql.com/doc/refman/5.6/en/subquery-optimization.html
Seaux
просто чтобы уточнить, это только более оптимально, если вы проиндексировали post_author.
Seaux
1
IN ( SELECT ... )гораздо менее эффективен, чем эквивалентный JOIN.
Рик Джеймс
3

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

Dennisch
источник
Дениш, как бы вы ответили на комментарии Bluefeet о том, что этот тип запроса не является правильным синтаксисом SQL и, следовательно, не переносится на платформы баз данных? Есть также опасения, что нет гарантии, что это будет давать правильные результаты каждый раз.
Роб Форрест
2

Просто используйте функцию max и функцию group

    select max(taskhistory.id) as id from taskhistory
            group by taskhistory.taskid
            order by taskhistory.datum desc
Константин XFlash Стратигенас
источник
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)можно избежать запросов sub select и упорядочить по столбцу max после группировки по.

guykaplan
источник
1
Это действительно возвращает самую последнюю запись post_date для каждого автора, но нет никакой гарантии, что остальная часть возвращаемых данных относится к записи с самой последней post_date.
Роб Форрест
@RobForrest -> Я не понимаю, почему? Это хорошая идея, чтобы разработать свой ответ и просто выбросить претензии. Насколько я понимаю, данные гарантированно будут связаны, поскольку я использую условие where для фильтрации связанных данных.
guykaplan
1
В какой-то степени вы совершенно правы, каждое из 4 полей, которые вы выбираете, будет относиться к этой максимальной дате post_date, но это не отвечает на заданный вопрос. Например, если вы добавили post_id или содержимое сообщения, то эти столбцы не обязательно будут иметь ту же запись, что и максимальная дата. Чтобы ваш запрос возвратил остальные детали поста, вам нужно будет выполнить второй запрос. Если бы вопрос был о поиске даты самой последней записи, тогда да, вы ответили бы хорошо.
Роб Форрест
@guykaplan, подзапросы не медленные. Размер набора данных не имеет значения. Это зависит от того, как вы используете это. См. Percona.com/blog/2010/03/18/when-the-subselect-runs-faster
Pacerier,
@Pacerier: статья действительно показывает, как вы можете получить выигрыш в производительности от подзапросов, но мне бы очень хотелось, чтобы вы преобразовали данный сценарий для повышения производительности. и Размер данных важен, опять же в данной статье, которую вы опубликовали, вы предполагаете, что есть только одна таблица для работы. размер данных не по размеру строки, а по размеру сложности. сказав, что, если вы работаете с действительно большой таблицей (не так много таблиц), подзапрос может работать намного лучше.
guykaplan
-4

Во-первых, не используйте * в select, это влияет на их производительность и затрудняет использование группы по порядку. Попробуйте этот запрос:

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, просто псевдоним, они упорядочат результат выбора.

Бруно Нардини
источник
Игнорируйте select *, они для краткости в этом примере. Ваш ответ точно такой же, как и в первом примере, который я дал.
Роб Форрест
Псевдоним не влияет на возвращаемую строку и сортировку результатов.
Роб Форрест