web-dev-qa-db-fra.com

ROW_NUMBER () dans MySQL

MySQL permet-il de répliquer la fonction SQL Server ROW_NUMBER()?

Par exemple:

SELECT 
    col1, col2, 
    ROW_NUMBER() OVER (PARTITION BY col1, col2 ORDER BY col3 DESC) AS intRow
FROM Table1

Ensuite, je pourrais, par exemple, ajouter une condition pour limiter intRow à 1 afin d'obtenir une seule ligne avec le plus élevé col3 pour chaque paire (col1, col2).

263
Paul

Je veux la ligne avec le plus haut col3 unique pour chaque paire (col1, col2).

C'est un maximum par groupe , l'une des questions SQL les plus fréquemment posées (puisqu'il semble que cela devrait être facile, mais en réalité, ce n'est pas le cas).

Je me plie souvent pour un null-self-join:

SELECT t0.col3
FROM table AS t0
LEFT JOIN table AS t1 ON t0.col1=t1.col1 AND t0.col2=t1.col2 AND t1.col3>t0.col3
WHERE t1.col1 IS NULL;

"Obtenez dans la table les lignes pour lesquelles aucune autre ligne avec col1 correspondant, col2 n'a un col3 supérieur." Si cela pose un problème, vous aurez peut-être besoin d’un post-traitement.)

97
bobince

Il n'y a pas de fonctionnalité de classement dans MySQL. Le plus proche que vous pouvez obtenir est d'utiliser une variable:

SELECT t.*, 
       @rownum := @rownum + 1 AS rank
  FROM YOUR_TABLE t, 
       (SELECT @rownum := 0) r

alors, comment cela fonctionnerait-il dans mon cas? J'aurais besoin de deux variables, une pour chacun de col1 et col2? Col2 aurait besoin d'une réinitialisation lorsque Col1 a changé ..?

Oui. S'il s'agissait d'Oracle, vous pourriez utiliser la fonction LEAD pour atteindre la valeur suivante. Heureusement, Quassnoi couvre la logique de ce que vous devez implémenter dans MySQL .

197
OMG Ponies

Je finis toujours par suivre ce modèle. Étant donné ce tableau:

+------+------+
|    i |    j |
+------+------+
|    1 |   11 |
|    1 |   12 |
|    1 |   13 |
|    2 |   21 |
|    2 |   22 |
|    2 |   23 |
|    3 |   31 |
|    3 |   32 |
|    3 |   33 |
|    4 |   14 |
+------+------+

Vous pouvez obtenir ce résultat:

+------+------+------------+
|    i |    j | row_number |
+------+------+------------+
|    1 |   11 |          1 |
|    1 |   12 |          2 |
|    1 |   13 |          3 |
|    2 |   21 |          1 |
|    2 |   22 |          2 |
|    2 |   23 |          3 |
|    3 |   31 |          1 |
|    3 |   32 |          2 |
|    3 |   33 |          3 |
|    4 |   14 |          1 |
+------+------+------------+

En exécutant cette requête, qui n'a besoin d'aucune variable définie:

SELECT a.i, a.j, count(*) as row_number FROM test a
JOIN test b ON a.i = b.i AND a.j >= b.j
GROUP BY a.i, a.j

J'espère que ça t'as aidé!

80
Mosty Mostacho
SELECT 
    @i:=@i+1 AS iterator, 
    t.*
FROM 
    tablename AS t,
    (SELECT @i:=0) AS foo
58
Peter Johnson

Découvrez cet article, il montre comment imiter SQL ROW_NUMBER () avec une partition dans MySQL. J'ai rencontré ce même scénario dans une implémentation WordPress. J'avais besoin de ROW_NUMBER () et ce n'était pas là.

http://www.explodybits.com/2011/11/mysql-row-number/

L'exemple de l'article utilise une seule partition par champ. Pour partitionner par des champs supplémentaires, vous pouvez faire quelque chose comme ceci:

  SELECT  @row_num := IF(@prev_value=concat_ws('',t.col1,t.col2),@row_num+1,1) AS RowNumber
         ,t.col1 
         ,t.col2
         ,t.Col3
         ,t.col4
         ,@prev_value := concat_ws('',t.col1,t.col2)
    FROM table1 t,
         (SELECT @row_num := 1) x,
         (SELECT @prev_value := '') y
   ORDER BY t.col1,t.col2,t.col3,t.col4 

