Uzyskaj n najlepszych rekordów dla każdej grupy zgrupowanych wyników

140

Poniżej przedstawiono najprostszy możliwy przykład, chociaż każde rozwiązanie powinno być w stanie skalować do dowolnej liczby n najlepszych wyników:

Biorąc pod uwagę poniższą tabelę z kolumnami osoby, grupy i wieku, w jaki sposób można uzyskać 2 najstarsze osoby w każdej grupie? (Remisy w grupach nie powinny dawać więcej wyników, ale należy podać pierwsze 2 w kolejności alfabetycznej)

+ -------- + ------- + ----- +
| Osoba | Grupa | Wiek |
+ -------- + ------- + ----- +
| Bob | 1 | 32 |
| Jill | 1 | 34 |
| Shawn | 1 | 42 |
| Jake | 2 | 29 |
| Paul | 2 | 36 |
| Laura | 2 | 39 |
+ -------- + ------- + ----- +

Pożądany zestaw wyników:

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

UWAGA: To pytanie opiera się na poprzednim - Uzyskaj rekordy z maksymalną wartością dla każdej grupy zgrupowanych wyników SQL - aby uzyskać pojedynczy górny wiersz z każdej grupy i który otrzymał świetną odpowiedź specyficzną dla MySQL od @Bohemian:

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

Chciałbym móc to zbudować, chociaż nie wiem jak.

Yarin
źródło
2
Sprawdź ten przykład. Jest prawie zbliżony do tego, o co prosisz: stackoverflow.com/questions/1537606/ ...
Savas Vedova
Używasz LIMIT w GROUP BY, aby uzyskać N wyników na grupę? stackoverflow.com/questions/2129693/…
Edye Chan

Odpowiedzi:

88

Oto jeden ze sposobów, aby to zrobić, używając UNION ALL(Zobacz SQL Fiddle with Demo ). Działa to z dwiema grupami, jeśli masz więcej niż dwie grupy, musisz określić groupliczbę i dodać zapytania dla każdej 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
)

Można to zrobić na wiele sposobów, zapoznaj się z tym artykułem, aby określić najlepszą trasę w swojej sytuacji:

http://www.xaprb.com/blog/2006/12/07/how-to-select-the-firstleastmax-row-per-group-in-sql/

Edytować:

To może zadziałać również dla Ciebie, generuje numer wiersza dla każdego rekordu. Korzystając z przykładu z linku powyżej, zwróci to tylko te rekordy z numerem wiersza mniejszym lub równym 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;

Zobacz Demo

Taryn
źródło
52
gdyby miał ponad 1000 grup, czy nie byłoby to trochę przerażające?
Charles Forest
1
@CharlesForest tak, byłoby i dlatego stwierdziłem, że musisz określić to dla więcej niż dwóch grup. Stanie się brzydki.
Taryn
1
@CharlesForest Myślę, że znalazłem lepsze rozwiązanie, zobacz moją edycję
Taryn
1
Uwaga dla każdego, kto to czyta: Wersja zawierająca zmienne jest bliska poprawności. Jednak MySQL nie gwarantuje kolejności oceny wyrażeń w SELECT(i, w rzeczywistości, czasami ocenia je poza kolejnością). Kluczem do rozwiązania jest umieszczenie wszystkich przypisań zmiennych w jednym wyrażeniu; oto przykład: stackoverflow.com/questions/38535020/… .
Gordon Linoff,
1
@GordonLinoff Zaktualizowałem moją odpowiedź, dziękuję za wskazanie. Aktualizacja zajęła mi też zbyt dużo czasu.
Taryn
63

W innych bazach danych możesz to zrobić za pomocą ROW_NUMBER. MySQL nie obsługuje, ROW_NUMBERale możesz użyć zmiennych, aby go emulować:

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

Zobacz, jak działa online: sqlfiddle


