Получить первые n записей для каждой группы сгруппированных результатов


147

Ниже приведен простейший пример, хотя любое решение должно иметь возможность масштабирования до любого количества n лучших результатов:

Учитывая таблицу, подобную приведенной ниже, со столбцами «человек», «группа» и «возраст», как бы вы могли получить 2 самых старых человека в каждой группе? (Связи внутри групп не должны приводить к большему количеству результатов, но дают первые 2 в алфавитном порядке)

+ -------- + ------- + ----- +
| Человек | Группа | Возраст |
+ -------- + ------- + ----- +
| Боб | 1 | 32 |
| Джилл | 1 | 34 |
| Шон | 1 | 42 |
| Джейк | 2 | 29 |
| Пол | 2 | 36 |
| Лаура | 2 | 39 |
+ -------- + ------- + ----- +

Желаемый результат:

+ -------- + ------- + ----- +
| Шон | 1 | 42 |
| Джилл | 1 | 34 |
| Лаура | 2 | 39 |
| Пол | 2 | 36 |
+ -------- + ------- + ----- +

ПРИМЕЧАНИЕ. Этот вопрос основан на предыдущем - Получить записи с максимальным значением для каждой группы сгруппированных результатов SQL - для получения одной верхней строки из каждой группы, на который был получен отличный ответ для MySQL от @Bohemian:

select * 
from (select * from mytable order by `Group`, Age desc, Person) x
group by `Group`

Хотел бы иметь возможность строить на этом, хотя я не понимаю, как.



2
Посмотрите этот пример. Это очень близко к тому, о чем вы спрашиваете: stackoverflow.com/questions/1537606/…
— Савас Ведова

Использование LIMIT в GROUP BY для получения N результатов на группу? stackoverflow.com/questions/2129693/…
— Эди Чан

Ответы:


90

Вот один из способов сделать это, используя UNION ALL(См. SQL Fiddle с демонстрацией ). Это работает с двумя группами, если у вас больше двух групп, вам нужно будет указать groupколичество и добавить запросы для каждой group:

(
  select *
  from mytable 
  where `group` = 1
  order by age desc
  LIMIT 2
)
UNION ALL
(
  select *
  from mytable 
  where `group` = 2
  order by age desc
  LIMIT 2
)

Есть множество способов сделать это, см. Эту статью, чтобы определить лучший путь для вашей ситуации:

http://www.xaprb.com/blog/2006/12/07/how-to-select-the-firstleastmax-row-per-group-in-sql/

Редактировать:

Это может сработать и для вас, он генерирует номер строки для каждой записи. Используя пример из приведенной выше ссылки, будут возвращены только те записи, номер строки которых меньше или равен 2:

select person, `group`, age
from 
(
   select person, `group`, age,
      (@num:=if(@group = `group`, @num +1, if(@group := `group`, 1, 1))) row_number 
  from test t
  CROSS JOIN (select @num:=0, @group:=null) c
  order by `Group`, Age desc, person
) as x 
where x.row_number <= 2;

См. Демонстрацию


54
если у него более 1000 групп, разве это не пугает?
— Чарльз Форест,

1
@CharlesForest: да, будет, и именно поэтому я заявил, что вам придется указать это для более чем двух групп. Это стало бы некрасиво.
— Тарин

1
@CharlesForest Я думаю, что нашел лучшее решение, см. Мою правку
— Тарин

1
Примечание для всех, кто это читает: версия переменных близка к правильной. Однако MySQL не гарантирует порядок оценки выражений в SELECT(и, фактически, иногда оценивает их не по порядку). Ключ к решению - поместить все назначения переменных в одно выражение; вот пример: stackoverflow.com/questions/38535020/… .
— Гордон Линофф

1
@GordonLinoff Обновил мой ответ, спасибо, что указали на него. Мне также потребовалось слишком много времени, чтобы обновить его.
— Тарин

65

В других базах данных это можно сделать с помощью ROW_NUMBER. MySQL не поддерживает, ROW_NUMBERно вы можете использовать переменные для его эмуляции:

SELECT
    person,
    groupname,
    age
FROM
(
    SELECT
        person,
        groupname,
        age,
        @rn := IF(@prev = groupname, @rn + 1, 1) AS rn,
        @prev := groupname
    FROM mytable
    JOIN (SELECT @prev := NULL, @rn := 0) AS vars
    ORDER BY groupname, age DESC, person
) AS T1
WHERE rn <= 2

Посмотрите, как это работает онлайн: sqlfiddle


Edit Я только что заметил, что bluefeet отправил очень похожий ответ: +1 ему. Однако у этого ответа есть два небольших преимущества:

  1. Это единый запрос. Переменные инициализируются внутри оператора SELECT.
  2. Он обрабатывает связи, как описано в вопросе (в алфавитном порядке по имени).