L'utilisation de concat_ws gère les valeurs nulles. J'ai testé cela contre 3 champs en utilisant un int, date et varchar. J'espère que cela t'aides. Consultez l'article car il décompose cette requête et l'explique.

27
birch

À partir de MySQL 8.0.0 et au-dessus, vous pouvez utiliser nativement des fonctions fenêtrées.

1.4 Nouveautés de MySQL 8. :

Fonctions de la fenêtre.

MySQL supporte maintenant les fonctions de fenêtre qui, pour chaque ligne d'une requête, effectuent un calcul en utilisant des lignes associées à cette ligne. Celles-ci incluent des fonctions telles que RANK (), LAG () et NTILE (). En outre, plusieurs fonctions d'agrégat existantes peuvent maintenant être utilisées en tant que fonctions de fenêtre. par exemple, SUM () et AVG ().

ROW_NUMBER () over_clause :

Renvoie le numéro de la ligne en cours dans sa partition. Les nombres de lignes vont de 1 au nombre de lignes de partition.

ORDER BY affecte l'ordre dans lequel les lignes sont numérotées. Sans ORDER BY, la numérotation des lignes est indéterminée.

Démo:

CREATE TABLE Table1(
  id INT AUTO_INCREMENT PRIMARY KEY, col1 INT,col2 INT, col3 TEXT);

INSERT INTO Table1(col1, col2, col3)
VALUES (1,1,'a'),(1,1,'b'),(1,1,'c'),
       (2,1,'x'),(2,1,'y'),(2,2,'z');

SELECT 
    col1, col2,col3,
    ROW_NUMBER() OVER (PARTITION BY col1, col2 ORDER BY col3 DESC) AS intRow
FROM Table1;

Démo DBFiddle

21
Lukasz Szozda

Je voterais également pour la solution de Mosty Mostacho avec une modification mineure de son code de requête:

SELECT a.i, a.j, (
    SELECT count(*) from test b where a.j >= b.j AND a.i = b.i
) AS row_number FROM test a

Ce qui donnera le même résultat:

+------+------+------------+
|    i |    j | row_number |
+------+------+------------+
|    1 |   11 |          1 |
|    1 |   12 |          2 |
|    1 |   13 |          3 |
|    2 |   21 |          1 |
|    2 |   22 |          2 |
|    2 |   23 |          3 |
|    3 |   31 |          1 |
|    3 |   32 |          2 |
|    3 |   33 |          3 |
|    4 |   14 |          1 |
+------+------+------------+

pour la table:

+------+------+
|    i |    j |
+------+------+
|    1 |   11 |
|    1 |   12 |
|    1 |   13 |
|    2 |   21 |
|    2 |   22 |
|    2 |   23 |
|    3 |   31 |
|    3 |   32 |
|    3 |   33 |
|    4 |   14 |
+------+------+

À la seule différence que la requête n'utilise pas JOIN et GROUP BY, utilisez plutôt la sélection imbriquée.

15
abcdn

Je définirais une fonction:

delimiter $$
DROP FUNCTION IF EXISTS `getFakeId`$$
CREATE FUNCTION `getFakeId`() RETURNS int(11)
    DETERMINISTIC
begin
return if(@fakeId, @fakeId:=@fakeId+1, @fakeId:=1);
end$$

alors je pourrais faire:

select getFakeId() as id, t.* from table t, (select @fakeId:=0) as t2;

Maintenant, vous n'avez pas de sous-requête, que vous ne pouvez pas avoir dans les vues.

11
Quincy

Il n’existe pas de fonction semblable à rownum, row_num() dans MySQL, mais la procédure est la suivante:

select 
      @s:=@s+1 serial_no, 
      tbl.* 
from my_table tbl, (select @s:=0) as s;
8
Md. Kamruzzaman

requête pour row_number dans mysql

set @row_number=0;
select (@row_number := @row_number +1) as num,id,name from sbs
7
user5528503