Edytuj Właśnie zauważyłem, że bluefeet opublikował bardzo podobną odpowiedź: +1 do niego. Jednak ta odpowiedź ma dwie małe zalety:

  1. To jest pojedyncze zapytanie. Zmienne są inicjowane wewnątrz instrukcji SELECT.
  2. Obsługuje remisy zgodnie z opisem w pytaniu (kolejność alfabetyczna według nazwy).

Więc zostawię to tutaj na wypadek, gdyby mogło to komuś pomóc.

Mark Byers
źródło
1
Mark: To działa dobrze dla nas. Dzięki za udostępnienie kolejnej dobrej alternatywy dla komplementu @ bluefeet's - bardzo docenione.
Yarin
+1. To zadziałało dla mnie. Naprawdę czysty i na temat odpowiedź. Czy możesz wyjaśnić, jak dokładnie to działa? Jaka logika się za tym kryje?
Aditya Hajare
3
Niezłe rozwiązanie, ale wygląda na to, że nie działa w moim środowisku (MySQL 5.6), ponieważ klauzula order by jest stosowana po wybraniu, więc nie zwraca najwyższego wyniku, zobacz moje alternatywne rozwiązanie, aby rozwiązać ten problem
Laurent PELE
Podczas uruchamiania udało mi się usunąć JOIN (SELECT @prev := NULL, @rn := 0) AS vars. Rozumiem, że chodzi o zadeklarowanie pustych zmiennych, ale wydaje się to obce dla MySql.
Joseph Cho
1
Działa to świetnie w MySQL 5.7, ale byłoby wspaniale, gdyby ktoś mógł wyjaśnić, jak to działa
George B
41

Spróbuj tego:

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

PRÓBNY

tabaka
źródło
6
tabaka pojawiająca się znikąd z najprostszym rozwiązaniem! Czy to jest bardziej eleganckie niż Ludo / Bill Karwin ? Czy mogę dostać jakiś komentarz
Yarin
Hm, nie jestem pewien, czy jest bardziej elegancki. Ale sądząc po głosowaniach, wydaje mi się, że bluefeet może mieć lepsze rozwiązanie.
snuffn
2
Jest z tym problem. Jeśli jest remis na drugim miejscu w grupie, zwracany jest tylko jeden najlepszy wynik. Zobacz zmodyfikowane demo
Yarin
2
Nie stanowi problemu, jeśli jest to pożądane. Możesz ustawić kolejność a.person.
Alberto Leal
nie, w moim przypadku nie działa, podobnie jak DEMO
Choix
31

Co powiesz na samodzielne łączenie:

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;

daje mi:

a.person    a.groupname  a.age     
----------  -----------  ----------
Shawn       1            42        
Jill        1            34        
Laura       2            39        
Paul        2            36      

Bardzo zainspirowała mnie odpowiedź Billa Karwina na wybór 10 najlepszych rekordów w każdej kategorii

Używam również SQLite, ale to powinno działać na MySQL.

Inna sprawa: w powyższym dla wygody wymieniłem groupkolumnę na groupnamekolumnę.

Edycja :

Kontynuując komentarz OP dotyczący brakujących wyników remisu, zwiększyłem odpowiedź snuffina, aby pokazać wszystkie remisy. Oznacza to, że jeśli ostatnie są remisami, można zwrócić więcej niż 2 wiersze, jak pokazano poniżej:

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

daje mi:

person      groupname   age       
----------  ----------  ----------
Shawn       1           42        
Jill        1           34        
Laura       2           39        
Paul        2           36        
Joe         2           36        
Chuck       3           112      
Społeczność
źródło
@ Ludo - Właśnie zobaczyłem tę odpowiedź od Billa Karwina - dzięki za zastosowanie jej tutaj
Yarin
Co myślisz o odpowiedzi Snuffina? Próbuję porównać oba
Yarin
2
Jest z tym problem. Jeśli jest remis na drugim miejscu w grupie, zwracany jest tylko jeden najlepszy wynik - Zobacz demo
Yarin
1
@ Ludo - pierwotnym wymaganiem było, aby każda grupa zwracała dokładnie n wyników, a wszelkie remisy były rozstrzygane alfabetycznie
Yarin
Edycja uwzględniająca krawaty nie działa dla mnie. Dostaję ERROR 1242 (21000): Subquery returns more than 1 row, prawdopodobnie z powodu GROUP BY. Kiedy SELECT MINwykonuję tylko podzapytanie, generuje ono trzy wiersze: 34, 39, 112i tam okazuje się, że druga wartość powinna wynosić 36, a nie 39.
verbamour
12

