Получить первые 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

Травести3
источник
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 существуют достаточно давно, поэтому некоторые из них устарели и должны быть удалены - люди цитируют неверную информацию.
Рик Джеймс
0
SELECT
p1.Person,
p1.`GROUP`,
p1.Age  
   FROM
person AS p1 
 WHERE
(
SELECT
    COUNT( DISTINCT ( p2.age ) ) 
FROM
    person AS p2 
WHERE
    p2.`GROUP` = p1.`GROUP` 
    AND p2.Age >= p1.Age 
) < 2 
ORDER BY
p1.`GROUP` ASC,
p1.age DESC

ссылка leetcode

байт мамба
источник