La fonctionnalité de numéro d'avant ne peut pas être imitée. Vous pouvez obtenir les résultats escomptés, mais vous serez probablement déçu à un moment donné. Voici ce que dit la documentation de mysql:

Pour d'autres instructions, telles que SELECT, vous pouvez obtenir les résultats escomptés, mais cela n'est pas garanti. Dans la déclaration suivante, vous pourriez penser que MySQL évaluera d'abord @a, puis effectuera ensuite une affectation: SELECT @a, @a: = @ a + 1, ...; Cependant, l'ordre d'évaluation des expressions impliquant des variables utilisateur n'est pas défini.

Cordialement, Georgi.

4
user3503199

La solution que j'ai trouvée pour fonctionner au mieux utilisait une sous-requête comme celle-ci:

SELECT 
    col1, col2, 
    (
        SELECT COUNT(*) 
        FROM Table1
        WHERE col1 = t1.col1
        AND col2 = t1.col2
        AND col3 > t1.col3
    ) AS intRow
FROM Table1 t1

Les colonnes PARTITION BY sont comparées à '=' et séparées par AND. Les colonnes ORDER BY seraient comparées avec '<' ou '>' et séparées par OR.

J'ai trouvé cela très flexible, même si c'est un peu coûteux.

4
snydergd

MariaDB 10.2 implémente des "fonctions de fenêtre", notamment RANK (), ROW_NUMBER () et plusieurs autres choses:

https://mariadb.com/kb/en/mariadb/window-functions/

Basés sur une présentation à Percona Live ce mois-ci, ils sont relativement bien optimisés.

La syntaxe est identique au code de la question.

3
Rick James

Je ne vois aucune réponse simple couvrant la partie "PARTITION PAR" alors voici la mienne:

SELECT
    *
FROM (
    select
        CASE WHEN @partitionBy_1 = l THEN @row_number:=@row_number+1 ELSE @row_number:=1 END AS i
        , @partitionBy_1:=l AS p
        , t.*
    from (
        select @row_number:=0,@partitionBy_1:=null
    ) as x
    cross join (
        select 1 as n, 'a' as l
        union all
        select 1 as n, 'b' as l    
        union all
        select 2 as n, 'b' as l    
        union all
        select 2 as n, 'a' as l
        union all
        select 3 as n, 'a' as l    
        union all    
        select 3 as n, 'b' as l    
    ) as t
    ORDER BY l, n
) AS X
where i > 1
  • La clause ORDER BY doit refléter votre besoin ROW_NUMBER. Il existe donc déjà une limite claire: vous ne pouvez pas avoir plusieurs "émulations" ROW_NUMBER de ce formulaire en même temps.
  • L'ordre de la "colonne calculée" questions. Si mysql calcule ces colonnes dans un autre ordre, cela pourrait ne pas fonctionner.
  • Dans cet exemple simple, je n’en mets qu’une, mais vous pouvez avoir plusieurs parties "PARTITION BY"

        CASE WHEN @partitionBy_1 = part1 AND @partitionBy_2 = part2 [...] THEN @row_number:=@row_number+1 ELSE @row_number:=1 END AS i
        , @partitionBy_1:=part1 AS P1
        , @partitionBy_2:=part2 AS P2
        [...] 
    FROM (
        SELECT @row_number:=0,@partitionBy_1:=null,@partitionBy_2:=null[...]
    ) as x
    
2

Également un peu en retard, mais aujourd'hui, j'avais le même besoin, j'ai donc effectué une recherche sur Google et enfin une approche générale simple, que l'on trouve ici dans l'article de Pinal Dave http://blog.sqlauthority.com/2014/03/09/mysql -reset-rang-ligne-pour-chaque-groupe-partition par ligne-nombre /

