Używasz LIMIT w ramach GROUP BY, aby uzyskać N wyników na grupę?

385

Następujące zapytanie:

SELECT
year, id, rate
FROM h
WHERE year BETWEEN 2000 AND 2009
AND id IN (SELECT rid FROM table2)
GROUP BY id, year
ORDER BY id, rate DESC

daje:

year    id  rate
2006    p01 8
2003    p01 7.4
2008    p01 6.8
2001    p01 5.9
2007    p01 5.3
2009    p01 4.4
2002    p01 3.9
2004    p01 3.5
2005    p01 2.1
2000    p01 0.8
2001    p02 12.5
2004    p02 12.4
2002    p02 12.2
2003    p02 10.3
2000    p02 8.7
2006    p02 4.6
2007    p02 3.3

Chciałbym tylko 5 najlepszych wyników dla każdego identyfikatora:

2006    p01 8
2003    p01 7.4
2008    p01 6.8
2001    p01 5.9
2007    p01 5.3
2001    p02 12.5
2004    p02 12.4
2002    p02 12.2
2003    p02 10.3
2000    p02 8.7

Czy można to zrobić za pomocą jakiegoś modyfikatora typu LIMIT, który działa w ramach GROUP BY?

Studnie
źródło
10
Można to zrobić w MySQL, ale nie jest to tak proste, jak dodanie LIMITklauzuli. Oto artykuł, który szczegółowo wyjaśnia problem: Jak wybrać pierwszy / najmniejszy / maksymalny wiersz na grupę w SQL To dobry artykuł - wprowadza eleganckie, ale naiwne rozwiązanie problemu „Top N na grupę”, a następnie stopniowo poprawia to.
danben,
WYBIERZ * OD (WYBIERZ rok, identyfikator, stawkę OD GDZIE ROKU MIĘDZY 2000 I 2009 ORAZ ID (WYBIERZ Z tabeli 2) GRUPA według id, rok ZAMÓWIENIE według id, stawka DESC) LIMIT 5
Mixcoatl

Odpowiedzi:

115

Możesz użyć funkcji zagregowanej GROUP_CONCAT, aby uzyskać wszystkie lata w jednej kolumnie, pogrupowane idi uporządkowane według rate:

SELECT   id, GROUP_CONCAT(year ORDER BY rate DESC) grouped_year
FROM     yourtable
GROUP BY id

Wynik:

-----------------------------------------------------------
|  ID | GROUPED_YEAR                                      |
-----------------------------------------------------------
| p01 | 2006,2003,2008,2001,2007,2009,2002,2004,2005,2000 |
| p02 | 2001,2004,2002,2003,2000,2006,2007                |
-----------------------------------------------------------

A potem możesz użyć FIND_IN_SET , która zwraca pozycję pierwszego argumentu wewnątrz drugiego, np.

SELECT FIND_IN_SET('2006', '2006,2003,2008,2001,2007,2009,2002,2004,2005,2000');
1

SELECT FIND_IN_SET('2009', '2006,2003,2008,2001,2007,2009,2002,2004,2005,2000');
6

Używając kombinacji GROUP_CONCATi FIND_IN_SEToraz filtrując według pozycji zwróconej przez find_in_set, możesz użyć tego zapytania, które zwraca tylko pierwsze 5 lat dla każdego identyfikatora:

SELECT
  yourtable.*
FROM
  yourtable INNER JOIN (
    SELECT
      id,
      GROUP_CONCAT(year ORDER BY rate DESC) grouped_year
    FROM
      yourtable
    GROUP BY id) group_max
  ON yourtable.id = group_max.id
     AND FIND_IN_SET(year, grouped_year) BETWEEN 1 AND 5
ORDER BY
  yourtable.id, yourtable.year DESC;

Zobacz skrzypce tutaj .

Należy pamiętać, że jeśli więcej niż jeden wiersz może mieć tę samą stawkę, należy rozważyć użycie GROUP_CONCAT (DISTINCT stawka ORDER BY według stawki) w kolumnie stawki zamiast kolumny roku.

Maksymalna długość ciągu zwracanego przez GROUP_CONCAT jest ograniczona, więc działa to dobrze, jeśli trzeba wybrać kilka rekordów dla każdej grupy.

