Функции RANK() и DENSE_RANK()
Эти функции, как и функция ROW_NUMBER(), тоже нумеруют строки, но делают это несколько отличным способом. Это отличие проявляется в том, что строки, которые имеют одинаковые значения в столбцах, по которым выполняется упорядочивание, получают одинаковые номера (ранги). Например, значения (отсортированные по возрастанию)
1 |
5 |
6 |
6 |
6 |
получат такие номера:
1 | 1 |
5 | 2 |
6 | 3 |
6 | 3 |
6 | 3 |
Возникает вопрос, с какого номера продолжится нумерация, если, скажем, в последовательности чисел появится 7 и т.д.? Здесь есть два варианта:
с номера 4, т.к. это следующий номер по порядку;
с номера 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;
[[ column ]] |
---|
[[ value ]] |
Здесь в двух последних столбцах выводятся значения сравниваемых функций при упорядочивании строк по столбцу type:
code | model | color | type | price | num | rnk |
---|---|---|---|---|---|---|
2 | 1433 | y | Jet | 270,00 | 1 | 1 |
3 | 1434 | y | Jet | 290,00 | 2 | 1 |
1 | 1276 | n | Laser | 400,00 | 3 | 3 |
6 | 1288 | n | Laser | 400,00 | 4 | 3 |
4 | 1401 | n | Matrix | 150,00 | 5 | 5 |
5 | 1408 | n | Matrix | 270,00 | 6 | 5 |
Как и следовало ожидать, ROW_NUMBER пронумеровывает весь набор строк, возвращаемых запросом. Функция RANK, как оказалось, работает по второму из рассмотренных выше варианту, т.е. следующим номером после строк с одинаковым рангом будет номер строки.
А теперь сравним “плотный” и “неплотный” ранги:
SELECT *,
RANK() OVER(ORDER BY type) rnk,
DENSE_RANK() OVER(ORDER BY type) rnk_dense
FROM Printer;
[[ column ]] |
---|
[[ value ]] |
code | model | color | type | price | rnk | rnk_dense |
---|---|---|---|---|---|---|
2 | 1433 | y | Jet | 270,00 | 1 | 1 |
3 | 1434 | y | Jet | 290,00 | 1 | 1 |
1 | 1276 | n | Laser | 400,00 | 3 | 2 |
6 | 1288 | n | Laser | 400,00 | 3 | 2 |
4 | 1401 | n | Matrix | 150,00 | 5 | 3 |
5 | 1408 | n | Matrix | 270,00 | 5 | 3 |
Следует также обратить вниманию на порядок, в котором выводятся строки результирующего набора. Поскольку оператор SELECT в нашем примере не имеет предложения ORDER BY, а для вычисления рангов используется одинаковое упорядочивание по столбцу type, то и результат выводится в том же порядке. В целях оптимизации, если вам не нужно какое-либо другое упорядочение результирующего набора, используйте этот факт, чтобы не выполнять лишние сортировки, которые ухудшают производительность запроса.
Как и для функции ROW_NUMBER, в предложении OVER может использоваться конструкция PARTITION BY, разбивающая весь набор строк, возвращаемых запросом, на группы, к которым затем применяется соответствующая функция.
Запрос
SELECT *,
RANK() OVER(PARTITION BY type ORDER BY price) rnk FROM Printer;
[[ column ]] |
---|
[[ value ]] |
code | model | color | type | price | rnk |
---|---|---|---|---|---|
2 | 1433 | y | Jet | 270,00 | 1 |
3 | 1434 | y | Jet | 290,00 | 2 |
1 | 1276 | n | Laser | 400,00 | 1 |
6 | 1288 | n | Laser | 400,00 | 1 |
4 | 1401 | n | Matrix | 150,00 | 1 |
5 | 1408 | n | Matrix | 270,00 | 2 |
А вот как можно выбрать самые дешевые модели в каждой категории:
SELECT model, color, type, price
FROM (
SELECT *,
RANK() OVER(PARTITION BY type ORDER BY price) rnk
FROM Printer
) Ranked_models
WHERE rnk =
1;
[[ column ]] |
---|
[[ value ]] |
model | color | type | price |
---|---|---|---|
1433 | y | Jet | 270,00 |
1276 | n | Laser | 400,00 |
1288 | n | Laser | 400,00 |
1401 | n | Matrix | 150,00 |
Запрос можно было бы написать короче, если бы функцию RANK можно было бы применять в предложении WHERE, т.к. само значение ранга нам не требуется. Однако это запрещено (как и для других ранжирующих функций), по крайней мере, в SQL Server.
Наконец, рассмотрим еще один пример.
Пример. Найти производителей, которые производят более 2-х моделей PC.
Эта задача имеет традиционное решение через агрегатные функции:
SELECT maker
FROM Product
WHERE type =
'PC'
GROUP BY maker
HAVING COUNT(*)
> 2;
[[ 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;
[[ column ]] |
---|
[[ value ]] |
И в одном, и в другом случае, естественно, мы получим один и тот же результат:
maker |
---|
E |
Еще раз повторю: упорядочивание в последнем случае должно быть выполнено по уникальной комбинации столбцов, т.к., в противном случае, моделей может быть больше трех, а ранг меньше (например, 1, 2, 2,…). В нашем случае данное условие выполнено, т.к. упорядочивание выполняется по столбцу model, который является первичным ключом в таблице Product.
Кстати, планы выполнения этих запросов демонстрируют одинаковые стоимости наиболее расходных операций – сканирования таблицы и сортировку (которая я первом случае присутствует неявно и вызвана операцией группировки).
Рекомендуемые упражнения: 105, 116
Пример использования DENSE_RANK
Часто встречается задача нахождения N-го по величине значения из набора значений некоторого столбца таблицы, например:
Давайте выведем отсортированный список значений цены из таблицы 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;
[[ column ]] |
---|
[[ value ]] |
price | dense_rnk | rnk | rn |
---|---|---|---|
980,00 | 1 | 1 | 1 |
970,00 | 2 | 2 | 2 |
950,00 | 3 | 3 | 3 |
850,00 | 4 | 4 | 4 |
850,00 | 4 | 4 | 5 |
850,00 | 4 | 4 | 6 |
600,00 | 5 | 7 | 7 |
600,00 | 5 | 7 | 8 |
400,00 | 6 | 9 | 9 |
350,00 | 7 | 10 | 10 |
350,00 | 7 | 10 | 11 |
350,00 | 7 | 10 | 12 |
В рамках стандарта SQL-92 эту задачу можно решить следующим образом:
SELECT MAX(price) "2nd_price" FROM PC
WHERE price < (SELECT MAX(price) FROM PC);
[[ 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)
);
[[ 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;
[[ column ]] |
---|
[[ value ]] |
А чтобы найти любую другую порядковую цену (например, третью), достаточно поменять константу в условиях отбора:
SELECT DISTINCT price FROM(
SELECT DENSE_RANK() OVER(ORDER BY price DESC) rnk, price FROM PC
) X WHERE rnk=3;
[[ column ]] |
---|
[[ value ]] |
Следует отметить, что использование DENSE_RANK, а не RANK, обусловлено тем, что в случае наличия одинаковых цен, значения, возвращаемые функцией RANK, будут иметь пропуски (рассмотрите задачу нахождения пятой по величине цены). Если же ставить задачу нахождения неуникального N-го значения, то можно использовать функцию ROW_NUMBER (например, третий человек в шеренге по росту). А если значения в таблице уникальны, то решение с помощью любой из этих функций даст один и тот же результат.