PostgreSQL ROW_NUMBER: ранг в топе SQL таблице
Как правильно получить порядковый номер (ранг) пользователя в отсортированной таблице топов PostgreSQL с помощью ROW_NUMBER(). Исправление ошибки WHERE до оконной функции, примеры CTE и подзапросов для sql postgresql.
Как получить порядковый номер (ранг) записи в отсортированной таблице топов пользователей? Я использую такой запрос, но он всегда возвращает 1 для выбранного пользователя:
self.cursor.execute(f"""
SELECT
ROW_NUMBER() OVER (ORDER BY количествоБуквСообщений DESC) AS row_num, вкИдПользователя
FROM
ТОПЫ_{айдиГруппы}
where
вкИдПользователя = {айдиПользователя} """)
Пример данных в таблице (топ по количеству сообщений и букв):
Страница 1/1 Рейтинг в группе
Количество сообщений | Количество букв
- @id860862230 - 117 | 1068
- ✨ @id284574337 - 3 | 31
- @id1057274875 - 4 | 20
- @id817637090 - 2 | 0
- @id32483757 - 1 | 0
Запрос возвращает: “Ваша позиция в топе 32483757 1”
Ожидаемая позиция: 5
Как правильно написать SQL-запрос, чтобы получить реальную позицию пользователя в отсортированном топе (PostgreSQL)?
Ваш текущий запрос в PostgreSQL всегда возвращает 1, потому что фильтр WHERE применяется до оконной функции ROW_NUMBER(), нумеруя только одну строку. Чтобы получить реальный ранг (порядковый номер) пользователя в отсортированном топе, сначала присвойте номера всем записям с помощью CTE или подзапроса, а потом отфильтруйте нужного. Это стандартный подход для порядковых номеров SQL в postgresql window функциях — работает быстро даже на больших таблицах.
Содержание
- Проблема с текущим запросом
- Правильный SQL-запрос с CTE
- Альтернатива с подзапросом
- Пример на ваших данных
- ROW_NUMBER() vs RANK() и DENSE_RANK()
- Источники
- Заключение
Проблема с текущим запросом
Представьте: таблица ТОПЫ_123 с топом по количеству букв сообщений. У вас данные вроде:
0001. @id860862230 - 117 сообщений | 1068 букв
0002. @id284574337 - 3 | 31
0003. @id1057274875 - 4 | 20
0004. @id817637090 - 2 | 0
0005. @id32483757 - 1 | 0
Вы хотите ранг для @id32483757 — ожидается 5. Но запрос:
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-инъекций!):
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? Используйте подзапрос. Тот же эффект:
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:
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:
SELECT
DENSE_RANK() OVER (ORDER BY количествоБуквСообщений DESC) AS dense_rank
-- вместо ROW_NUMBER()
Для вашего топа ROW_NUMBER() лучше — уникальные позиции. Но если ничьи важны, меняйте. DatabaseFAQs советует тестировать на реальных данных.
А ранг sql в топ 10 sql? Комбинируйте с LIMIT.
Источники
- PostgreSQL ROW_NUMBER Function - GeeksforGeeks
- Paginating PostgreSQL queries with ROW_NUMBER - Sourcetrail
- PostgreSQL ROW_NUMBER Window Function - PGTutorial
- PostgreSQL Documentation: Window Functions
- Row_Number() vs. Rank() vs. Dense_Rank() - Jess Ramos Data
- PostgreSQL RANK Function - DatabaseFAQs
- RANK() vs DENSE_RANK() vs ROW_NUMBER() - Towards Data Science
- Row_number in PostgreSQL - DatabaseFAQs
- PostgreSQL ROW_NUMBER Tutorial - Galaxy
Заключение
Теперь ваш ранг в PostgreSQL будет точным — используйте CTE с ROW_NUMBER() для порядковых номеров sql в топах. Это решает проблему навсегда, работает на миллионах строк. Протестируйте, добавьте индексы — и топы полетят. Если ничьи — переходите на RANK(). Удачи с sql postgresql!