fthiella
źródło
3
To pięknie występujące, stosunkowo proste i świetne wytłumaczenie; Dziękuję bardzo. Do ostatniego punktu, gdzie można obliczyć rozsądną maksymalną długość, można użyć SET SESSION group_concat_max_len = <maximum length>;W przypadku PO, nie problem (ponieważ domyślnie jest to 1024), ale na przykład group_concat_max_len powinien wynosić co najmniej 25: 4 (maks. długość ciągu roku) + 1 (znak separatora), razy 5 (pierwsze 5 lat). Ciągi są raczej obcinane niż zgłaszane błędy, więc należy uważać na ostrzeżenia takie jak 1054 rows in set, 789 warnings (0.31 sec).
Timothy Johns
Jeśli chcę pobrać dokładnie 2 wiersze zamiast 1 do 5, to czego powinienem użyć FIND_IN_SET(). Próbowałem, FIND_IN_SET() =2ale nie pokazałem wyników zgodnie z oczekiwaniami.
Amogh
FIND_IN_SET MIĘDZY 1 a 5 zajmie pierwsze 5 pozycji zestawu GROUP_CONCAT, jeśli rozmiar jest równy lub większy niż 5. Zatem FIND_IN_SET = 2 zajmie tylko dane z drugą pozycją w Twojej GROUP_CONCAT. Zdobywając 2 rzędy, możesz spróbować MIĘDZY 1 i 2 dla 1. i 2. pozycji, zakładając, że zestaw ma 2 rzędy do podania.
jDub9
To rozwiązanie ma znacznie lepszą wydajność niż Salman w przypadku dużych zestawów danych. Zresztą zrezygnowałem z obu, aby uzyskać tak sprytne rozwiązania. Dzięki!!
tiomno
105

W pierwotnym zapytaniu użyto zmiennych użytkownika i ORDER BYtabel pochodnych; zachowanie obu dziwactw nie jest gwarantowane. Zmieniona odpowiedź w następujący sposób.

W MySQL 5.x można użyć rangi biedaka nad partycją, aby osiągnąć pożądany rezultat. Wystarczy zewnętrznie połączyć tabelę ze sobą i dla każdego wiersza policzyć liczbę wierszy mniejszą od niego. W powyższym przypadku mniejszy wiersz to ten o wyższej stawce:

SELECT t.id, t.rate, t.year, COUNT(l.rate) AS rank
FROM t
LEFT JOIN t AS l ON t.id = l.id AND t.rate < l.rate
GROUP BY t.id, t.rate, t.year
HAVING COUNT(l.rate) < 5
ORDER BY t.id, t.rate DESC, t.year

Demo i wynik :

| id  | rate | year | rank |
|-----|------|------|------|
| p01 |  8.0 | 2006 | 0    |
| p01 |  7.4 | 2003 | 1    |
| p01 |  6.8 | 2008 | 2    |
| p01 |  5.9 | 2001 | 3    |
| p01 |  5.3 | 2007 | 4    |
| p02 | 12.5 | 2001 | 0    |
| p02 | 12.4 | 2004 | 1    |
| p02 | 12.2 | 2002 | 2    |
| p02 | 10.3 | 2003 | 3    |
| p02 |  8.7 | 2000 | 4    |

Pamiętaj, że jeśli stawki były powiązane, na przykład:

100, 90, 90, 80, 80, 80, 70, 60, 50, 40, ...

Powyższe zapytanie zwróci 6 wierszy:

100, 90, 90, 80, 80, 80

Zmień, aby HAVING COUNT(DISTINCT l.rate) < 5uzyskać 8 wierszy:

100, 90, 90, 80, 80, 80, 70, 60

Lub zmień, aby ON t.id = l.id AND (t.rate < l.rate OR (t.rate = l.rate AND t.pri_key > l.pri_key))uzyskać 5 wierszy:

 100, 90, 90, 80, 80

W MySQL 8 lub później po prostu użyć RANK, DENSE_RANKlubROW_NUMBER funkcje:

SELECT *
FROM (
    SELECT *, RANK() OVER (PARTITION BY id ORDER BY rate DESC) AS rnk
    FROM t
) AS x
WHERE rnk <= 5
Salman A.
źródło
7
Myślę, że warto wspomnieć, że kluczową częścią jest ORDER BY id, ponieważ każda zmiana wartości id ponownie rozpocznie liczenie w rankingu.
ruuter
Dlaczego powinienem uruchomić go dwukrotnie, aby uzyskać odpowiedź WHERE rank <=5? Po raz pierwszy nie dostaję 5 wierszy z każdego identyfikatora, ale potem mogę uzyskać, jak powiedziałeś.
Brenno Leal
@BrennoLeal Myślę, że zapominasz SEToświadczenia (patrz pierwsze zapytanie). Czy to jest to konieczne.
Salman A
3
W nowszych wersjach ORDER BYtabela pochodna może i często będzie ignorowana. To pokonuje cel. Wydajne grupowo można znaleźć tutaj .
Rick James
1
+1 przepisywanie twoich odpowiedzi jest bardzo ważne, ponieważ współczesne wersje MySQL / MariaDB są zgodne ze standardami ANSI / ISO SQL 1992/1999/2003 więcej, w których nigdy tak naprawdę nie wolno było ich używać ORDER BYw dostarczanych / takich zapytaniach. To jest powód, dla którego nowoczesne wersje MySQL / MariaDB ignorują ORDER BYpodzapytanie bez użycia LIMIT, wierzę, że normy ANSI / ISO SQL 2008/2011/2016 dopuszczają ORDER BYdostarczanie / podkwerendy, gdy są używane w połączeniu zFETCH FIRST n ROWS ONLY
Raymond Nijland
21

Dla mnie coś takiego

SUBSTRING_INDEX(group_concat(col_name order by desired_col_order_name), ',', N) 

działa świetnie. Bez skomplikowanego zapytania.


na przykład: zdobądź 1 miejsce dla każdej grupy

SELECT 
    *
FROM
    yourtable
WHERE
    id IN (SELECT 
            SUBSTRING_INDEX(GROUP_CONCAT(id
                            ORDER BY rate DESC),
                        ',',
                        1) id
        FROM
            yourtable
        GROUP BY year)
ORDER BY rate DESC;
Vishal Kumar
źródło
Twoje rozwiązanie działało idealnie, ale chcę również pobrać rok i inne kolumny z podzapytania. Jak to zrobić?
Maj
9

Nie, nie możesz OGRANICZAĆ podkwerend arbitralnie (możesz to zrobić w ograniczonym zakresie w nowszych MySQL, ale nie dla 5 wyników na grupę).

Jest to zapytanie grupowe o maksymalnej liczbie, które nie jest trywialne w SQL. Istnieją różne sposoby rozwiązania tego problemu, które mogą być bardziej wydajne w niektórych przypadkach, ale w przypadku top-n ogólnie będziesz chciał spojrzeć na odpowiedź Billa na podobne poprzednie pytanie.

Podobnie jak w przypadku większości rozwiązań tego problemu, może zwrócić więcej niż pięć wierszy, jeśli istnieje wiele wierszy o tej samej ratewartości, więc nadal może być wymagana dodatkowa obróbka, aby to sprawdzić.

Bobin
źródło
9

Wymaga to serii podkwerend do uszeregowania wartości, ograniczenia ich, a następnie wykonania sumy podczas grupowania

@Rnk:=0;
@N:=2;
select
  c.id,
  sum(c.val)
from (
select
  b.id,
  b.bal
from (
select   
  if(@last_id=id,@Rnk+1,1) as Rnk,
  a.id,
  a.val,
  @last_id=id,
from (   
select 
  id,
  val 
from list
order by id,val desc) as a) as b
where b.rnk < @N) as c
group by c.id;
Brian L. Cartwright
źródło
9

Spróbuj tego:

SELECT h.year, h.id, h.rate 
FROM (SELECT h.year, h.id, h.rate, IF(@lastid = (@lastid:=h.id), @index:=@index+1, @index:=0) indx 
      FROM (SELECT h.year, h.id, h.rate 
            FROM h
            WHERE h.year BETWEEN 2000 AND 2009 AND id IN (SELECT rid FROM table2)
            GROUP BY id, h.year
            ORDER BY id, rate DESC
            ) h, (SELECT @lastid:='', @index:=0) AS a
    ) h 