Je voulais me concentrer sur la question initiale de Paul (c'était aussi mon problème), aussi je résume ma solution en exemple pratique.

Puisque nous voulons partitionner deux colonnes, je créerais une variable SET lors de l'itération pour identifier si un nouveau groupe avait été démarré.

SELECT col1, col2, col3 FROM (
  SELECT col1, col2, col3,
         @n := CASE WHEN @v = MAKE_SET(3, col1, col2)
                    THEN @n + 1 -- if we are in the same group
                    ELSE 1 -- next group starts so we reset the counter
                END AS row_number,
         @v := MAKE_SET(3, col1, col2) -- we store the current value for next iteration
    FROM Table1, (SELECT @n := 0, @v := NULL) r -- helper table for iteration with startup values
   ORDER BY col1, col2, col3 DESC -- because we want the row with maximum value
) x WHERE row_number = 1 -- and here we select exactly the wanted row from each group

Le 3 signifie au premier paramètre de MAKE_SET que je veux les deux valeurs dans le SET (3 = 1 | 2). Bien sûr, si nous n'avons pas deux ou plusieurs colonnes construisant les groupes, nous pouvons éliminer l'opération MAKE_SET. La construction est exactement la même. Cela fonctionne pour moi au besoin. Un grand merci à Dave Pinal pour sa démonstration claire.

1
Miklos Krivan

Cela pourrait aussi être une solution:

SET @row_number = 0;

SELECT 
    (@row_number:=@row_number + 1) AS num, firstName, lastName
FROM
    employees
1
Rishabh Pandey

MySQL supporte le ROW_NUMBER () depuis la version 8.0 +.

Si vous utilisez MySQL 8.0 ou version ultérieure, consultez la fonction ROW_NUMBER (). Sinon, vous devez émuler la fonction ROW_NUMBER ().

Row_number () est une fonction de classement qui renvoie un numéro séquentiel d'une ligne, en commençant à 1 pour la première ligne.

pour l'ancienne version,

SELECT t.*, 
       @rowid := @rowid + 1 AS ROWID
  FROM TABLE t, 
       (SELECT @rowid := 0) dummy;
1

Un peu tard mais peut aussi aider quelqu'un qui cherche des réponses ...

Entre rows/row_number example - requête récursive pouvant être utilisée dans n’importe quel code SQL:

WITH data(row_num, some_val) AS 
(
 SELECT 1 row_num, 1 some_val FROM any_table --dual in Oracle
  UNION ALL
 SELECT row_num+1, some_val+row_num FROM data WHERE row_num < 20 -- any number
)
SELECT * FROM data
 WHERE row_num BETWEEN 5 AND 10
/

ROW_NUM    SOME_VAL
-------------------
5           11
6           16
7           22
8           29
9           37
10          46
1
Art

Ceci permet la réalisation de la même fonctionnalité que ROW_NUMBER () AND PARTITION BY dans MySQL

SELECT  @row_num := IF(@prev_value=GENDER,@row_num+1,1) AS RowNumber
       FirstName, 
       Age,
       Gender,
       @prev_value := GENDER
  FROM Person,
      (SELECT @row_num := 1) x,
      (SELECT @prev_value := '') y
  ORDER BY Gender, Age DESC
1
Alankar

Important: envisagez de passer à MySQL 8+ et d'utiliser la fonction ROW_NUMBER () définie et documentée, et de supprimer les anciens hacks liés à une version ancienne limitée de MySQL

Maintenant, voici un de ces hacks:

Les réponses ici qui utilisent principalement/toutes les variables dans la requête semblent ignorer le fait que la documentation dit (paraphrase):

Ne comptez pas sur les éléments de la liste SELECT en cours d’évaluation dans l’ordre croissant. Ne pas affecter de variables dans un élément SELECT et les utiliser dans un autre

En tant que tel, ils risquent de donner la mauvaise réponse, car ils font généralement

select
  (row number variable that uses partition variable),
  (assign partition variable)

Si celles-ci sont évaluées de bas en haut, le numéro de ligne cessera de fonctionner (pas de partitions)

Nous devons donc utiliser quelque chose avec un ordre d'exécution garanti. Entrez CASSE QUAND:

SELECT
  t.*, 
  @r := CASE 
    WHEN col = @prevcol THEN @r + 1 
    WHEN (@prevcol := col) = null THEN null
    ELSE 1 END AS rn
FROM
  t, 
  (SELECT @r := 0, @prevcol := null) x
ORDER BY col

En tant que contour ld, l'ordre d'attribution de prevcol est important - prevcol doit être comparé à la valeur de la ligne actuelle avant de lui attribuer une valeur de la ligne actuelle (sinon, il s'agirait de la valeur col de la ligne en cours, et non de la valeur col de la ligne précédente). .

Voici comment cela s'accorde:

  • Le premier WHEN est évalué. Si le col de cette ligne est identique à celui de la rangée précédente, @r est incrémenté et renvoyé à partir de CASE. Ce retour des valeurs de led est stocké dans @r. C'est une fonctionnalité de MySQL que l'affectation renvoie la nouvelle valeur de ce qui est assigné dans @r dans les lignes de résultat.

  • Pour la première ligne du jeu de résultats, @prevcol est null (initialisé à null dans la sous-requête), donc ce prédicat est faux. Ce premier prédicat renvoie également false à chaque changement de colonne (la ligne actuelle est différente de la ligne précédente). Cela provoque l'évaluation du deuxième WHEN.

  • Le deuxième prédicat WHEN est toujours faux et il existe uniquement pour attribuer une nouvelle valeur à @prevcol. Comme le col de cette ligne est différent du col de la rangée précédente (nous le savons car s'il était identique, le premier WHEN aurait été utilisé), nous devons attribuer la nouvelle valeur afin de la conserver pour le test suivant. Étant donné que l'affectation est faite et que le résultat de l'affectation est comparé à null, et que tout ce qui est égal à null est faux, ce prédicat est toujours faux. Mais au moins l’évaluer a fait son travail en conservant la valeur de col de cette ligne afin qu’elle puisse être évaluée par rapport à la valeur de col de la ligne suivante.

  • Parce que le second WHEN est faux, cela signifie que dans les cas où la colonne partitionnée par (col) a été modifiée, c’est l’ELSE qui donne une nouvelle valeur pour @r, en reprenant la numérotation à partir de 1.

Nous arrivons à une situation où:

SELECT
  t.*, 
  ROW_NUMBER() OVER(PARTITION BY pcol1, pcol2, ... pcolX ORDER BY ocol1, ocol2, ... ocolX) rn
FROM
  t

A la forme générale:

SELECT
  t.*, 
  @r := CASE 
    WHEN col1 = @pcol1 AND col2 = @pcol2 AND ... AND colX = @pcolX THEN @r + 1 
    WHEN (@pcol1 := pcol1) = null OR (@pcol2 := col2) = null OR ... OR (@pcolX := colX) = null THEN null
    ELSE 1 
  END AS rn
FROM
  t, 
  (SELECT @r := 0, @pcol1 := null, @pcol2 := null, ..., @pcolX := null) x
ORDER BY pcol1, pcol2, ..., pcolX, ocol1, ocol2, ..., ocolX

Notes de bas de page:

  • Le p dans pcol signifie "partition", le o dans ocol signifie "ordre" - sous la forme générale, j'ai supprimé le "prev" du nom de la variable afin de réduire l'encombrement visuel.

  • Les crochets autour de (@pcolX := colX) = null sont importants. Sans eux, vous affecterez null à @pcolX et tout cessera de fonctionner

  • C'est un compromis que le jeu de résultats doit également être commandé par les colonnes de la partition, pour que la colonne précédente puisse être comparée. Vous ne pouvez donc pas ordonner votre numéro d'ordre en fonction d'une colonne, mais votre ensemble de résultats en ordre différent performance

  • Je n'ai pas encore approfondi le point de savoir si la méthode fonctionnait bien, mais s'il existe un risque que les prédicats du second WHEN soient optimisés (tout ce qui est comparé à null est null/false alors pourquoi se donner la peine d'exécuter l'affectation) et non exécuté , ça s'arrête aussi. Cela ne semble pas se produire dans mon expérience, mais j'accepterai volontiers les commentaires et proposerai une solution si cela pouvait raisonnablement se produire.

  • Il peut être judicieux de transtyper les types NULL qui créent @pcolX sur les types réels de vos colonnes, dans la sous-requête qui crée les variables @pcolX, à savoir: select @pcol1 := CAST(null as INT), @pcol2 := CAST(null as DATE)

0
Caius Jard