SQL服务器排名查询优化

SQL Server rank query optimization

我正在尝试优化查询以从数据库中获取特定国家/地区的用户排名。目前看来效率很低。

我正在尝试确定可以做些什么来改进它。

这是当前的SQL:

SELECT COUNT(*) + 1
FROM leaderboard lb, users u
WHERE u.country = 'United States' 
  AND lb.id = u.id 
  AND lb.score + 1 > (SELECT lb2.score 
                      FROM leaderboard lb2
                      WHERE lb2.id = some_user_id);

详情:

Users table:

Leadeboard table:

执行计划显示2个警告:

Columns With No Statistics: [dbo].[leaderboard].id (cost 23%)

Columns With No Statistics: [dbo].[users].id (cost 28%)

我想你正在寻找类似的东西:

SELECT u.id AS user_id, u.country, lu.score, RANK() OVER (ORDER BY lu.score DESC) AS rnk
FROM users u
INNER JOIN leaderboard lu ON u.id = lu.id
WHERE u.country = 'United States'

您可以在此处查看演示:http://rextester.com/KHM76159

增加 readable/testable 并使用 CTE 强制执行步骤

WITH scoreToRank AS (
  SELECT score
  FROM leaderboard
  WHERE id = {some_user_id}
)

, usersInCountry AS (
  SELECT id
  FROM users
  WHERE country = 'United States'
)

, countOfUsersWithGreaterScore AS (
  SELECT COUNT(*) AS count
  FROM leaderboard l
  INNER JOIN usersInCountry u ON u.id = l.id
  WHERE l.score > (SELECT score FROM scoreToRank)
)

SELECT count + 1 AS usersRank FROM countOfUsersWithGreaterScore

根据 SQL 版本和数据密度,将 as IN 子句用于 countOfUsersWithGreaterScore / usersInCountry 可能更有效

尝试将 score 存储在一个变量中,然后在 JOIN 子句中使用它。

declare @score int = ( select top 1 score 
                       FROM leaderboard
                       WHERE id = some_user_id 
                     );

SELECT COUNT(*) + 1 as 'rank'
FROM leaderboard lb
JOIN users u
  ON lb.id = u.id 
 AND lb.score > @score
 AND u.country = 'United States';

你能试试这个吗?它看起来有点奇怪,但我认为它可能有用:

SELECT COUNT(*) + 1
FROM leaderboard lb, users u, leaderboard lb2
WHERE u.country = 'United States' 
  AND lb.id = u.id 
  AND lb.score + 1 > lb2.score AND lb2.id = some_user_id

更新 1:只需从 where 子句中删除计算并使用连接

根据您的评论,如果我的第一个建议没有提高性能,那么我认为您唯一能做的就是:首先,确保您创建了所有需要的索引和统计数据WHERE子句中删除计算,因为它不是必需的,并且使用JOIN 而不是在 where 子句中链接 table(使用联接不会提高性能,但它是 syntax is clearer and less ambiguous

SELECT COUNT(*) + 1
FROM leaderboard lb INNER JOIN users u
ON lb.id = u.id 
WHERE u.country = 'United States' 
AND lb.score  > (SELECT lb2.score 
                  FROM leaderboard lb2
                  WHERE lb2.id = some_user_id)

请注意,如果分数是整数,lb.score + 1> (SELECT lb2.score FROM leaderboard lb2 WHERE lb2.id = some_user_id) 等同于 lb.score >= (SELECT lb2.score FROM leaderboard lb2 WHERE lb2.id = some_user_id),您不需要它。


初始答案:使用带有子查询或 CTE 的排名函数之一

我认为最好使用像RANK()

这样的排名函数

子查询

SELECT * FROM (

    SELECT u.id AS user_id, u.country, lb.score, RANK() OVER (ORDER BY lb.score DESC) AS rnk
    FROM users u
    INNER JOIN leaderboard lb ON u.id = lb.id
    WHERE u.country = 'United States' 

) T1 WHERE T1.user_id = some_user_id

常用table表达式

 WITH CTE_1 AS (

    SELECT u.id AS user_id, u.country, lb.score, RANK() OVER (ORDER BY lb.score DESC) AS rnk
    FROM users u
    INNER JOIN leaderboard lb ON u.id = lb.id
    WHERE u.country = 'United States' 

) SELECT * FROM CTE_1 WHERE CTE_1.user_id = some_user_id

参考资料

我更喜欢连接而不是子查询,下面的查询应该会为您提供与您的问题相同的结果。

SELECT COUNT(*) + 1
FROM leaderboard lb2
    LEFT OUTER JOIN users u ON u.Id <> lb2.Id AND u.country = 'United States'
    LEFT OUTER JOIN leaderboard lb ON lb.Id = u.Id
WHERE lb2.Id = some_user_id AND lb.score >= lb2.score 

如果某些用户没有分数,您应该检查是否存在空值,或者您可以更改加入顺序,这在某些情况下甚至可能更好:

SELECT COUNT(*) + 1
FROM leaderboard lb2
    LEFT OUTER JOIN leaderboard lb ON lb.Id <> lb2.Id AND lb.score >= lb2.score 
    LEFT OUTER JOIN users u ON u.Id = lb.Id
WHERE lb2.Id = some_user_id AND u.country = 'United States'

尝试像这样的东西:

SELECT score FROM leaderboard WHERE id in
    SELECT id FROM users WHERE country='United States' and id=some_user_id

这不会解决您的查询,而是解决整体问题以防有用。

我在一场国际比赛中遇到了同样的问题,那里的队伍 table 可能会变得相当大。我从来没有能够让 SQL 运行king 查询执行得足够好以获得良好的用户体验(目标是 80 毫秒,查询比你的更复杂),所以最终决定使用redis server 仅用于返回 运行ks。

它提供了一个非常适合这个问题的运行king函数。速度很快:table 1000 万参赛者只需几毫秒。

我仍然将存储在 SQL 数据库中的分数视为真实来源。 Redis 不是 ACID。它仅将其在 RAM 中的数据映像的快照保存到磁盘。如果服务器出现故障,它会恢复到最后一个快照。所以 redis 和 source of truth 可能会有点分歧。

这对我来说没有任何问题,因为立即返回 运行ks 被认为是非官方的,等待法官的最终审查。由于从快照重新启动而丢失的数据 "self healing"。也就是说,如果我查询了一个团队的 运行k 而它不在 redis 存储中,我添加它然后重新查询。我还 运行 每日同步作业以恢复完美协议。我可以随时 运行 这个同步来从头开始初始化一个新的 redis。

这个方案在 7 年的时间里被证明是非常快速和稳健的。它替换的实现使用了基于 BerkeleyDB 的自定义服务。在过去的 7 年里,它运行良好。

还有一点是,redis 服务可以非常方便地用于缓存等其他用途。

也许尝试反规范化?对于排行榜中的每一行 table,包括用户所在的国家/地区。

此外,使用 CountryID 而不是国家/地区名称,因为 int 比 varchar 查询速度更快。 (您可以单独查找国家名称。)

然后你可以在不需要连接或子select的情况下得到你正在寻找的计数 - 它只是一个 select 在单个 table 上(和一个更快的, 因为您将使用 int).