web-dev-qa-db-fra.com

Utiliser LIMIT dans GROUP BY pour obtenir N résultats par groupe?

La requête suivante:

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

rendements:

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

Ce que j'aimerais, c'est seulement les 5 meilleurs résultats pour chaque identifiant:

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

Y a-t-il un moyen de faire cela en utilisant un type de modificateur LIMIT qui fonctionne dans GROUP BY?

348
Wells

Vous pouvez utiliser la fonction agrégée GROUP_CONCAT pour rassembler toutes les années dans une seule colonne, regroupée par id et ordonnée par rate:

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

Résultat:

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

Et ensuite, vous pourriez utiliser FIND_IN_SET , qui renvoie la position du premier argument dans le second, par exemple.

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

En combinant GROUP_CONCAT et FIND_IN_SET, et en filtrant selon la position renvoyée par find_in_set, vous pouvez ensuite utiliser cette requête qui renvoie uniquement les 5 premières années pour chaque identifiant:

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;

S'il vous plaît voir le violon ici .

Veuillez noter que si plusieurs lignes peuvent avoir le même taux, vous devriez envisager d’utiliser GROUP_CONCAT (taux de taux DISTINCT ORDER BY) dans la colonne taux au lieu de la colonne année.

La longueur maximale de la chaîne renvoyée par GROUP_CONCAT est limitée. Cela fonctionne donc bien si vous devez sélectionner quelques enregistrements pour chaque groupe.

101
fthiella

Les requête d'origine variables utilisateur utilisées et _ORDER BY_ sur les tables dérivées; le comportement des deux bizarreries n'est pas garanti. Réponse révisée comme suit.

Dans MySQL 5.x, vous pouvez utiliser le rang d'un homme pauvre sur une partition pour obtenir le résultat souhaité. Juste externe joindre la table avec lui-même et pour chaque ligne, compter le nombre de lignes inférieur que cela . Dans le cas ci-dessus, la rangée inférieure est celle avec le taux le plus élevé:

_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
_

démo et résultat :

_| 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    |
_

Notez que si les taux avaient des liens, par exemple:

_100, 90, 90, 80, 80, 80, 70, 60, 50, 40, ...
_

La requête ci-dessus renverra 6 lignes:

_100, 90, 90, 80, 80, 80
_

Passez à HAVING COUNT(DISTINCT l.rate) < 5 pour obtenir 8 lignes:

_100, 90, 90, 80, 80, 80, 70, 60
_

Ou passez à ON t.id = l.id AND (t.rate < l.rate OR (t.rate = l.rate AND t.pri_key > l.pri_key)) pour obtenir 5 lignes:

_ 100, 90, 90, 80, 80
_

Dans MySQL version 8 ou ultérieure, utilisez simplement les fonctions (RANK, _DENSE_RANK_ OU _ROW_NUMBER ):

_SELECT *
FROM (
    SELECT *, RANK() OVER (PARTITION BY id ORDER BY rate DESC) AS rnk
    FROM t
) AS x
WHERE rnk <= 5
_
86
Salman A

Pour moi quelque chose comme

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

fonctionne parfaitement. Pas de requête compliquée.


par exemple: top 1 pour chaque groupe

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;
20
Vishal Kumar

Non, vous ne pouvez pas LIMITER les sous-requêtes de manière arbitraire (vous pouvez le faire de manière limitée dans les nouveaux MySQL, mais pas pour 5 résultats par groupe).

Il s'agit d'une requête de type groupe maximum, ce qui n'est pas chose facile en SQL. Il y a différentes façons de s'attaquer à ce qui peut être plus efficace dans certains cas, mais pour le top-n en général, vous aurez envie de regarder la réponse de Bill à un précédent similaire question.

Comme avec la plupart des solutions à ce problème, il peut renvoyer plus de cinq lignes s'il existe plusieurs lignes avec la même valeur rate. Il se peut donc que vous ayez encore besoin d'une quantité de post-traitement pour le vérifier.

9
bobince

Cela nécessite une série de sous-requêtes pour classer les valeurs, les limiter, puis effectuer la somme lors du regroupement.

@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;
9

Essaye ça:

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;
9
Saharsh Shah

Construisez les colonnes virtuelles (comme RowID dans Oracle)

table:

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

les données:

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 comme ceci:

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;

si supprime la clause where dans t3, elle se présente comme suit:

enter image description here

GET "TOP N Record" -> ajoute le "rownum <= 3" dans la clause where (la clause where de t3);

CHOISISSEZ "l'année" -> ajoutez le "ENTRE 2000 ET 2009" dans la clause where (la clause where de t3);

4
Wang Wen'an

Cela a pris du temps, mais je pense que ma solution serait quelque chose à partager car elle semble élégante et assez rapide.

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

Notez que cet exemple est spécifié dans le but de la question et peut être modifié assez facilement pour d'autres buts similaires.

3
John
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

La sous-requête est presque identique à votre requête. Seul le changement consiste à ajouter

row_number() over (partition by id order by rate DESC)
2
Ricky Moreno

Le post suivant: sql: sélection du numéro d’enregistrement le plus élevé par groupe décrit la manière compliquée d’y parvenir sans sous-requêtes.

Il améliore les autres solutions proposées ici par:

  • Tout faire en une seule requête
  • Être capable d'utiliser correctement les index
  • Eviter les sous-requêtes, connues pour produire des plans d’exécution incorrects dans MySQL

Ce n'est cependant pas joli. Une bonne solution serait réalisable si les fonctions de fenêtre (aussi appelées fonctions analytiques) étaient activées dans MySQL - mais ce n’est pas le cas. L'astuce utilisée dans cet article utilise GROUP_CONCAT, qui est parfois décrite comme "Les fonctions de la fenêtre de l'homme pauvre pour MySQL".

2
Shlomi Noach

pour ceux qui comme moi avaient des questions de temps mort. J'ai fait le ci-dessous pour utiliser des limites et toute autre chose par un groupe spécifique.

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 $$

il parcourt une liste de domaines, puis insère uniquement une limite de 200 chacun

1
Dev-Ria

Essaye ça:

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;
1
MLF

Veuillez essayer ci-dessous la procédure stockée. J'ai déjà vérifié. J'obtiens un résultat correct mais sans utiliser 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
0
Himanshu Patel