У меня есть таблица лидеров, которая выглядит так:
|--------------------------------------|
| userId | allTimePoints | allTimeRank |
|--------------------------------------|
| .. | ... | ... |
| xx | 5555555 | ? |
| .. | ... | ... |
----------------------------------------
Предположим, что в таблице миллион записей и что allTimePoints
постоянно обновляется. Когда пользователь просит показать таблицу лидеров, я хотел бы иметь возможность показать ему его рейтинг, счет, а также ближайших конкурентов. Я хотел бы добиться следующего:
- выяснить ранг каждого пользователя (сортировать таблицу по allTimePoints DESC)
- выяснить смещение страницы, чтобы средство просмотра таблицы лидеров находилось в середине сокращенного набора результатов
- сделать это в течение приемлемого времени выполнения (например, создать ощущение мгновенного отклика, даже если сотни тысяч других пользователей одновременно попадают на экран таблицы лидеров)
Я начал так, и на моей машине это занимает около 0,4 секунды, когда в таблице 1 миллион строк.
SET @rowIndex := 0;
SET @rank := 0;
SET @prev := NULL;
SET @userIdPosition := 0;
SELECT
@rowIndex := @rowIndex+1 AS rowIndex,
userId,
@rank := IF(@prev=allTimePoints, @rank, @rank+1) AS rank,
@prev := allTimePoints AS allTimePoints,
@userIdPosition := IF(userId=1860, @rowIndex, @userIdPosition) AS requestedOffset
FROM Leaderboard
ORDER BY allTimePoints DESC;
Кстати, преимущество этого метода во время выполнения по сравнению с использованием самосоединения описано здесь (это намного быстрее): http://code.openark.org/blog/mysql/sql-ranking-without-self.-join
Я сохраняю rowIndex
и rank
как отдельные переменные, чтобы можно было более точно рассчитать смещение страницы запрашивающего пользователя, если есть связи в рейтинге (т. е. n пользователей имеют одинаковую оценку).
Пока все хорошо, хотя я опасаюсь, что если время выполнения не уменьшится до миллисекунд, это будет нежизнеспособно, когда сотни тысяч пользователей одновременно запускают запрос.
Что еще хуже, если я расширим этот запрос, чтобы он правильно работал с пейджингом, как описано выше, время выполнения увеличится до 1,5 с.
SET @rowIndex := 0;
SET @rank := 0;
SET @prev := NULL;
SET @userIdPosition := 0;
SELECT sortedL.userId, sortedL.rank, sortedL.allTimePoints
FROM
(SELECT
@rowIndex := @rowIndex+1 AS rowIndex,
userId,
@rank := IF(@prev=allTimePoints, @rank, @rank+1) AS rank,
@prev := allTimePoints AS allTimePoints,
@userIdPosition := IF(userId=1860, @rowIndex, @userIdPosition) AS requestedOffset
FROM Leaderboard
ORDER BY allTimePoints DESC) AS sortedL
-- simulate paging, as LIMIT doesn't seem to accept variables
WHERE sortedL.rowIndex > sortedL.requestedOffset -15 AND sortedL.rowIndex < sortedL.requestedOffset + 15;
Это возвращает 29 пользователей, а запрашивающий пользователь находится посередине, как и хотелось.
Если я запускаю это с EXPLAIN
, я вижу, что подзапрос использует FILESORT, но результаты не индексируются, и, следовательно, внешний SELECT вынужден выполнить еще одно полное сканирование набора результатов, используя WHERE (медленнее, чем FILESORT).
Вопросы (1): как это оптимизировать?
Другая идея состояла в том, чтобы хранить рейтинг в индексированном столбце: allTimeRank
. Я подумал, что поэкспериментирую с сортировкой таблицы в процедуре по расписанию (скажем, каждые 10 минут), а затем предложу очень быстрый доступ с помощью более простого SELECT, который будет использовать индекс. Мне не удалось заставить это работать должным образом, похоже, оно не использует условие в моем предложении WHERE (ранжирование, хранящееся в allTimeRank
, неверно, и MySQL жалуется, поэтому мне нужно отключить безопасные обновления, чтобы получить его хоть беги)
SET SQL_SAFE_UPDATES=0;
SET @rowIndex := 0;
SET @rank := 0;
SET @prev := NULL;
SET @userIdPosition := 0;
UPDATE Leaderboard L,
(SELECT
@rowIndex := @rowIndex+1 AS rowIndex,
userId,
@rank := IF(@prev=allTimePoints, @rank, @rank+1) AS rank,
@prev := allTimePoints AS allTimePoints,
@userIdPosition := IF(userId=1860, @rowIndex, @userIdPosition) AS requestedOffset
FROM Leaderboard
ORDER BY allTimePoints DESC) AS sortedL
SET L.allTimeRank = sortedL.rank
WHERE sortedL.userId = L.userId;
SET SQL_SAFE_UPDATES=1;
Вопрос (2): как заставить работать условие WHERE.
Это заняло от 2 минут до 12 секунд. Не знаю, почему несоответствие. В любом случае это заблокирует ОБНОВЛЕНИЯ от пользователей, которые набирают очки, создавая ощущение, что приложение зависло. Вопрос (3): есть ли обходной путь?