Так что я оставлю его здесь на случай, если он кому-то поможет.


2
Марк: Это хорошо работает для нас. Спасибо, что предоставили еще одну хорошую альтернативу комплименту @ bluefeet - очень признательны.
— Ярин

+1. Это сработало для меня. Действительно чистый и точный ответ. Не могли бы вы объяснить, как именно это работает? Какая логика за этим?
— Адитья Хаджаре

3
Хорошее решение, но похоже, что оно не работает в моей среде (MySQL 5.6), потому что предложение order by применяется после выбора, поэтому оно не возвращает лучший результат, см. Мое альтернативное решение для устранения этой проблемы
— Laurent

При запуске мне удалось удалить JOIN (SELECT @prev := NULL, @rn := 0) AS vars. Я понимаю, что нужно объявить пустые переменные, но для MySql это кажется лишним.
— Джозеф Чо

1
Это отлично работает для меня в MySQL 5.7, но было бы здорово, если бы кто-нибудь мог объяснить, как это работает
— Джордж Б.

41

Попробуй это:

SELECT a.person, a.group, a.age FROM person AS a WHERE 
(SELECT COUNT(*) FROM person AS b 
WHERE b.group = a.group AND b.age >= a.age) <= 2 
ORDER BY a.group ASC, a.age DESC

ДЕМО


6
табак появляется из ниоткуда с помощью самого простого решения! Это более элегантно, чем у Людо / Билла Карвина ? Могу я получить комментарий
— Ярин

Хм, не уверен, что он элегантнее. Но, судя по голосам, я думаю, что у bluefeet могло быть лучшее решение.
— snuffn

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

2
При желании это не проблема. Вы можете установить порядок a.person.
— Альберто Леаль

нет, в моем случае это не работает, как и DEMO не работает
— Choix

31

Как насчет использования самосоединения:

CREATE TABLE mytable (person, groupname, age);
INSERT INTO mytable VALUES('Bob',1,32);
INSERT INTO mytable VALUES('Jill',1,34);
INSERT INTO mytable VALUES('Shawn',1,42);
INSERT INTO mytable VALUES('Jake',2,29);
INSERT INTO mytable VALUES('Paul',2,36);
INSERT INTO mytable VALUES('Laura',2,39);

SELECT a.* FROM mytable AS a
  LEFT JOIN mytable AS a2 
    ON a.groupname = a2.groupname AND a.age <= a2.age
GROUP BY a.person
HAVING COUNT(*) <= 2
ORDER BY a.groupname, a.age DESC;

дает мне:

a.person    a.groupname  a.age     
----------  -----------  ----------
Shawn       1            42        
Jill        1            34        
Laura       2            39        
Paul        2            36      

Меня сильно вдохновил ответ Билла Карвина на выбор 10 лучших рекордов для каждой категории.

Кроме того, я использую SQLite, но это должно работать в MySQL.

Другое дело: в приведенном выше я заменил groupстолбец на groupnameстолбец для удобства.

Редактировать :

Следуя за комментарием ОП относительно недостающих результатов ничьей, я увеличил ответ Снаффина, чтобы показать все связи. Это означает, что если последние являются связями, может быть возвращено более 2 строк, как показано ниже:

.headers on
.mode column

CREATE TABLE foo (person, groupname, age);
INSERT INTO foo VALUES('Paul',2,36);
INSERT INTO foo VALUES('Laura',2,39);
INSERT INTO foo VALUES('Joe',2,36);
INSERT INTO foo VALUES('Bob',1,32);
INSERT INTO foo VALUES('Jill',1,34);
INSERT INTO foo VALUES('Shawn',1,42);
INSERT INTO foo VALUES('Jake',2,29);
INSERT INTO foo VALUES('James',2,15);
INSERT INTO foo VALUES('Fred',1,12);
INSERT INTO foo VALUES('Chuck',3,112);


SELECT a.person, a.groupname, a.age 
FROM foo AS a 
WHERE a.age >= (SELECT MIN(b.age)
                FROM foo AS b 
                WHERE (SELECT COUNT(*)
                       FROM foo AS c
                       WHERE c.groupname = b.groupname AND c.age >= b.age) <= 2
                GROUP BY b.groupname)
ORDER BY a.groupname ASC, a.age DESC;

дает мне:

person      groupname   age       
----------  ----------  ----------
Shawn       1           42        
Jill        1           34        
Laura       2           39        
Paul        2           36        
Joe         2           36        
Chuck       3           112      

