SQL Server排名查询优化

时间:2017-12-08 15:54:04

标签: sql sql-server tsql query-optimization ranking

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

我正在努力确定如何改进它。

这是当前的SQL:

Just :: a -> Maybe a
     -- ((->) a) (Maybe a)
     -- f (g a)   for f ~ ((->) a)  and  g ~ Maybe

(fmap . fmap) :: (a   -> b) -> f (g a  ) -> f (g b)
     -- Num x => ([x] -> x) -> f (g [x]) -> f (g x)
     -- Num x => ([x] -> x) -> ([x] -> Maybe [x]) -> [x] -> Maybe x
     --          ^             ^                     ^
     --          sum           Just                  [1,2,3]

详细说明:

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是主键
  • id列已编入索引

country表:

  • Leadeboard是主键
  • id列已编入索引

enter image description here

执行计划显示2个警告:

  

没有统计数据的列:[dbo]。[排行榜] .id(费用23%)

     

没有统计数据的列:[dbo]。[用户] .id(成本28%)

9 个答案:

答案 0 :(得分:5)

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

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

答案 1 :(得分:3)

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

根据您的评论,如果我的第一个建议没有改善性能,那么我认为您唯一能做的就是:首先,确保您创建了所有需要的索引和统计WHERE子句中删除计算,因为没有必要,并且使用JOIN 而不是在where子句中链接表(使用加入并没有提高绩效,但它是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

公用表格式

 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

<强>参考

答案 2 :(得分:2)

尝试将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';

答案 3 :(得分:0)

使其更具可读性/可测试性并使用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版本和数据密度,使用countOfUsersWithGreaterScore / usersInCountry作为IN子句可能更有效

答案 4 :(得分:0)

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

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

答案 5 :(得分:0)

我更喜欢连接子查询,下面的查询应该从你的问题中得到相同的结果。

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'

答案 6 :(得分:0)

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

我在国际比赛中遇到同样的问题,球队的表格可能会变得非常大。我从来没有能够获得一个SQL排名查询,以获得良好的用户体验(目标是80毫秒,并且查询比你的更复杂),所以最后决定使用redis server只是为了返回排名。

它提供了一个完全适合这个问题的排名函数。速度很快:1000万参赛者的桌子几毫米。

我仍然认为存储在SQL DB中的分数是真相的来源。 Redis不是ACID。它仅在RAM中保存其数据映像的快照。如果服务器出现故障,它将恢复为上一个快照。因此,redis和真相的来源可能会有所不同。

这在我的案件中没有任何问题,因为即时返回的队伍被认为是非正式的,等待法官的最终审查。由于快照重新启动而丢失的数据是“自我修复”。也就是说,如果我查询团队的级别并且它不在redis商店中,我会添加它然后重新查询。我还运行了每日同步工作以恢复完美的协议。我可以随时运行此同步以从头开始初始化新的redis。

该方案已经证明非常快速和稳健7年。它替换的实现使用了基于BerkeleyDB的自定义服务。那一个在过去的7年中运作良好。

另一点是,redis服务可以非常方便用于缓存等其他目的。

答案 7 :(得分:0)

也许尝试非规范化?对于排行榜表格中的每一行,请包含用户的国家/地区。

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

然后你可以得到你不需要连接或子选择的搜索计数 - 它只是一个表上的选择(而且更快,因为你将使用int)。

答案 8 :(得分:-1)

尝试类似:

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