175 votes

Fonction de classement dans MySQL

J'ai besoin de trouver le rang des clients. J'ajoute ici la requête SQL standard ANSI correspondante à mon besoin. Veuillez m'aider à la convertir en MySQL.

SELECT RANK() OVER (PARTITION BY Gender ORDER BY Age) AS [Partition by Gender], 
  FirstName, 
  Age,
  Gender 
FROM Person

Existe-t-il une fonction permettant de connaître le rang dans MySQL ?

288voto

Daniel Vassallo Points 142049

Une option consiste à utiliser une variable de classement, comme la suivante :

SELECT    first_name,
          age,
          gender,
          @curRank := @curRank + 1 AS rank
FROM      person p, (SELECT @curRank := 0) r
ORDER BY  age;

Le site (SELECT @curRank := 0) permet l'initialisation de la variable sans nécessiter une partie séparée SET commandement.

Cas de test :

CREATE TABLE person (id int, first_name varchar(20), age int, gender char(1));

INSERT INTO person VALUES (1, 'Bob', 25, 'M');
INSERT INTO person VALUES (2, 'Jane', 20, 'F');
INSERT INTO person VALUES (3, 'Jack', 30, 'M');
INSERT INTO person VALUES (4, 'Bill', 32, 'M');
INSERT INTO person VALUES (5, 'Nick', 22, 'M');
INSERT INTO person VALUES (6, 'Kathy', 18, 'F');
INSERT INTO person VALUES (7, 'Steve', 36, 'M');
INSERT INTO person VALUES (8, 'Anne', 25, 'F');

Résultat :

+------------+------+--------+------+
| first_name | age  | gender | rank |
+------------+------+--------+------+
| Kathy      |   18 | F      |    1 |
| Jane       |   20 | F      |    2 |
| Nick       |   22 | M      |    3 |
| Bob        |   25 | M      |    4 |
| Anne       |   25 | F      |    5 |
| Jack       |   30 | M      |    6 |
| Bill       |   32 | M      |    7 |
| Steve      |   36 | M      |    8 |
+------------+------+--------+------+
8 rows in set (0.02 sec)

59 votes

+1 pour l'initialisation déviée en ligne, c'est une belle astuce.

34 votes

N'a-t-il pas demandé une partition ? D'après ce que j'ai compris des partitions, l'ensemble des résultats aurait des classements séparés pour les hommes et les femmes.

2 votes

@Jesse : Si c'est le cas, j'ai récemment répondu à une question similaire : stackoverflow.com/questions/3162389/multiples-rangs-dans-un-tableau

60voto

Salman A Points 60620

Voici une solution générique qui attribue un rang dense sur la partition aux rangs. Elle utilise des variables utilisateur :

CREATE TABLE person (
    id INT NOT NULL PRIMARY KEY,
    firstname VARCHAR(10),
    gender VARCHAR(1),
    age INT
);

INSERT INTO person (id, firstname, gender, age) VALUES
(1,  'Adams',  'M', 33),
(2,  'Matt',   'M', 31),
(3,  'Grace',  'F', 25),
(4,  'Harry',  'M', 20),
(5,  'Scott',  'M', 30),
(6,  'Sarah',  'F', 30),
(7,  'Tony',   'M', 30),
(8,  'Lucy',   'F', 27),
(9,  'Zoe',    'F', 30),
(10, 'Megan',  'F', 26),
(11, 'Emily',  'F', 20),
(12, 'Peter',  'M', 20),
(13, 'John',   'M', 21),
(14, 'Kate',   'F', 35),
(15, 'James',  'M', 32),
(16, 'Cole',   'M', 25),
(17, 'Dennis', 'M', 27),
(18, 'Smith',  'M', 35),
(19, 'Zack',   'M', 35),
(20, 'Jill',   'F', 25);

SELECT person.*, @rank := CASE
    WHEN @partval = gender AND @rankval = age THEN @rank
    WHEN @partval = gender AND (@rankval := age) IS NOT NULL THEN @rank + 1
    WHEN (@partval := gender) IS NOT NULL AND (@rankval := age) IS NOT NULL THEN 1
