【问题标题】:How to efficiently get a range of ranked users (for a leaderboard) using Postgresql如何使用 Postgresql 有效地获得一系列排名用户(用于排行榜)
【发布时间】:2015-10-24 09:22:41
【问题描述】:

我已经阅读了很多关于这个主题的帖子,例如 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
  • 性能必须可扩展,例如对于 100,000 个用户,时间低于 80 毫秒。

尝试 #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 个用户有随机评分。但是,这只会返回具有特定 id (2000) 的用户的排名。

我看不到任何有效的方法来扩展此查询以获得一系列排名。任何扩展它的尝试都会使查询变得非常缓慢。

我只知道“中心”用户的 ID,因为在我们知道哪些用户在范围内之前,用户必须按等级排序!

尝试 #3:内存中的有序树

我最终使用 Java TreeSet 来存储排名。每当将新用户插入数据库或用户评分发生变化时,我都可以更新 TreeSet。

这非常快,大约 25 毫秒,有 100,000 个用户。

但是,它有一个严重的缺点,即它只在为请求提供服务的 Webapp 节点上更新。我正在使用 Heroku,将为我的应用部署多个节点。所以,我需要为服务器添加一个计划任务,以每小时重新构建这个排名树,以确保节点不会太不同步!

如果有人知道在 Postgres 中使用完整解决方案执行此操作的有效方法,那么我会全力以赴!

【问题讨论】:

  • 根据您的更新工作方式,或许可以考虑物化视图?
  • 在你的第一次尝试中,为什么你在 over 函数中排序desc 然后你又重新排序为 asc?我认为如果你只使用一个订单(在你的情况下是 over 函数),它可能会更快。
  • 就 Node JS 而言,对于性能关键的请求,请考虑使用 github.com/brianc/node-pg-query-stream
  • 你在使用redis吗?如果你是 redis zorted 套装是为此目的量身定做的,而且速度非常快且可扩展。

标签: sql postgresql


【解决方案1】:

您可以通过使用order by rating descoffsetlimit 获得相同的结果来获取某个排名之间的用户。

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! 您的解决方案并不能完全满足我的需求,但它使我朝着正确的方向前进。 这是我现在要使用的完整解决方案。

    您的解决方案的唯一限制是无法获得特定用户的唯一排名。具有相同评级的所有用户将具有相同的排名(或至少 SQL 标准未定义)。如果我提前知道OFFSET,那么你的排名就足够了,但我必须先获得特定用户的排名。

    我的解决方案是执行以下查询以获得一系列排名:

    SELECT * FROM my_game_users ORDER BY rating DESC, id ASC LIMIT ? OFFSET ?

    这基本上是根据评分唯一地定义排名,然后是先加入游戏的人(较低的 id)。 为了提高效率,我在 (rating DESC, id) 上创建了一个索引

    然后,我将获取特定用户的排名以插入此查询:

    SELECT COUNT(*) FROM my_game_users WHERE rating &gt; ? OR (rating = ? AND id &lt; ?)

    我实际上通过以下方式提高了效率:

    SELECT (SELECT COUNT(*) FROM my_game_users WHERE rating &gt; ?) + (SELECT COUNT(*) FROM my_game_users WHERE rating = ? AND id &lt; ?) + 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)"

    因此,它使用了 rating 和 id 索引,但它仍然具有从 0.29...12704.83 的高度可变成本。有什么改进的方法吗??

    【讨论】:

    • 查看我的更新,您可以根据需要将 rank() 与 row_number() 互换。应该比较快
    【解决方案3】:

    如果您按 desc 顺序订购,则说明顺序正确。使用 rownumber() 函数。 Select Row number in postgres

    您还可以使用内存缓存将内容存储在内存中。类似redis的东西。它是一个独立的应用程序,可以为多个实例提供服务,甚至可以远程服务。

    【讨论】:

    • 您先生,您没有阅读完整的帖子。我已经尝试过 row_number(),但是对于 100,000 个用户来说速度太慢了!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-05-24
    • 2015-01-29
    • 1970-01-01
    • 2021-10-07
    • 1970-01-01
    • 2020-05-26
    • 2019-03-22
    相关资源
    最近更新 更多