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