Функции RANK() и DENSE_RANK()

Эти функции, как и функция ROW_NUMBER(), тоже нумеруют строки, но делают это несколько отличным способом. Это отличие проявляется в том, что строки, которые имеют одинаковые значения в столбцах, по которым выполняется упорядочивание, получают одинаковые номера (ранги). Например, значения (отсортированные по возрастанию)

1
5
6
6
6

получат такие номера:

11
52
63
63
63

Возникает вопрос, с какого номера продолжится нумерация, если, скажем, в последовательности чисел появится 7 и т.д.? Здесь есть два варианта:

  1. с номера 4, т.к. это следующий номер по порядку;

  2. с номера 6, т.к. следующая строка будет шестая по счету.

Такая “неоднозначность” и привела к появлению двух функций вместо одной - RANK и DENSE_RANK, первая из которых продолжит нумерацию с 6, а вторая (плотная) - с 4.

Рассмотрим несколько примеров. Начнем с демонстрации отличия в поведении функций RANK и ROW_NUMBER:

SELECT *, ROW_NUMBER() OVER(ORDER BY type) num,
RANK() OVER(ORDER BY type) rnk
FROM Printer;
mssql
🚫
[[ error ]]
[[ column ]]
[[ value ]]

Здесь в двух последних столбцах выводятся значения сравниваемых функций при упорядочивании строк по столбцу type:

codemodelcolortypepricenumrnk
21433yJet270,0011
31434yJet290,0021
11276nLaser400,0033
61288nLaser400,0043
41401nMatrix150,0055
51408nMatrix270,0065

Как и следовало ожидать, ROW_NUMBER пронумеровывает весь набор строк, возвращаемых запросом. Функция RANK, как оказалось, работает по второму из рассмотренных выше варианту, т.е. следующим номером после строк с одинаковым рангом будет номер строки.

А теперь сравним “плотный” и “неплотный” ранги:

SELECT *,
RANK() OVER(ORDER BY type) rnk,
DENSE_RANK() OVER(ORDER BY type) rnk_dense
FROM Printer;
mssql
🚫
[[ error ]]
[[ column ]]
[[ value ]]
codemodelcolortypepricernkrnk_dense
21433yJet270,0011
31434yJet290,0011
11276nLaser400,0032
61288nLaser400,0032
41401nMatrix150,0053
51408nMatrix270,0053

Следует также обратить вниманию на порядок, в котором выводятся строки результирующего набора. Поскольку оператор SELECT в нашем примере не имеет предложения ORDER BY, а для вычисления рангов используется одинаковое упорядочивание по столбцу type, то и результат выводится в том же порядке. В целях оптимизации, если вам не нужно какое-либо другое упорядочение результирующего набора, используйте этот факт, чтобы не выполнять лишние сортировки, которые ухудшают производительность запроса.

Как и для функции ROW_NUMBER, в предложении OVER может использоваться конструкция PARTITION BY, разбивающая весь набор строк, возвращаемых запросом, на группы, к которым затем применяется соответствующая функция.

Запрос

SELECT *,
RANK() OVER(PARTITION BY type ORDER BY price) rnk FROM Printer;
mssql
🚫
[[ error ]]
[[ column ]]
[[ value ]]
позволяет в каждой группе, определяемой типом принтера, ранжировать модели по цене в порядке ее возрастания:

codemodelcolortypepricernk
21433yJet270,001
31434yJet290,002
11276nLaser400,001
61288nLaser400,001
41401nMatrix150,001
51408nMatrix270,002

А вот как можно выбрать самые дешевые модели в каждой категории:

SELECT model, color, type, price
FROM (
SELECT *,
RANK() OVER(PARTITION BY type ORDER BY price) rnk
FROM Printer
) Ranked_models
WHERE rnk =
1;
mssql
🚫
[[ error ]]
[[ column ]]
[[ value ]]
modelcolortypeprice
1433yJet270,00
1276nLaser400,00
1288nLaser400,00
1401nMatrix150,00

Запрос можно было бы написать короче, если бы функцию RANK можно было бы применять в предложении WHERE, т.к. само значение ранга нам не требуется. Однако это запрещено (как и для других ранжирующих функций), по крайней мере, в SQL Server.

Наконец, рассмотрим еще один пример.

Пример. Найти производителей, которые производят более 2-х моделей PC.

Эта задача имеет традиционное решение через агрегатные функции:

SELECT maker
FROM Product
WHERE type =
'PC'
GROUP BY maker
HAVING COUNT(*)
> 2;
mssql
🚫
[[ error ]]
[[ column ]]
[[ value ]]

Однако эту задачу можно решить и с помощью функции RANK. Идея состоит в следующем: ранжировать модели каждого производителя по уникальному ключу и выбрать только тех производителей, модели которых достигают ранга 3:

SELECT maker
FROM (
SELECT maker,
RANK() OVER(PARTITION BY maker ORDER BY model) rnk
FROM
Product
WHERE type =
'PC'
) Ranked_makers
WHERE rnk = 3;
mssql
🚫
[[ error ]]
[[ column ]]
[[ value ]]

И в одном, и в другом случае, естественно, мы получим один и тот же результат:

maker
E

Еще раз повторю: упорядочивание в последнем случае должно быть выполнено по уникальной комбинации столбцов, т.к., в противном случае, моделей может быть больше трех, а ранг меньше (например, 1, 2, 2,…). В нашем случае данное условие выполнено, т.к. упорядочивание выполняется по столбцу model, который является первичным ключом в таблице Product.

Кстати, планы выполнения этих запросов демонстрируют одинаковые стоимости наиболее расходных операций – сканирования таблицы и сортировку (которая я первом случае присутствует неявно и вызвана операцией группировки).

Рекомендуемые упражнения: 105, 116

Пример использования DENSE_RANK

Часто встречается задача нахождения N-го по величине значения из набора значений некоторого столбца таблицы, например:

Найти второе по величине значение цены в таблице PC.

Давайте выведем отсортированный список значений цены из таблицы PC для контроля, добавив столбцы со значениями ранжирующих функций:

SELECT  price, DENSE_RANK() OVER(ORDER BY price DESC) dense_rnk,   
RANK() OVER(ORDER BY price DESC) rnk,  
ROW_NUMBER() OVER(ORDER BY price DESC) rn  
FROM PC ORDER BY price DESC;
mssql
🚫
[[ error ]]
[[ column ]]
[[ value ]]
pricedense_rnkrnkrn
980,00111
970,00222
950,00333
850,00444
850,00445
850,00446
600,00577
600,00578
400,00699
350,0071010
350,0071011
350,0071012

В рамках стандарта SQL-92 эту задачу можно решить следующим образом:

SELECT MAX(price) "2nd_price"  FROM PC
WHERE price < (SELECT MAX(price) FROM PC);
mssql
🚫
[[ error ]]
[[ column ]]
[[ value ]]
2nd_price
970,00

Т.е. мы находим значение максимума среди всех значений, меньших максимального. А если нам потребуется найти значение третьей по величине цены? Можно поступить аналогично:

SELECT MAX(price) "3rd_price" FROM PC WHERE price <
(
SELECT MAX(price) FROM PC
WHERE price < (SELECT MAX(price) FROM PC)
);
mssql
🚫
[[ error ]]
[[ column ]]
[[ value ]]
3rd_price
950,00

А как найти N-е значение цены? Следуя используемой логике, мы можем добавлять новые “матрешки” к уже существующим вплоть до N-ой. Это решение никак не назовешь универсальным.

Для решения подобных задач хорошо подходит функция DENSE_RANK. Например, исходную задачу с помощью этой функции можно решить так:

SELECT DISTINCT price FROM(
SELECT DENSE_RANK() OVER(ORDER BY price DESC) rnk, price FROM PC
) X WHERE rnk=2;
mssql
🚫
[[ error ]]
[[ column ]]
[[ value ]]

А чтобы найти любую другую порядковую цену (например, третью), достаточно поменять константу в условиях отбора:

SELECT DISTINCT price FROM(
SELECT DENSE_RANK() OVER(ORDER BY price DESC) rnk, price FROM PC
) X WHERE rnk=3;
mssql
🚫
[[ error ]]
[[ column ]]
[[ value ]]

Следует отметить, что использование DENSE_RANK, а не RANK, обусловлено тем, что в случае наличия одинаковых цен, значения, возвращаемые функцией RANK, будут иметь пропуски (рассмотрите задачу нахождения пятой по величине цены). Если же ставить задачу нахождения неуникального N-го значения, то можно использовать функцию ROW_NUMBER (например, третий человек в шеренге по росту). А если значения в таблице уникальны, то решение с помощью любой из этих функций даст один и тот же результат.