Multiple Ranks in one table

Solution 1:

This is a bit tricky. You may want to use variables, such as in the following example:

SELECT    ( 
            CASE cust_type 
            WHEN @curType 
            THEN @curRow := @curRow + 1 
            ELSE @curRow := 1 AND @curType := cust_type END
          ) + 1 AS rank,
          cust_type,
          cust_name,
          revenue
FROM      sales,
          (SELECT @curRow := 0, @curType := '') r
ORDER BY  cust_type DESC, revenue DESC;

The (SELECT @curRow := 0, @curType := '') r part allows the variable initialization without requiring a separate SET command.

Test case:

CREATE TABLE sales (cust_type varchar(10), cust_name varchar(10), revenue int);

INSERT INTO sales VALUES ('Top', 'A', 10000);
INSERT INTO sales VALUES ('Top', 'B', 9000);
INSERT INTO sales VALUES ('Top', 'C', 8000);
INSERT INTO sales VALUES ('Bottom', 'X', 5000);
INSERT INTO sales VALUES ('Bottom', 'Y', 6000);
INSERT INTO sales VALUES ('Bottom', 'Z', 7000);

Result:

+------+-----------+-----------+---------+
| rank | cust_type | cust_name | revenue |
+------+-----------+-----------+---------+
|    1 | Top       | A         |   10000 |
|    2 | Top       | B         |    9000 |
|    3 | Top       | C         |    8000 |
|    1 | Bottom    | Z         |    7000 |
|    2 | Bottom    | Y         |    6000 |
|    3 | Bottom    | X         |    5000 |
+------+-----------+-----------+---------+
6 rows in set (0.00 sec)

Another test case:

CREATE TABLE sales (cust_type varchar(10), cust_name varchar(10), revenue int);

INSERT INTO sales VALUES ('Type X', 'A', 7000);
INSERT INTO sales VALUES ('Type X', 'B', 8000);
INSERT INTO sales VALUES ('Type Y', 'C', 5000);
INSERT INTO sales VALUES ('Type Y', 'D', 6000);
INSERT INTO sales VALUES ('Type Y', 'E', 4000);
INSERT INTO sales VALUES ('Type Z', 'F', 4000);
INSERT INTO sales VALUES ('Type Z', 'G', 3000);

Result:

+------+-----------+-----------+---------+
| rank | cust_type | cust_name | revenue |
+------+-----------+-----------+---------+
|    1 | Type Z    | F         |    4000 |
|    2 | Type Z    | G         |    3000 |
|    1 | Type Y    | D         |    6000 |
|    2 | Type Y    | C         |    5000 |
|    3 | Type Y    | E         |    4000 |
|    1 | Type X    | B         |    8000 |
|    2 | Type X    | A         |    7000 |
+------+-----------+-----------+---------+
7 rows in set (0.00 sec)

You can obviously order the cust_type in ascending order instead of descending. I used descending just to have Top before Bottom in the original test case.

Solution 2:

I found a problem with the solution using CASE, @curRow, and @curType. It depends on the execution plan MySQL uses to process the query. For example, it shows up if you add a join to the query. Then there is no guarantee that the rank is going to be computed correctly.

Making a slight change to the answer:

CREATE TABLE sales (cust_type_id int, cust_name varchar(10), revenue int);
CREATE TABLE cust_type (cust_type_id int, type_name varchar(10));

INSERT INTO cust_type VALUES (1, 'Bottom');
INSERT INTO cust_type VALUES (2, 'Top');

INSERT INTO sales VALUES (2, 'A', 10000);
INSERT INTO sales VALUES (2, 'B', 9000);
INSERT INTO sales VALUES (2, 'C', 8000);
INSERT INTO sales VALUES (1, 'X', 5000);
INSERT INTO sales VALUES (1, 'Y', 6000);
INSERT INTO sales VALUES (1, 'Z', 7000);

If I query only the sales table I get the rank in the correct order, but if I join to the cust_type table the rank values are no longer correct

SELECT    ( 
            CASE s.cust_type_id 
            WHEN @curType 
            THEN @curRow := @curRow + 1 
            ELSE @curRow := 1 AND @curType := s.cust_type_id END
          ) AS rank,
          t.type_name,
          s.cust_name,
          s.revenue
FROM      sales s,
          cust_type t,
          (SELECT @curRow := 0, @curType := 0) r
WHERE s.cust_type_id = t.cust_type_id 
ORDER BY  t.type_name DESC, s.revenue DESC;

Result:

+------+-----------+-----------+---------+
| rank | type_name | cust_name | revenue |
+------+-----------+-----------+---------+
|    1 | Top       | A         |   10000 |
|    2 | Top       | B         |    9000 |
|    3 | Top       | C         |    8000 |
|    3 | Bottom    | Z         |    7000 |
|    2 | Bottom    | Y         |    6000 |
|    1 | Bottom    | X         |    5000 |
+------+-----------+-----------+---------+

MySQL is running the initial query into a temp table and then the ORDER BY is executing against the temp table after a rank was already computed.

Solution 3:

This is similar to Thomas's answer, but a little simpler:

SELECT (SELECT COUNT(Cust_Type) FROM sales 
           WHERE Cust_Type = S.Cust_Type AND Revenue >= S.Revenue) AS Rank,
        Cust_Type,
        Cust_Name,
        Revenue 
  FROM sales AS S
  ORDER BY Cust_Type DESC, Rank;

I'm not sure how the performance compares to the Daniel's solution, particularly on very large data sets, or if you have to use complex joins.

Solution 4:

What is not exactly clear is how the items should be ranked (I assumed by Revenue) or whether you are only pulling a certain number of values (e.g. the top 3 and the bottom 3) so I assumed you wanted all values. Given those assumptions,

Select Cust_Name, Cust_Type
    , (Select Count(*)
        From Table As T1
        Where T1.Revenue > T.Revenue ) + 1 As Rank
From Table As T
Where Cust_Type = 'Top'
Union All
Select Cust_Name, Cust_Type
    , (Select Count(*)
        From Table As T1
        Where T1.Revenue < T.Revenue ) + 1 As Rank
From Table As T
Where Cust_Type = 'Bottom'

If you were trying to do this in a single non-union query you could do:

Select Cust_Name, Cust_Type
    , Case Z.Cust_Type
        When 'Top' Then Z.TopRank
        Else Z.BottomRank
        End As Rank
From    (
        Select Cust_Name, Cust_Type
            , (Select Count(*)
                From Table As T1
                Where T1.Revenue > T.Revenue ) + 1 As TopRank
            , (Select Count(*)
                From Table As T1
                Where T1.Revenue < T.Revenue ) + 1 As BottomRank
        From Table As T
        ) As Z