WHERE h.indx <= 5;
Saharsh Shah
źródło
1
nieznana kolumna a. typ na liście pól
anu
5
SELECT year, id, rate
FROM (SELECT
  year, id, rate, row_number() over (partition by id order by rate DESC)
  FROM h
  WHERE year BETWEEN 2000 AND 2009
  AND id IN (SELECT rid FROM table2)
  GROUP BY id, year
  ORDER BY id, rate DESC) as subquery
WHERE row_number <= 5

Podzapytanie jest prawie identyczne z zapytaniem. Tylko zmiana się dodaje

row_number() over (partition by id order by rate DESC)
Ricky Moreno
źródło
8
To miłe, ale MySQL nie ma funkcji okna (jak ROW_NUMBER()).
ypercubeᵀᴹ
3
Począwszy od MySQL 8.0, row_number()jest dostępny .
erickg
4

Zbuduj wirtualne kolumny (jak RowID w Oracle)

stół:

`
CREATE TABLE `stack` 
(`year` int(11) DEFAULT NULL,
`id` varchar(10) DEFAULT NULL,
`rate` float DEFAULT NULL) 
ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
`

dane:

insert into stack values(2006,'p01',8);
insert into stack values(2001,'p01',5.9);
insert into stack values(2007,'p01',5.3);
insert into stack values(2009,'p01',4.4);
insert into stack values(2001,'p02',12.5);
insert into stack values(2004,'p02',12.4);
insert into stack values(2005,'p01',2.1);
insert into stack values(2000,'p01',0.8);
insert into stack values(2002,'p02',12.2);
insert into stack values(2002,'p01',3.9);
insert into stack values(2004,'p01',3.5);
insert into stack values(2003,'p02',10.3);
insert into stack values(2000,'p02',8.7);
insert into stack values(2006,'p02',4.6);
insert into stack values(2007,'p02',3.3);
insert into stack values(2003,'p01',7.4);
insert into stack values(2008,'p01',6.8);

SQL w ten sposób:

select t3.year,t3.id,t3.rate 
from (select t1.*, (select count(*) from stack t2 where t1.rate<=t2.rate and t1.id=t2.id) as rownum from stack t1) t3 
where rownum <=3 order by id,rate DESC;

jeśli usuniesz klauzulę where w t3, będzie to wyglądać następująco:

wprowadź opis zdjęcia tutaj

UZYSKAJ „TOP N Record” -> dodaj „rownum <= 3” w klauzuli where (klauzula where t3);

WYBIERZ „rok” -> dodaj „MIĘDZY 2000 I 2009” w klauzuli where (klauzula where t3);

Wang Wen'an
źródło
Jeśli masz stawki powtarzające się dla tego samego identyfikatora, to nie zadziała, ponieważ liczba rowNum wzrośnie wyżej; nie dostaniesz 3 w rzędzie, możesz dostać 0, 1 lub 2. Czy możesz znaleźć jakieś rozwiązanie tego problemu?
głodujący
@starvator zmień wartość „t1.rate <= t2.rate” na „t1.rate <t2.rate”, jeśli najlepsza stawka ma te same wartości w tym samym identyfikatorze, wszystkie z nich mają tę samą wartość, ale nie wzrośnie wyżej; jak „stopa 8 w id p01”, jeśli się powtarza, używając „t1.rate <t2.rate”, oba z „stawki 8 w id p01” mają to samo rundum 0; jeśli używasz „t1.rate <= t2.rate”, wartość rownum wynosi 2;
Wang Wen'an,
3

Zajęło mi to trochę pracy, ale myślę, że moim rozwiązaniem byłoby udostępnienie, ponieważ wydaje się eleganckie i dość szybkie.

SELECT h.year, h.id, h.rate 
  FROM (
    SELECT id, 
      SUBSTRING_INDEX(GROUP_CONCAT(CONCAT(id, '-', year) ORDER BY rate DESC), ',' , 5) AS l
      FROM h
      WHERE year BETWEEN 2000 AND 2009
      GROUP BY id
      ORDER BY id
  ) AS h_temp
    LEFT JOIN h ON h.id = h_temp.id 
      AND SUBSTRING_INDEX(h_temp.l, CONCAT(h.id, '-', h.year), 1) != h_temp.l

