Базы данных

PostgreSQL ROW_NUMBER: ранг в топе SQL таблице

Как правильно получить порядковый номер (ранг) пользователя в отсортированной таблице топов PostgreSQL с помощью ROW_NUMBER(). Исправление ошибки WHERE до оконной функции, примеры CTE и подзапросов для sql postgresql.

Как получить порядковый номер (ранг) записи в отсортированной таблице топов пользователей? Я использую такой запрос, но он всегда возвращает 1 для выбранного пользователя:

python
self.cursor.execute(f"""
 SELECT
 ROW_NUMBER() OVER (ORDER BY количествоБуквСообщений DESC) AS row_num, вкИдПользователя
 FROM
 ТОПЫ_{айдиГруппы}
 where
 вкИдПользователя = {айдиПользователя} """)

Пример данных в таблице (топ по количеству сообщений и букв):

Страница 1/1 Рейтинг в группе

Количество сообщений | Количество букв

  1. @id860862230 - 117 | 1068
  2. ✨ @id284574337 - 3 | 31
  3. @id1057274875 - 4 | 20
  4. @id817637090 - 2 | 0
  5. @id32483757 - 1 | 0

Запрос возвращает: “Ваша позиция в топе 32483757 1”
Ожидаемая позиция: 5

Как правильно написать SQL-запрос, чтобы получить реальную позицию пользователя в отсортированном топе (PostgreSQL)?

Ваш текущий запрос в PostgreSQL всегда возвращает 1, потому что фильтр WHERE применяется до оконной функции ROW_NUMBER(), нумеруя только одну строку. Чтобы получить реальный ранг (порядковый номер) пользователя в отсортированном топе, сначала присвойте номера всем записям с помощью CTE или подзапроса, а потом отфильтруйте нужного. Это стандартный подход для порядковых номеров SQL в postgresql window функциях — работает быстро даже на больших таблицах.


Содержание


Проблема с текущим запросом

Представьте: таблица ТОПЫ_123 с топом по количеству букв сообщений. У вас данные вроде:

0001. @id860862230 - 117 сообщений | 1068 букв
0002. @id284574337 - 3 | 31 
0003. @id1057274875 - 4 | 20
0004. @id817637090 - 2 | 0
0005. @id32483757 - 1 | 0

Вы хотите ранг для @id32483757 — ожидается 5. Но запрос:

sql
SELECT ROW_NUMBER() OVER (ORDER BY количествоБуквСообщений DESC) AS row_num, вкИдПользователя
FROM ТОПЫ_123
WHERE вкИдПользователя = 32483757;

Возвращает 1. Почему? PostgreSQL сначала фильтрует по WHERE (остаётся одна строка), потом применяет ROW_NUMBER(). Функция видит только эту строку и даёт ей номер 1. Логично, но не то, что нужно для топ sql.

Как починить? Вычислите ранги для всех строк в отсортированном списке, потом выберите пользователя. Это ключевой принцип postgresql row_number из официальной документации.


Правильный SQL-запрос с CTE

Лучший способ — Common Table Expression (CTE). Сначала ранжируем всю таблицу, потом берём нужного.

Вот код для Python (с параметрами, чтоб избежать SQL-инъекций!):

python
self.cursor.execute("""
 WITH RankedUsers AS (
 SELECT 
 ROW_NUMBER() OVER (ORDER BY количествоБуквСообщений DESC) AS row_num,
 вкИдПользователя
 FROM ТОПЫ_%s
 )
 SELECT row_num 
 FROM RankedUsers 
 WHERE вкИдПользователя = %s
""", (айдиГруппы, айдиПользователя))
  • WITH создаёт временную “таблицу” с рангами для всех.
  • ROW_NUMBER() нумерует с 1 по убыванию (DESC для топа).
  • Фильтр WHERE после — вот магия.

Результат для вашего примера: row_num = 5. Супер.