@ Ludo - Только что видел этот ответ Билла Карвина - спасибо, что применили его здесь
— Ярин

Что вы думаете об ответе Снаффина? Я пытаюсь сравнить двух
— Ярин

2
С этим проблема. Если в группе ничья за второе место, возвращается только один лучший результат - см. Демо
— Yarin

1
@ Ludo - исходное требование заключалось в том, чтобы каждая группа возвращала точные n результатов, а любые связи разрешались в алфавитном порядке
— Ярин

Редактирование для включения галстуков у меня не работает. Я получаю ERROR 1242 (21000): Subquery returns more than 1 row, предположительно, из-за файла GROUP BY. Когда я выполняю только SELECT MINподзапрос, он генерирует три строки: 34, 39, 112и оказывается, что второе значение должно быть 36, а не 39.
— verbamour

14

Решение Snuffin кажется довольно медленным для выполнения, когда у вас много строк, а решения Mark Byers / Rick James и Bluefeet не работают в моей среде (MySQL 5.6), потому что порядок по применяется после выполнения select, поэтому вот вариант решений Marc Byers / Rick James для устранения этой проблемы (с дополнительным фрагментированным выбором):

select person, groupname, age
from
(
    select person, groupname, age,
    (@rn:=if(@prev = groupname, @rn +1, 1)) as rownumb,
    @prev:= groupname 
    from 
    (
        select person, groupname, age
        from persons 
        order by groupname ,  age desc, person
    )   as sortedlist
    JOIN (select @prev:=NULL, @rn :=0) as vars
) as groupedlist 
where rownumb<=2
order by groupname ,  age desc, person;

Я пробовал аналогичный запрос в таблице с 5 миллионами строк, и он возвращает результат менее чем за 3 секунды.


3
Это единственный запрос, который работал в моей среде. Благодарность!
— herrherr

3
Добавить LIMIT 9999999в любую производную таблицу с расширением ORDER BY. Это может предотвратить ORDER BYигнорирование.
— Rick James

Я выполнил аналогичный запрос к таблице, содержащей несколько тысяч строк, и потребовалось 60 секунд, чтобы вернуть один результат, так что ... спасибо за сообщение, это начало для меня. (Расчетное время прибытия: уменьшено до 5 секунд. Хорошо!)
— Эван

Это запрос, который идеально подходит для заказа. Ответы ниже не работают. Спасибо
— emmanuel sio

10

Проверь это:

SELECT
  p.Person,
  p.`Group`,
  p.Age
FROM
  people p
  INNER JOIN
  (
    SELECT MAX(Age) AS Age, `Group` FROM people GROUP BY `Group`
    UNION
    SELECT MAX(p3.Age) AS Age, p3.`Group` FROM people p3 INNER JOIN (SELECT MAX(Age) AS Age, `Group` FROM people GROUP BY `Group`) p4 ON p3.Age < p4.Age AND p3.`Group` = p4.`Group` GROUP BY `Group`
  ) p2 ON p.Age = p2.Age AND p.`Group` = p2.`Group`
ORDER BY
  `Group`,
  Age DESC,
  Person;

Скрипка SQL: http://sqlfiddle.com/#!2/cdbb6/15


5
Черт, другие находили гораздо более простые решения ... Я потратил на это около 15 минут и был невероятно горд тем, что придумал такое сложное решение. Это отстой.
— Travesty3

Мне нужно было найти внутренний номер версии, который был на 1 меньше, чем текущий - это дало мне ответ: max(internal_version - 1)- Так что меньше стресса :)
— Джейми Штраус

9

Если другие ответы недостаточно быстры, попробуйте этот код :

SELECT
        province, n, city, population
    FROM
      ( SELECT  @prev := '', @n := 0 ) init
    JOIN
      ( SELECT  @n := if(province != @prev, 1, @n + 1) AS n,
                @prev := province,
                province, city, population
            FROM  Canada
            ORDER BY
                province   ASC,
                population DESC
      ) x
    WHERE  n <= 3
    ORDER BY  province, n;

Выход:

+---------------------------+------+------------------+------------+
| province                  | n    | city             | population |
+---------------------------+------+------------------+------------+
| Alberta                   |    1 | Calgary          |     968475 |
| Alberta                   |    2 | Edmonton         |     822319 |
| Alberta                   |    3 | Red Deer         |      73595 |
| British Columbia          |    1 | Vancouver        |    1837970 |
| British Columbia          |    2 | Victoria         |     289625 |
| British Columbia          |    3 | Abbotsford       |     151685 |
| Manitoba                  |    1 | ...

Посмотрел ваш сайт - где взять источник данных о населении городов? TIA и rgs.
— Vérace

