Как эффективно получить ряд ранжированных пользователей (для лидеров), используя Postgresql
Я прочитал много сообщений по этой теме, таких как
mysql-get-rank-from-leaderboards.
Однако ни одно из решений не эффективно в масштабе для получения диапазона рангов из базы данных.
Проблема проста. Предположим, что у нас есть таблица Postgres с столбцом "id" и другим столбцом INTEGER, значения которого не уникальны, но у нас есть индекс для этого столбца.
например. таблица может быть:
CREATE TABLE my_game_users (id serial PRIMARY KEY, rating INTEGER NOT NULL);
Цель
- Определите ранг для пользователей, заказывающих пользователей в столбце "рейтинг" по убыванию.
- Уметь запрашивать список из ~ 50 пользователей, заказанных этим новым "рангом", с центром в любом конкретном пользователе.
- Например, мы можем возвращать пользователей с рангом {15, 16,..., 64, 65}, где центральный пользователь имеет ранг № 40
- Производительность должна масштабироваться, например. не менее 80 мс для 100 000 пользователей.
Попытка # 1: функция окна row_number()
WITH my_ranks AS
(SELECT my_game_users.*, row_number() OVER (ORDER BY rating DESC) AS rank
FROM my_game_users)
SELECT *
FROM my_ranks
WHERE rank >= 4000 AND rank <= 4050
ORDER BY rank ASC;
Это "работает", но запросы усредняют 550 мс с 100 000 пользователей на быстром ноутбуке без какой-либо другой реальной работы.
Я попробовал добавить индексы и перефразировал этот запрос, чтобы не использовать синтаксис "WITH", и ничего не помогло ускорить его.
Попытка # 2 - подсчет количества строк с большим значением оценки
Я попробовал такой запрос:
SELECT t1.*,
(SELECT COUNT(*)
FROM my_game_users t2
WHERE (t1.rating, -t1.id) <= (t2.rating, -t2.id)
) AS rank
FROM my_game_users t1
WHERE id = 2000;
Это прилично, этот запрос занимает около 120 мс, при этом 100 000 пользователей имеют случайные рейтинги. Однако это возвращает только ранг для пользователя с определенным идентификатором (2000).
Я не вижу эффективного способа расширить этот запрос, чтобы получить ряд рангов. Любая попытка расширить это делает очень медленный запрос.
Я знаю только идентификатор пользователя "center", так как пользователи должны быть упорядочены по рангу, прежде чем мы узнаем, какие из них находятся в диапазоне!
Попытка # 3: упорядоченное в памяти дерево
В итоге я использовал Java TreeSet для хранения рангов. Я могу обновить TreeSet всякий раз, когда новый пользователь вставлен в базу данных или изменяется рейтинг пользователя.
Это супер быстрый, около 25 мс с 100 000 пользователей.
Однако у него есть серьезный недостаток, который он обновил только на Webapp node, обслуживающем запрос. Я использую Heroku и развожу несколько узлов для своего приложения. Таким образом, мне нужно было добавить запланированную задачу для сервера, чтобы каждый раз создавать таблицу ранжирования, чтобы убедиться, что узлы не слишком из-за синхронизации!
Если кто-нибудь знает об эффективном способе сделать это в Postgres с полным решением, то я все уши!
Ответы
Ответ 1
Вы можете получить те же результаты, используя order by rating desc
и offset
и limit
, чтобы получить пользователей от определенного ранга.
WITH my_ranks AS
(SELECT my_game_users.*, row_number() OVER (ORDER BY rating DESC) AS rank FROM my_game_users)
SELECT * FROM my_ranks WHERE rank >= 4000 AND rank <= 4050 ORDER BY rank ASC;
Вышеуказанный запрос совпадает с
select * , rank() over (order by rating desc) rank
from my_game_users
order by rating desc
limit 50 offset 4000
Если вы хотите выбрать пользователей вокруг ранга № 40, вы можете выбрать ранжирование # 15- # 65
select *, rank() over (order by rating desc) rank
from my_game_users
order by rating desc
limit 50 offset 15
Ответ 2
Спасибо, @FuzzyTree!
Ваше решение не дает мне все, что мне нужно, но оно подтолкнуло меня в правильном направлении. Здесь полное решение, на котором я собираюсь сейчас.
Единственное ограничение с вашим решением заключается в том, что нет способа получить уникальный ранг для определенного пользователя. Все пользователи с одинаковым рейтингом будут иметь одинаковый ранг (или, по крайней мере, это undefined по стандарту SQL). Если бы я знал OFFSET раньше времени, то ваш рейтинг был бы достаточно хорош, но я должен сначала получить ранг определенного пользователя.
Мое решение состоит в том, чтобы выполнить следующий запрос, чтобы получить ряд рангов:
SELECT * FROM my_game_users ORDER BY rating DESC, id ASC LIMIT ? OFFSET ?
Это в основном уникальное определение рангов по рейтингу, а затем кто присоединился к игре сначала (нижний id).
Чтобы сделать это эффективным, я создаю индекс (рейтинг DESC, id)
Затем я получаю определенный пользовательский ранг для подключения к этому запросу с помощью:
SELECT COUNT(*) FROM my_game_users WHERE rating > ? OR (rating = ? AND id < ?)
Я действительно сделал это более эффективным:
SELECT (SELECT COUNT(*) FROM my_game_users WHERE rating > ?) + (SELECT COUNT(*) FROM my_game_users WHERE rating = ? AND id < ?) + 1
Теперь даже с этими запросами требуется около 78 мс среднего и среднего времени, чтобы получить ряды вокруг пользователя. Если у кого-то есть хорошая идея, как ускорить их, я все уши!
Например, получение диапазона рангов занимает около 60 мс, и объяснение этого дает:
EXPLAIN SELECT * FROM word_users ORDER BY rating DESC, id ASC LIMIT 50 OFFSET 50000;
"Limit (cost=6350.28..6356.63 rows=50 width=665)"
" -> Index Scan using idx_rating_desc_and_id on word_users (cost=0.29..12704.83 rows=100036 width=665)"
Таким образом, он использует рейтинг и индекс id, но он все еще имеет эту переменную стоимость от 0.29... 12704.83. Любые идеи о том, как улучшить?
Ответ 3
Если вы закажете его в порядке убывания, у вас есть его в правильном порядке. Используйте функцию rownumber().
Выберите номер строки в postgres
Также вы должны использовать кеш в памяти для хранения данных в памяти. Что-то вроде redis. Это отдельное приложение, которое может обслуживать несколько экземпляров даже удаленно.