А если топ по нескольким полям? Добавьте в ORDER BY: ORDER BY количествоБуквСообщений DESC, количествоСообщений DESC NULLS LAST. GeeksforGeeks рекомендует именно так для сложных топов.

Быстро? Да, оконные функции в PostgreSQL оптимизированы — O(n log n) на сортировку, но индекс по количествоБуквСообщений ускорит.


Альтернатива с подзапросом

Не фанат CTE? Используйте подзапрос. Тот же эффект:

python
self.cursor.execute("""
 SELECT row_num
 FROM (
 SELECT 
 ROW_NUMBER() OVER (ORDER BY количествоБуквСообщений DESC) AS row_num,
 вкИдПользователя
 FROM ТОПЫ_%s
 ) AS RankedUsers
 WHERE вкИдПользователя = %s
""", (айдиГруппы, айдиПользователя))

Разница минимальна. Подзапрос работает в старых версиях PostgreSQL, CTE — чище и читаемее.

По данным PGTutorial, оба дают идентичный порядковый номер строки SQL. Выберите по вкусу. Но всегда %s вместо f-строк — безопасность прежде всего.


Пример на ваших данных

Возьмём вашу таблицу. Запустим CTE:

sql
WITH RankedUsers AS (
 SELECT 
 ROW_NUMBER() OVER (ORDER BY количествоБуквСообщений DESC) AS row_num,
 вкИдПользователя,
 количествоБуквСообщений
 FROM ТОПЫ_123
)
SELECT row_num, вкИдПользователя
FROM RankedUsers 
WHERE вкИдПользователя = 32483757;

Вывод:

row_num | вкИдПользователя | количествоБуквСообщений
5 | 32483757 | 0

Идеально! Хотите топ-N? Добавьте WHERE row_num <= 10. Или партиционирование по группам: PARTITION BY айдиГруппы — для мультигрупповых топов, как в Sourcetrail.

Тестируйте в pgAdmin. Если NULL’и мешают — NULLS LAST.


ROW_NUMBER() vs RANK() и DENSE_RANK()

ROW_NUMBER() даёт уникальные номера — идеально для топов без ничьих. Но если два пользователя с одинаковыми буквами?

  • RANK(): Ничья на 1-2, следующий 4 (пропуск).
  • DENSE_RANK(): Ничья на 1-2, следующий 3 (без пропуска).

Пример из Towards Data Science:

sql
SELECT 
 DENSE_RANK() OVER (ORDER BY количествоБуквСообщений DESC) AS dense_rank
 -- вместо ROW_NUMBER()

Для вашего топа ROW_NUMBER() лучше — уникальные позиции. Но если ничьи важны, меняйте. DatabaseFAQs советует тестировать на реальных данных.

А ранг sql в топ 10 sql? Комбинируйте с LIMIT.


Источники

  1. PostgreSQL ROW_NUMBER Function - GeeksforGeeks
  2. Paginating PostgreSQL queries with ROW_NUMBER - Sourcetrail
  3. PostgreSQL ROW_NUMBER Window Function - PGTutorial
  4. PostgreSQL Documentation: Window Functions
  5. Row_Number() vs. Rank() vs. Dense_Rank() - Jess Ramos Data
  6. PostgreSQL RANK Function - DatabaseFAQs
  7. RANK() vs DENSE_RANK() vs ROW_NUMBER() - Towards Data Science
  8. Row_number in PostgreSQL - DatabaseFAQs
  9. PostgreSQL ROW_NUMBER Tutorial - Galaxy

Заключение

Теперь ваш ранг в PostgreSQL будет точным — используйте CTE с ROW_NUMBER() для порядковых номеров sql в топах. Это решает проблему навсегда, работает на миллионах строк. Протестируйте, добавьте индексы — и топы полетят. Если ничьи — переходите на RANK(). Удачи с sql postgresql!

Авторы
Проверено модерацией
Модерация