selecting top N rows for each group in a table

后端 未结 3 1169
醉酒成梦
醉酒成梦 2020-11-30 04:03

I am facing a very common issue regarding \"Selecting top N rows for each group in a table\".

Consider a table with id, name, hair_colour, score columns

相关标签:
3条回答
  • 2020-11-30 04:48

    The way the algorithm comes up with the rank, is to count the number of rows in the cross-product with a score equal to or greater than the girl in question, in order to generate rank. Hence in the problem case you're talking about, Sarah's grid would look like

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

    and similarly for Deborah, which is why both girls get a rank of 2 here.

    The problem is that when there's a tie, all girls take the lowest value in the tied range due to this count, when you'd want them to take the highest value instead. I think a simple change can fix this:

    Instead of a greater-than-or-equal comparison, use a strict greater-than comparison to count the number of girls who are strictly better. Then, add one to that and you have your rank (which will deal with ties as appropriate). So the inner select would be:

    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
    

    Can anyone see any problems with this approach that have escaped my notice?

    0 讨论(0)
  • 2020-11-30 04:55

    Use this compound select which handles OP problem properly

    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)
    

    Note that you need to use IFNULL here to handle case when table girls has less rows for some type of hair then we want to see in sql answer (in OP case it is 3 items).

    0 讨论(0)
  • 2020-11-30 04:58

    If you're using SQL Server 2005 or newer, you can use the ranking functions and a CTE to achieve this:

    ;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
    

    This CTE will "partition" your data by the value of the hair column, and each partition is then order by score (descending) and gets a row number; the highest score for each partition is 1, then 2 etc.

    So if you want to the TOP 3 of each group, select only those rows from the CTE that have a RowNum of 3 or less (1, 2, 3) --> there you go!

    0 讨论(0)
提交回复
热议问题