Rozwiązanie Snuffin wydaje się dość powolne do wykonania, gdy masz wiele wierszy, a rozwiązania Mark Byers / Rick James i Bluefeet nie działają w moim środowisku (MySQL 5.6), ponieważ polecenie zlecenie jest stosowane po wykonaniu polecenia select, więc oto wariant rozwiązań Marc Byers / Rick James w celu rozwiązania tego problemu (z dodatkową zawartością wyboru):

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;

Próbowałem podobnego zapytania na tabeli mającej 5 milionów wierszy i zwraca wynik w mniej niż 3 sekundy

Laurent PELE
źródło
3
To jedyne zapytanie, które działa w moim środowisku. Dzięki!
herrherr
3
Dodaj LIMIT 9999999do dowolnej tabeli pochodnej z rozszerzeniem ORDER BY. Może to zapobiec ORDER BYignorowaniu.
Rick James
Wykonałem podobne zapytanie na tabeli zawierającej kilka tysięcy wierszy, a zwrócenie jednego wyniku zajęło 60 sekund, więc ... dzięki za post, to dla mnie początek. (ETA: do 5 sekund. Dobrze!)
Evan
10

Sprawdź to:

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

Travesty3
źródło
5
Człowieku, inni znaleźli znacznie prostsze rozwiązania ... Właśnie spędziłem nad tym jakieś 15 minut i byłem z siebie niesamowicie dumny, że wymyśliłem tak skomplikowane rozwiązanie. To jest do bani.
Travesty3
Musiałem znaleźć wewnętrzny numer wersji, który był o 1 mniejszy niż obecny - to dało mi odpowiedź, aby to zrobić: max(internal_version - 1)- więc mniej stresu :)
Jamie Strauss
8

Jeśli inne odpowiedzi nie są wystarczająco szybkie Wypróbuj ten kod :

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;

Wynik:

+---------------------------+------+------------------+------------+
| 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 | ...
Rick James
źródło
Spojrzałem na twoją witrynę - skąd mam uzyskać źródło danych dla populacji miast? TIA i rgs.
Vérace
maxmind.com/en/worldcities - przydaje mi się do eksperymentowania z wyszukiwaniem lat / lng , zapytaniami, partycjonowaniem itp. Jest wystarczająco duży, aby był interesujący, ale wystarczająco czytelny, aby rozpoznać odpowiedzi. Podzbiór kanadyjski jest przydatny w przypadku tego rodzaju pytań. (Mniej prowincji niż miast w USA).
Rick James,
2

Chciałem się tym podzielić, ponieważ spędziłem dużo czasu szukając łatwego sposobu na zaimplementowanie tego w programie Java, nad którym pracuję. To nie do końca daje wynik, którego szukasz, ale jest blisko. Funkcja o nazwie mysql GROUP_CONCAT()działała bardzo dobrze przy określaniu liczby wyników zwracanych w każdej grupie. Używanie LIMITlub inne fantazyjne sposoby próby zrobienia tego COUNTnie zadziałały dla mnie. Więc jeśli chcesz zaakceptować zmodyfikowane wyjście, jest to świetne rozwiązanie. Powiedzmy, że mam tabelę o nazwie „student” z identyfikatorami uczniów, ich płcią i gpa. Powiedzmy, że chcę uzyskać najwyższe 5 gpa dla każdej płci. Wtedy mogę napisać zapytanie w ten sposób