END AS rnk
FROM person, (SELECT @rank := NULL, @partval := NULL, @rankval := NULL) AS x
ORDER BY gender, age;

Remarquez que les affectations de variables sont placées à l'intérieur de la balise CASE l'expression. Cela permet (en théorie) de résoudre le problème de l'ordre d'évaluation. Le site IS NOT NULL est ajouté pour gérer les problèmes de conversion de type de données et de court-circuit.

PS : Il peut facilement être converti en nombre de lignes sur partition en supprimant toutes les conditions qui vérifient l'égalité.

| id | firstname | gender | age | rank |
|----|-----------|--------|-----|------|
| 11 | Emily     | F      | 20  | 1    |
| 20 | Jill      | F      | 25  | 2    |
| 3  | Grace     | F      | 25  | 2    |
| 10 | Megan     | F      | 26  | 3    |
| 8  | Lucy      | F      | 27  | 4    |
| 6  | Sarah     | F      | 30  | 5    |
| 9  | Zoe       | F      | 30  | 5    |
| 14 | Kate      | F      | 35  | 6    |
| 4  | Harry     | M      | 20  | 1    |
| 12 | Peter     | M      | 20  | 1    |
| 13 | John      | M      | 21  | 2    |
| 16 | Cole      | M      | 25  | 3    |
| 17 | Dennis    | M      | 27  | 4    |
| 7  | Tony      | M      | 30  | 5    |
| 5  | Scott     | M      | 30  | 5    |
| 2  | Matt      | M      | 31  | 6    |
| 15 | James     | M      | 32  | 7    |
| 1  | Adams     | M      | 33  | 8    |
| 18 | Smith     | M      | 35  | 9    |
| 19 | Zack      | M      | 35  | 9    |

Démonstration sur db<>fiddle

2 votes

Cette solution, ou la solution de Mukesh, devrait être la solution correcte. Bien que techniquement, je pense que vos deux solutions représentent un classement dense et non un grade régulier. Voici une bonne explication des différences : sqlservercurry.com/2009/04/ .

0 votes

Pouvez-vous également nous faire savoir comment le code .php doit être exactement ? J'ai essayé de suivre, mais le code ci-dessus ne fonctionne pas. Comment entrer au format .php ?

0 votes

Cette solution n'est pas très générique ; elle ne fonctionnera pas si rank_column a une valeur de 0. sqlfiddle.com/#!2/9c5dd/1

58voto

Rahul Agarwal Points 60

Bien que la réponse la plus votée soit la bonne, il n'y a pas de partition. Vous pouvez faire une jointure automatique pour partitionner l'ensemble :

SELECT    a.first_name,
      a.age,
      a.gender,
        count(b.age)+1 as rank
FROM  person a left join person b on a.age>b.age and a.gender=b.gender 
group by  a.first_name,
      a.age,
      a.gender

Cas d'utilisation

CREATE TABLE person (id int, first_name varchar(20), age int, gender char(1));

INSERT INTO person VALUES (1, 'Bob', 25, 'M');
INSERT INTO person VALUES (2, 'Jane', 20, 'F');
INSERT INTO person VALUES (3, 'Jack', 30, 'M');
INSERT INTO person VALUES (4, 'Bill', 32, 'M');
INSERT INTO person VALUES (5, 'Nick', 22, 'M');
INSERT INTO person VALUES (6, 'Kathy', 18, 'F');
INSERT INTO person VALUES (7, 'Steve', 36, 'M');
INSERT INTO person VALUES (8, 'Anne', 25, 'F');

Réponse : :

Bill    32  M   4
Bob     25  M   2
Jack    30  M   3
Nick    22  M   1
Steve   36  M   5
Anne    25  F   3
Jane    20  F   2
Kathy   18  F   1

0 votes

C'est une réponse formidable, précisément parce que j'ai besoin de faire un classement par partition. Merci, Monsieur !

0 votes

IMO il a la même complexité que subselect dans la réponse de @Sam Kidman : O(n^2). Mais je ne sais pas s'il est possible de faire mieux dans MySQL.