maxmind.com/en/worldcities - я считаю , это удобно для экспериментов с лат поисков / LNG , запросы, секционирования и т.д. Это достаточно большой , чтобы быть интересным, но читаемый достаточно , чтобы распознать ответы. Канадская подгруппа удобна для такого рода вопросов. (Меньше провинций, чем городов США.)
— Рик Джеймс,

2

Я хотел поделиться этим, потому что я долго искал простой способ реализовать это в java-программе, над которой я работаю. Это не совсем дает результат, который вы ищете, но он близок. Вызываемая функция в mysql GROUP_CONCAT()очень хорошо работала для определения количества результатов, возвращаемых в каждой группе. Использование LIMITили любые другие причудливые способы попытаться сделать это COUNTдля меня не сработали. Так что, если вы готовы принять измененный вывод, это отличное решение. Допустим, у меня есть таблица под названием «студент» с идентификаторами учащихся, их полом и GPA. Допустим, я хочу набрать 5 лучших баллов для каждого пола. Тогда я могу написать такой запрос

SELECT sex, SUBSTRING_INDEX(GROUP_CONCAT(cast(gpa AS char ) ORDER BY gpa desc), ',',5) 
AS subcategories FROM student GROUP BY sex;

Обратите внимание, что параметр '5' указывает, сколько записей объединить в каждую строку.

И результат будет выглядеть примерно так

+--------+----------------+
| Male   | 4,4,4,4,3.9    |
| Female | 4,4,3.9,3.9,3.8|
+--------+----------------+

Вы также можете изменить ORDER BYпеременные и заказать их другим способом. Так что, если бы у меня был возраст студента, я мог бы заменить «gpa desc» на «age desc», и это сработает! Вы также можете добавить переменные в группу по оператору, чтобы получить больше столбцов в выводе. Я обнаружил, что это довольно гибкий способ, который хорошо работает, если вас устраивает просто перечисление результатов.


0

В SQL Server row_numer()есть мощная функция, которая может легко получить результат, как показано ниже

select Person,[group],age
from
(
select * ,row_number() over(partition by [group] order by age desc) rn
from mytable
) t
where rn <= 2

Поскольку 8.0 и 10.2 являются GA, этот ответ становится разумным.
— Рик Джеймс

@RickJames, что значит «быть GA»? Оконные функции ( dev.mysql.com/doc/refman/8.0/en/window-functions.html ) очень хорошо решили мою проблему.
— iedmrc 01

1
@iedmrc - «GA» означает «общедоступный». Это технический язык для «готов к прайм-тайм» или «выпущен». Они разрабатывают версию и сосредоточат внимание на пропущенной ими ошибке. В этой ссылке обсуждается реализация MySQL 8.0, которая может отличаться от реализации MariaDB 10.2.
— Рик Джеймс

0

В MySQL есть действительно хороший ответ на эту проблему - как получить первые N строк для каждой группы

На основе решения в указанной ссылке ваш запрос будет выглядеть следующим образом:

SELECT Person, Group, Age
   FROM
     (SELECT Person, Group, Age, 
                  @group_rank := IF(@group = Group, @group_rank + 1, 1) AS group_rank,
                  @current_group := Group 
       FROM `your_table`
       ORDER BY Group, Age DESC
     ) ranked
   WHERE group_rank <= `n`
   ORDER BY Group, Age DESC;

где nесть top nи your_tableэто имя вашей таблицы.

Я думаю, что объяснение в ссылке действительно ясное. Для быстрого ознакомления я скопирую и вставлю его сюда:

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

Эти переменные не требуют объявления и могут использоваться в запросе для выполнения вычислений и хранения промежуточных результатов.

@current_country: = country Этот код выполняется для каждой строки и сохраняет значение столбца страны в переменной @current_country.

@country_rank: = IF (@current_country = country, @country_rank + 1, 1) В этом коде, если @current_country совпадает, мы увеличиваем рейтинг, в противном случае устанавливаем его на 1. Для первой строки @current_country имеет значение NULL, поэтому рейтинг равен также установлен в 1.

Для правильного ранжирования нам необходимо иметь ORDER BY country, Population DESC


Что ж, это принцип, используемый в решениях Марка Байерса, Рика Джеймса и меня.
— Laurent PELE

Сложно сказать, какой пост (Stack Overflow или SQLlines) был первым,
— Лоран ПЕЛЕ

@LaurentPELE - Моя была опубликована в феврале 2015 года. Я не вижу ни метки времени, ни имени в строках SQL. Блоги MySQL существуют достаточно давно, поэтому некоторые из них устарели и должны быть удалены - люди цитируют неверную информацию.
— Рик Джеймс

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.