SELECT sex, SUBSTRING_INDEX(GROUP_CONCAT(cast(gpa AS char ) ORDER BY gpa desc), ',',5) 
AS subcategories FROM student GROUP BY sex;

Zauważ, że parametr „5” mówi mu, ile wpisów ma zostać połączonych w każdym wierszu

A wynik wyglądałby jakoś

+--------+----------------+
| Male   | 4,4,4,4,3.9    |
| Female | 4,4,3.9,3.9,3.8|
+--------+----------------+

Możesz także zmienić ORDER BYzmienną i zamówić je w inny sposób. Więc gdybym miał wiek ucznia, mógłbym zamienić „gpa desc” na „age desc” i to zadziała! Możesz także dodać zmienne do grupy według instrukcji, aby uzyskać więcej kolumn w wynikach. To jest po prostu sposób, który uważam, że jest dość elastyczny i działa dobrze, jeśli nie przeszkadza ci tylko wyświetlanie wyników.

Jon Bown
źródło
0

W SQL Server row_numer()jest potężna funkcja, którą można łatwo uzyskać, jak poniżej

select Person,[group],age
from
(
select * ,row_number() over(partition by [group] order by age desc) rn
from mytable
) t
where rn <= 2
Prakash
źródło
Ponieważ 8.0 i 10.2 to GA, ta odpowiedź staje się rozsądna.
Rick James,
@RickJames co oznacza „bycie GA”? Funkcje okna ( dev.mysql.com/doc/refman/8.0/en/window-functions.html ) bardzo dobrze rozwiązały mój problem.
iedmrc
1
@iedmrc - „GA” oznacza „ogólnie dostępne”. Jest to mowa techniczna, która oznacza „gotowy na najlepszy czas” lub „wydany”. Są w trakcie opracowywania wersji i będą się skupiać na błędzie, który przegapili. To łącze omawia implementację MySQL 8.0, która może być inna niż implementacja MariaDB 10.2.
Rick James
-1

W MySQL jest naprawdę fajna odpowiedź na ten problem - jak uzyskać N wierszy w każdej grupie

Na podstawie rozwiązania w odnośniku, do którego się odwołuje, Twoje zapytanie wyglądałoby tak:

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;

gdzie njest top ni your_tableto nazwa twojego stołu.

Myślę, że wyjaśnienie w dokumencie jest naprawdę jasne. Dla szybkiego odniesienia skopiuję i wkleję tutaj:

Obecnie MySQL nie obsługuje funkcji ROW_NUMBER (), która może przypisać numer kolejny w grupie, ale jako obejście możemy użyć zmiennych sesji MySQL.

Te zmienne nie wymagają deklaracji i mogą być używane w zapytaniu do wykonywania obliczeń i przechowywania wyników pośrednich.

@current_country: = country Ten kod jest wykonywany dla każdego wiersza i przechowuje wartość kolumny country w zmiennej @current_country.

@country_rank: = IF (@current_country = country, @current_rank + 1, 1) W tym kodzie, jeśli @current_country jest takie samo, zwiększamy rangę, w przeciwnym razie ustawiamy ją na 1. Dla pierwszego wiersza @current_country ma wartość NULL, więc ranga jest również ustawiony na 1.

Aby uzyskać poprawny ranking, potrzebujemy ORDER BY kraj, ludność DESC

kovac
źródło
Cóż, jest to zasada stosowana w rozwiązaniach Marca Byersa, Ricka Jamesa i moich.
Laurent PELE
Trudno powiedzieć, który post (przepełnienie stosu lub SQLlines) był pierwszy
Laurent PELE
@LaurentPELE - Mój został opublikowany w lutym 2015 r. Nie widzę znacznika czasu ani nazwy na SQLlines. Blogi MySQL istnieją wystarczająco długo, że niektóre z nich są nieaktualne i powinny zostać usunięte - ludzie cytują błędne informacje.
Rick James,