Отримайте найпопулярніші 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.