2010-10-22 16 views
12

Je suis confronté à un problème très courant concernant la "sélection des N premières lignes pour chaque groupe dans un tableau".Sélection des N premières lignes pour chaque groupe dans un tableau

Considérons un tableau avec des colonnes id, name, hair_colour, score.

Je veux un résultat tel que, pour chaque couleur de cheveux, obtenez-moi les 3 meilleurs noms de marqueur.

Pour résoudre cela, je suis exactement ce que je dois sur Rick Osborne's blogpost "sql-getting-top-n-rows-for-a-grouped-query"

Cette solution ne fonctionne pas comme prévu lorsque mes scores sont égaux.

Dans l'exemple ci-dessus, le résultat est le suivant.

id name hair score ranknum 
--------------------------------- 
12 Kit Blonde 10 1 
    9 Becca Blonde 9 2 
    8 Katie Blonde 8 3 
    3 Sarah Brunette 10 1  
    4 Deborah Brunette 9 2 - ------- - - > if 
    1 Kim Brunette 8 3 

Considérons la ligne 4 Deborah Brunette 9 2. Si cela a aussi le même score (10) que Sarah, alors le ranknum sera de 2,2,3 pour les cheveux de type "Brunette".

Quelle est la solution à ce problème?

+1

Quel RDBMS utilisez-vous? –

+0

Il existe une solution pour cela sur http://stackoverflow.com/questions/3823939/ dans le cas où vous n'utilisez pas les nouveaux serveurs SQL. –

Répondre

16

Si vous utilisez SQL Server 2005 ou plus récent, vous pouvez utiliser les fonctions de classement et un CTE pour y parvenir:

;WITH HairColors AS 
(SELECT id, name, hair, score, 
     ROW_NUMBER() OVER(PARTITION BY hair ORDER BY score DESC) as 'RowNum' 
) 
SELECT id, name, hair, score 
FROM HairColors 
WHERE RowNum <= 3 

Ce CTE « partition » données par la valeur de la colonne hair , et chaque partition est ensuite classée par score (décroissant) et obtient un numéro de rangée; le score le plus élevé pour chaque partition est 1, puis 2 etc.

Donc, si vous voulez le TOP 3 de chaque groupe, sélectionnez uniquement les lignes du CTE qui ont un RowNum de 3 ou moins (1, 2, 3) -> voilà!

+0

ROW_NUMBER() OVER (PARTITION PAR CHEVEUX ORDER BY score DESC) comme 'RowNum') le crochet dans cette ligne n'est pas équilibré. est-ce compatible avec la grammaire db2 sql? – zinking

+0

@zinking: merci - il y avait une clôture parens trop ... réparé! Je ne sais pas si DB2 supporte cela (pas assez DB2) - mais c'est certainement une construction standard ANSI/ISO SQL - pas une fonctionnalité inventée par Microsoft :-) –

+1

Merde, ça vient de faire ma journée! Quelle introduction aux CTE! –

0

La façon dont l'algorithme arrive avec le rang, est de compter le nombre de lignes dans le produit croisé avec un score égal ou supérieur à la fille en question, afin de générer le rang. Par conséquent, dans le cas du problème que vous parlez, la grille de Sarah ressemblerait

a.name | a.score | b.name | b.score 
-------+---------+---------+-------- 
Sarah | 9  | Sarah | 9 
Sarah | 9  | Deborah | 9 

et de même pour Deborah, ce qui explique pourquoi les deux filles obtiennent un rang de 2 ici. Le problème est que lorsqu'il y a une égalité, toutes les filles prennent la valeur la plus basse dans la gamme liée en raison de ce nombre, quand vous voudriez qu'ils prennent la valeur la plus élevée à la place. Je pense qu'un simple changement peut résoudre ce problème:

Au lieu d'une comparaison supérieure ou égale, utilisez une comparaison strictement supérieure pour compter le nombre de filles qui sont strictement meilleures. Ensuite, ajoutez-en un et vous avez votre rang (qui traitera des liens si nécessaire). Donc le choix interne serait:

SELECT a.id, COUNT(*) + 1 AS ranknum 
FROM girl AS a 
    INNER JOIN girl AS b ON (a.hair = b.hair) AND (a.score < b.score) 
GROUP BY a.id 
HAVING COUNT(*) <= 3 

Quelqu'un peut-il voir des problèmes avec cette approche qui ont échappé à mon attention?

+0

Cela ne fonctionne-t-il pas en temps quadratique? – b0fh

0

Utilisez ce composé sélectionner qui gère problème OP correctement

SELECT g.* FROM girls as g 
WHERE g.score > IFNULL((SELECT g2.score FROM girls as g2 
       WHERE g.hair=g2.hair ORDER BY g2.score DESC LIMIT 3,1), 0) 

Notez que vous devez utiliser IFNULL ici pour gérer le cas lorsque la table filles a moins de lignes pour un certain type de cheveux nous voulons voir dans la réponse sql (dans le cas OP il est 3 éléments).