Отримайте найпопулярніші n записи для кожної групи згрупованих результатів


140

Нижче наведено найпростіший можливий приклад, хоча будь-яке рішення повинно бути здатне масштабувати до того, як потрібно багато 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 / ...
Edye Chan

Відповіді:


88

Ось один із способів зробити це, використовуючи 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/

Редагувати:

Це може працювати і для вас, воно створює номер рядка для кожного запису. Використовуючи приклад із посилання вище, це поверне лише ті записи з номером рядків менше або рівним 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;

Див. Демо


52
якби у нього 1 000+ груп, чи не зробить це трохи страшно?
Чарльз Форест

1
@CharlesForest так, так і саме тому я заявив, що вам доведеться вказати це для більш ніж двох груп. Це стало б потворно.
Taryn

1
@CharlesForest Я думаю, що я знайшов краще рішення, дивіться свою редакцію
Taryn

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

1
@GordonLinoff Оновив мою відповідь, дякую, що вказав на неї. Також мені знадобилося занадто багато часу, щоб оновити його.
Taryn

63

В інших базах даних ви можете це зробити, використовуючи 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 йому. Однак ця відповідь має дві невеликі переваги:

  1. Це єдиний запит. Змінні ініціалізуються всередині оператора SELECT.
  2. Він обробляє зв’язки, як описано у запитанні (в алфавітному порядку за назвою).

Тож я залишу його тут, якщо він може комусь допомогти.


1
Марк - це для нас добре. Дякуємо за надання ще однієї гарної альтернативи компліменту @ bluefeet's - дуже вдячний.
Ярін

+1. Це працювало для мене. Дійсно чистий і до точної відповіді. Чи можете ви поясніть, як саме це працює? У чому полягає логіка цього?
Aditya Hajare

3
Приємне рішення, але, здається, він не працює в моєму середовищі (MySQL 5.6), оскільки порядок за умовами застосовується після вибору, щоб він не повернув верхній результат, дивіться моє альтернативне рішення, щоб виправити цю проблему
Laurent PELE

Під час роботи я зміг видалити 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

DEMO


6
нюхальний зал з нізвідки найпростішим рішенням! Це більш елегантно, ніж у Людо / Білла Карвіна ? Чи можу я отримати коментар
Ярін

Гм, не впевнений, чи витонченіший він. Але, судячи з голосів, я вважаю, що синій кінець може мати краще рішення.
snuffn

2
У цьому проблема. Якщо в групі є нічия для другого місця, повертається лише один верхній результат. Дивіться модифіковану демонстрацію
Ярін

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      

@ Людо. Щойно побачив цю відповідь від Білла Карвіна - дякую, що застосував її тут
Ярін

Що ви думаєте про відповідь Снуффіна? Я намагаюся порівняти двох
Ярін

2
У цьому проблема. Якщо в групі є нічия за друге місце, повертається лише один найкращий результат - Дивіться демонстрацію
Ярін

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

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

12

Рішення Snuffin здається досить повільним для виконання, коли у вас є багато рядків, і рішення Марка Байєра / Ріка Джеймса та Bluefeet не працює в моєму середовищі (MySQL 5.6), оскільки порядок застосовується після виконання вибору, тому ось варіант рішення Марка Байєрса / Ріка Джеймса, щоб виправити цю проблему (з додатковим мастильним вибором):

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ігноруванню.
Рік Джеймс

Я провів аналогічний запит по таблиці, що містить кілька тисяч рядків, і потрібно було 60 секунд, щоб повернути один результат, так що ... дякую за пост, це для мене початок. (ETA: до 5 секунд. Добре!)
Еван

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


5
Людина, інші знайшли набагато простіші рішення ... Я просто витратив на це 15 хвилин і був неймовірно гордий за себе, що також придумав таке складне рішення. Це смокче.
Travesty3

Мені довелося знайти внутрішній номер версії, який був на 1 менший від поточного - це дало мені відповідь на це: max(internal_version - 1)- так стрес менше :)
Джеймі Стросс

8

Якщо інші відповіді недостатньо швидкі, спробуйте цей код :

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 - я вважаю, що це зручно для експериментів з lat / 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

1
@iedmrc - "GA" означає "Загальнодоступний". Це технологічно говорити "готовий до прайм-тайму" або "звільнений". Вони розробляють версію і зосереджуватимуться на помилках, які вони пропустили. У цьому посиланні обговорюється реалізація MySQL 8.0, яка може відрізнятися від впровадження MariaDB 10.2.
Рік Джеймс

-1

На 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_rank + 1, 1) У цьому коді, якщо @current_country такий самий, ми збільшуємо ранг, інакше встановіть його на 1. Для першого ряду @current_country - NULL, значить, ранг також встановлено на 1.

Для правильного ранжирування нам потрібно ЗАМОВИТИ ЗА країною, населенням DESC


Ну, це принцип, який використовують рішення Марка Байєрса, Ріка Джеймса та мого.
Лоран PELE

Складно сказати, який пост (Переповнення стека або SQLlines) був першим
Лоран PELE

@LaurentPELE - Моя була опублікована лютого 2015 року. Я не бачу часової позначки або імені на SQLlines. Блоги MySQL існують досить довго, що деякі з них застаріли, і їх слід видалити - люди цитують неправильну інформацію.
Рік Джеймс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.