0 votes

Vérifiez onlamp.com/pub/a/mysql/2007/03/29/ pour un excellent tutoriel sur le même sujet

24voto

Mukesh Soni Points 2747

Une mise au point de la version de Daniel pour calculer le percentile en même temps que le rang. Ainsi, deux personnes ayant les mêmes notes auront le même rang.

set @totalStudents = 0;
select count(*) into @totalStudents from marksheets;
SELECT id, score, @curRank := IF(@prevVal=score, @curRank, @studentNumber) AS rank, 
@percentile := IF(@prevVal=score, @percentile, (@totalStudents - @studentNumber + 1)/(@totalStudents)*100),
@studentNumber := @studentNumber + 1 as studentNumber, 
@prevVal:=score
FROM marksheets, (
SELECT @curRank :=0, @prevVal:=null, @studentNumber:=1, @percentile:=100
) r
ORDER BY score DESC

Résultats de la requête pour une donnée type -

+----+-------+------+---------------+---------------+-----------------+
| id | score | rank | percentile    | studentNumber | @prevVal:=score |
+----+-------+------+---------------+---------------+-----------------+
| 10 |    98 |    1 | 100.000000000 |             2 |              98 |
|  5 |    95 |    2 |  90.000000000 |             3 |              95 |
|  6 |    91 |    3 |  80.000000000 |             4 |              91 |
|  2 |    91 |    3 |  80.000000000 |             5 |              91 |
|  8 |    90 |    5 |  60.000000000 |             6 |              90 |
|  1 |    90 |    5 |  60.000000000 |             7 |              90 |
|  9 |    84 |    7 |  40.000000000 |             8 |              84 |
|  3 |    83 |    8 |  30.000000000 |             9 |              83 |
|  4 |    72 |    9 |  20.000000000 |            10 |              72 |
|  7 |    60 |   10 |  10.000000000 |            11 |              60 |
+----+-------+------+---------------+---------------+-----------------+

1 votes

Même si ce n'est pas vraiment optimal en termes de performances, c'est génial !

6voto

David Husnian Points 70

@Sam, votre point est excellent dans le concept mais je pense que vous avez mal compris ce que les docs MySQL disent sur la page référencée -- ou je me suis mal fait comprendre :-). -- Je voulais juste ajouter ceci pour que si quelqu'un se sent mal à l'aise avec la réponse de @Daniel, il soit plus rassuré ou au moins creuse un peu plus.

Vous voyez le "@curRank := @curRank + 1 AS rank" à l'intérieur de la SELECT n'est pas "une déclaration", c'est une partie "atomique" de la déclaration, elle devrait donc être sûre.

Le document auquel vous faites référence poursuit en montrant des exemples où la même variable définie par l'utilisateur se trouve dans deux parties (atomiques) de la déclaration, par exemple, "SELECT @curRank, @curRank := @curRank + 1 AS rank" .

On pourrait dire que @curRank est utilisé deux fois dans la réponse de @Daniel : (1) le "@curRank := @curRank + 1 AS rank" et (2) le "(SELECT @curRank := 0) r" mais comme le second usage fait partie de la FROM je suis presque sûr qu'elle est garantie d'être évaluée en premier, ce qui en fait essentiellement une deuxième déclaration, qui précède la première.

En fait, sur la même page de documentation MySQL que vous avez référencée, vous verrez la même solution dans les commentaires - c'est peut-être de là que @Daniel l'a tirée ; oui, je sais que ce sont les commentaires mais ce sont des commentaires sur la page de documentation officielle et cela a du poids.

0 votes

Rien de tout cela n'est justifié par la documentation. Ce ne sont que des spéculations (floues). Comme toutes les réponses utilisant et écrivant la même variable, dont le manuel dit qu'elle n'est explicitement pas définie, bien que le manuel ait beaucoup de texte peu utile sur ce qui pourrait fonctionner comme vous l'attendez, sans dire ce qu'il pense que vous attendez ou quelle est l'utilité d'une description de comportement non garanti. PS Depuis la version 8.0, l'assignation de variables en dehors de SET est dépréciée.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X