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

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

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

+--------+-------+-----+
| Person | Group | Age |
+--------+-------+-----+
| Bob    | 1     | 32  |
| Jill   | 1     | 34  |
| Shawn  | 1     | 42  |
| Jake   | 2     | 29  |
| Paul   | 2     | 36  |
| Laura  | 2     | 39  |
+--------+-------+-----+

Желаемый результирующий набор:

+--------+-------+-----+
| Shawn  | 1     | 42  |
| Jill   | 1     | 34  |
| Laura  | 2     | 39  |
| Paul   | 2     | 36  |
+--------+-------+-----+

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

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

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

Ответы

Ответ 1

Вот один из способов сделать это, используя UNION ALL (см. SQL Fiddle with Demo). Это работает с двумя группами, если у вас более двух групп, вам нужно указать номер 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/

Edit:

Это может сработать и для вас, оно генерирует номер строки для каждой записи. Используя пример из ссылки выше, будут возвращены только те записи с номером строки меньше или равным 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;

См. Демо

Ответ 2

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


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

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

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

Ответ 3

Попробуйте следующее:

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

DEMO

Ответ 4

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

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 для удобства.

Edit

Следя за комментариями OP относительно недостающих результатов связи, я увеличил ответ на откат, чтобы показать все связи. Это означает, что если последние являются связями, может быть возвращено более 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      

Ответ 5

Проверьте это:

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 Fiddle: http://sqlfiddle.com/#!2/cdbb6/15

Ответ 6

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

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 | ...

Ответ 7

Решение 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 секунды

Ответ 8

Я хотел бы поделиться этим, потому что я долго искал простой способ реализовать это в 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", и он будет работать! Вы также можете добавить переменные в операторную группу для получения большего количества столбцов на выходе. Так что это всего лишь способ, которым я нашел, что он довольно гибкий и работает хорошо, если вы в порядке, просто перечисляя результаты.

Ответ 9

В bigquery следующий запрос работал у меня:

SELECT
  *
FROM (
  SELECT
    *,
    ROW_NUMBER() OVER (PARTITION BY Group ORDER BY Age desc) rownum
  FROM
    mytable)
WHERE
  rownum <= 2

Ответ 10

В 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: = страна Этот код выполняется для каждой строки и сохраняет значение столбца страны переменной @current_country.

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

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

Ответ 11

В 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