Zauważ, że ten przykład został podany na potrzeby pytania i można go łatwo modyfikować do innych podobnych celów.

Jan
źródło
2

Następujący post: sql: wybieranie rekordu N na grupę opisuje skomplikowany sposób osiągnięcia tego bez podkwerend.

Poprawia inne oferowane tutaj rozwiązania:

  • Robienie wszystkiego w jednym zapytaniu
  • Umiejętność prawidłowego wykorzystania indeksów
  • Unikanie podzapytań, o których wiadomo, że produkuje złe plany wykonania w MySQL

To jednak nie jest ładne. Dobrym rozwiązaniem byłoby osiągnięcie funkcji okna (aka funkcji analitycznych) włączonych w MySQL - ale tak nie jest. Trik zastosowany we wspomnianym poście wykorzystuje GROUP_CONCAT, który jest czasami opisywany jako „Funkcje okna biednego człowieka dla MySQL”.

Shlomi Noach
źródło
1

dla takich jak ja, którzy mieli czas na zapytania. Zrobiłem poniżej, aby użyć limitów i czegokolwiek innego przez określoną grupę.

DELIMITER $$
CREATE PROCEDURE count_limit200()
BEGIN
    DECLARE a INT Default 0;
    DECLARE stop_loop INT Default 0;
    DECLARE domain_val VARCHAR(250);
    DECLARE domain_list CURSOR FOR SELECT DISTINCT domain FROM db.one;

    OPEN domain_list;

    SELECT COUNT(DISTINCT(domain)) INTO stop_loop 
    FROM db.one;
    -- BEGIN LOOP
    loop_thru_domains: LOOP
        FETCH domain_list INTO domain_val;
        SET a=a+1;

        INSERT INTO db.two(book,artist,title,title_count,last_updated) 
        SELECT * FROM 
        (
            SELECT book,artist,title,COUNT(ObjectKey) AS titleCount, NOW() 
            FROM db.one 
            WHERE book = domain_val
            GROUP BY artist,title
            ORDER BY book,titleCount DESC
            LIMIT 200
        ) a ON DUPLICATE KEY UPDATE title_count = titleCount, last_updated = NOW();

        IF a = stop_loop THEN
            LEAVE loop_thru_domain;
        END IF;
    END LOOP loop_thru_domain;
END $$

zapętla listę domen, a następnie wstawia limit tylko 200

Dev-Ria
źródło
1

Spróbuj tego:

SET @num := 0, @type := '';
SELECT `year`, `id`, `rate`,
    @num := if(@type = `id`, @num + 1, 1) AS `row_number`,
    @type := `id` AS `dummy`
FROM (
    SELECT *
    FROM `h`
    WHERE (
        `year` BETWEEN '2000' AND '2009'
        AND `id` IN (SELECT `rid` FROM `table2`) AS `temp_rid`
    )
    ORDER BY `id`
) AS `temph`
GROUP BY `year`, `id`, `rate`
HAVING `row_number`<='5'
ORDER BY `id`, `rate DESC;
MLF
źródło
0

Spróbuj poniżej procedury składowanej. Już zweryfikowałem. Otrzymuję właściwy wynik, ale bez użycia groupby.

CREATE DEFINER=`ks_root`@`%` PROCEDURE `first_five_record_per_id`()
BEGIN
DECLARE query_string text;
DECLARE datasource1 varchar(24);
DECLARE done INT DEFAULT 0;
DECLARE tenants varchar(50);
DECLARE cur1 CURSOR FOR SELECT rid FROM demo1;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;

    SET @query_string='';

      OPEN cur1;
      read_loop: LOOP

      FETCH cur1 INTO tenants ;

      IF done THEN
        LEAVE read_loop;
      END IF;

      SET @datasource1 = tenants;
      SET @query_string = concat(@query_string,'(select * from demo  where `id` = ''',@datasource1,''' order by rate desc LIMIT 5) UNION ALL ');

       END LOOP; 
      close cur1;

    SET @query_string  = TRIM(TRAILING 'UNION ALL' FROM TRIM(@query_string));  
  select @query_string;
PREPARE stmt FROM @query_string;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

END
Himanshu Patel
źródło