10

我正在为我的一些在线游戏建立一个排行榜。这是我需要对数据做的事情:

  • 跨多个时间范围(今天、上周、所有时间等)获取给定游戏的玩家排名
  • 获取分页排名(例如最近 24 小时的最高分,获取排名 25 到 50 之间的玩家,获取排名或单个用户)

我使用下表定义和索引进行了定义,我有几个问题。

考虑到我的场景,我是否有一个好的主键?我在 gameId、playerName 和 score 中有一个聚集键的原因仅仅是因为我想确保给定游戏的所有数据都在同一个区域中并且该分数已经排序。大多数情况下,我将显示给定 gameId 的得分降序排列(+updateDateTime 表示平局)。这是一个正确的策略吗?换句话说,我想确保我可以运行我的查询以尽可能快地获得我的玩家的排名。

CREATE TABLE score (
    [gameId]            [smallint] NOT NULL,
    [playerName]        [nvarchar](50) NOT NULL,
    [score]             [int] NOT NULL,
    [createdDateTime]   [datetime2](3) NOT NULL,
    [updatedDateTime]   [datetime2](3) NOT NULL,
PRIMARY KEY CLUSTERED ([gameId] ASC, [playerName] ASC, [score] DESC, [updatedDateTime] ASC)

CREATE NONCLUSTERED INDEX [Score_Idx] ON score ([gameId] ASC, [score] DESC, [updatedDateTime] ASC) INCLUDE ([playerName])

下面是我将用来获取玩家排名的查询的第一次迭代。但是,我对执行计划有点失望(见下文)。为什么 SQL 需要排序?额外的排序似乎来自 RANK 函数。但是我的数据不是已经按降序排序了吗(基于分数表的聚集键)?我还想知道是否应该对我的表进行更多规范化并移出 Player 表中的 PlayerName 列。我最初决定将所有内容都保存在同一个表中以尽量减少连接数。

DECLARE @GameId AS INT = 0
DECLARE @From AS DATETIME2(3) = '2013-10-01'

SELECT DENSE_RANK() OVER (ORDER BY Score DESC), s.PlayerName, s.Score, s.CountryCode, s.updatedDateTime
FROM [mrgleaderboard].[score] s
WHERE s.GameId = @GameId 
  AND (s.UpdatedDateTime >= @From OR @From IS NULL)

在此处输入图像描述

感谢您的帮助!

4

6 回答 6

10

[更新]

主键不好

您有一个独特的实体,即 [GameID] + [PlayerName]。并且复合聚集索引 > 120 字节与 nvarchar。在相关主题SQL Server - Clustered index design for dictionary中寻找@marc_s 的答案

您的表架构与时间段的要求不匹配

例如:我在星期三获得了 300 分,这个分数存储在排行榜上。第二天我获得了 250 分,但它不会记录在排行榜上,如果我对周二排行榜进行查询,你也不会得到结果

有关完整信息,您可以从历史桌上游戏的得分中获得,但它可能非常昂贵

CREATE TABLE GameLog (
  [id]                int NOT NULL IDENTITY
                      CONSTRAINT [PK_GameLog] PRIMARY KEY CLUSTERED,
  [gameId]            smallint NOT NULL,
  [playerId]          int NOT NULL,
  [score]             int NOT NULL,
  [createdDateTime]   datetime2(3) NOT NULL)

以下是与聚合相关的加速它的解决方案:

  • 历史表的索引视图(参见@Twinkles 的帖子)

您需要 3 个索引视图的 3 个时间段。可能巨大的历史表和 3 个索引视图。无法删除表格的“旧”时段。保存分数的性能问题。

  • 异步排行榜

分数保存在历史表中。SQL 作业/“工人”(或多个)根据计划(每分钟 1 个?)对历史表进行排序并使用预先计算的用户排名填充排行榜表(3 个时间段的 3 个表或一个带时间段键的表)。该表也可以非规范化(具有分数、日期时间、玩家名称和...)。优点:快速阅读(无需排序),快速保存分数,任何时间段,灵活的逻辑和灵活的时间表。缺点:用户已完成游戏但没有立即在排行榜上找到自己

  • 预聚合排行榜

在记录比赛结果的过程中做预处理。在您的情况下,类似于UPDATE [Leaderboard] SET score = @CurrentScore WHERE @CurrentScore > MAX (score) AND ...玩家/游戏 ID,但您只为“所有时间”排行榜这样做。该方案可能如下所示:

CREATE TABLE [Leaderboard] (
    [id]                int NOT NULL IDENTITY
                             CONSTRAINT [PK_Leaderboard] PRIMARY KEY CLUSTERED,
    [gameId]            smallint NOT NULL,
    [playerId]          int NOT NULL,
    [timePeriod]        tinyint NOT NULL,   -- 0 -all time, 1-monthly, 2 -weekly, 3 -daily
    [timePeriodFrom]    date NOT NULL,  -- '1900-01-01' for all time, '2013-11-01' for monthly, etc.
    [score]             int NOT NULL,
    [createdDateTime]   datetime2(3) NOT NULL
    )
playerId timePeriod timePeriodFrom 分数
----------------------------------------------
1 0 1900-01-01 300  
...
1 1 2013-10-01 150
1 1 2013-11-01 300
...
1 2 2013-10-07 150
1 2 2013-11-18 300
...
1 3 2013-11-19 300
1 3 2013-11-20 250
...

因此,您必须更新所有时间段的所有 3 个分数。此外,您可以看到排行榜将包含“旧”时期,例如十月的每月。如果您不需要此统计信息,也许您必须将其删除。优点:不需要历史表。缺点:存储结果的过程复杂。需要维护排行榜。查询需要排序和JOIN

CREATE TABLE [Player] (
    [id]    int NOT NULL IDENTITY CONSTRAINT [PK_Player] PRIMARY KEY CLUSTERED,
    [playerName]        nvarchar(50) NOT NULL CONSTRAINT [UQ_Player_playerName] UNIQUE NONCLUSTERED)

CREATE TABLE [Leaderboard] (
    [id]                int NOT NULL IDENTITY CONSTRAINT [PK_Leaderboard] PRIMARY KEY CLUSTERED,
    [gameId]            smallint NOT NULL,
    [playerId]          int NOT NULL,
    [timePeriod]        tinyint NOT NULL,   -- 0 -all time, 1-monthly, 2 -weekly, 3 -daily
    [timePeriodFrom]    date NOT NULL,  -- '1900-01-01' for all time, '2013-11-01' for monthly, etc.
    [score]             int NOT NULL,
    [createdDateTime]   datetime2(3) 
)

CREATE UNIQUE NONCLUSTERED INDEX [UQ_Leaderboard_gameId_playerId_timePeriod_timePeriodFrom] ON [Leaderboard] ([gameId] ASC, [playerId] ASC, [timePeriod]  ASC,  [timePeriodFrom] ASC)
CREATE NONCLUSTERED INDEX [IX_Leaderboard_gameId_timePeriod_timePeriodFrom_Score] ON [Leaderboard] ([gameId] ASC, [timePeriod]  ASC,  [timePeriodFrom] ASC, [score] ASC)
GO

-- Generate test data
-- Generate 500K unique players
;WITH digits (d) AS (SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION
   SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION SELECT 0)

INSERT INTO Player (playerName)
SELECT TOP (500000) LEFT(CAST(NEWID() as nvarchar(50)), 20 + (ABS(CHECKSUM(NEWID())) & 15)) as Name
FROM   digits CROSS JOIN digits ii CROSS  JOIN digits iii CROSS  JOIN digits iv CROSS  JOIN digits v CROSS  JOIN digits vi

-- Random score 500K players * 4 games = 2M rows
INSERT INTO [Leaderboard] (
    [gameId],[playerId],[timePeriod],[timePeriodFrom],[score],[createdDateTime])
SELECT  GameID, Player.id,ABS(CHECKSUM(NEWID())) & 3 as [timePeriod], DATEADD(MILLISECOND, CHECKSUM(NEWID()),GETDATE()) as Updated, ABS(CHECKSUM(NEWID())) & 65535 as score
    , DATEADD(MILLISECOND, CHECKSUM(NEWID()),GETDATE()) as Created
FROM (  SELECT 1 as GameID  UNION ALL SELECT 2  UNION ALL SELECT 3  UNION ALL SELECT 4) as Game
    CROSS JOIN Player
ORDER BY NEWID()
UPDATE [Leaderboard] SET [timePeriodFrom]='19000101' WHERE [timePeriod] = 0
GO

DECLARE @From date = '19000101'--'20131108'
    ,@GameID int = 3
    ,@timePeriod tinyint = 0

-- Get paginated ranking 
;With Lb as (
SELECT 
    DENSE_RANK() OVER (ORDER BY Score DESC) as Rnk
    ,Score, createdDateTime, playerId
FROM [Leaderboard]
WHERE GameId = @GameId
  AND [timePeriod] = @timePeriod
  AND [timePeriodFrom] = @From)

SELECT lb.rnk,lb.Score, lb.createdDateTime, lb.playerId, Player.playerName
FROM Lb INNER JOIN Player ON lb.playerId = Player.id
ORDER BY rnk OFFSET 75 ROWS FETCH NEXT 25 ROWS ONLY;

-- Get rank of a player for a given game 
SELECT (SELECT COUNT(DISTINCT rnk.score) 
        FROM [Leaderboard] as rnk 
        WHERE rnk.GameId = @GameId 
            AND rnk.[timePeriod] = @timePeriod
            AND rnk.[timePeriodFrom] = @From
            AND rnk.score >= [Leaderboard].score) as rnk
    ,[Leaderboard].Score, [Leaderboard].createdDateTime, [Leaderboard].playerId, Player.playerName
FROM [Leaderboard]  INNER JOIN Player ON [Leaderboard].playerId = Player.id
where [Leaderboard].GameId = @GameId
    AND [Leaderboard].[timePeriod] = @timePeriod
    AND [Leaderboard].[timePeriodFrom] = @From
    and Player.playerName = N'785DDBBB-3000-4730-B'
GO

这只是表达想法的一个例子。它可以被优化。例如,通过字典表将 GameID、TimePeriod、TimePeriodDate 列合并为一列。该指标的有效性会更高。

PS对不起我的英语。随意修正语法或拼写错误

于 2013-11-12T14:33:29.957 回答
4

您可以查看索引视图以创建常见时间范围的记分牌(今天、本周/月/年、所有时间)。

于 2013-11-14T14:33:03.480 回答
2

要在多个时间范围内获得给定游戏的玩家排名,您将选择游戏并在多个时间范围内按分数进行排名(即排序)。为此,您的非聚集索引可以像这样更改,因为这是您的选择似乎查询的方式。

CREATE NONCLUSTERED INDEX [Score_Idx] 
ON score ([gameId] ASC, [updatedDateTime] ASC, [score] DESC) 
INCLUDE ([playerName])

对于分页排名:

对于 24 小时最高分,我想你会想要一个用户在过去 24 小时内所有游戏中的所有最高分。为此,您将[playername], [updateddatetime]使用[gameid].

对于排名在 25-50 之间的玩家,我假设您正在谈论一款游戏,并且排名很长,您可以翻页。然后,查询将基于[gameid], [score]并稍微基于[updateddatetime]关系。

单用户排名,可能对于每个游戏来说,都比较困难。您需要查询所有游戏的排行榜,以获得玩家在其中的排名,然后过滤玩家。您将需要[gameid], [score], [updateddatetime]然后按播放器过滤。

总结这一切,我建议你保留你的非聚集索引并将主键更改为:

PRIMARY KEY CLUSTERED ([gameId] ASC, [score] DESC, [updatedDateTime] ASC)

对于 24 小时最高分,我认为这可能会有所帮助:

CREATE NONCLUSTERED INDEX [player_Idx] 
ON score ([playerName] ASC) 
INCLUDE ([gameId], [score])

dense_rank 查询排序,因为它选择[gameId], [updatedDateTime], [score]. 请参阅我对上面非聚集索引的评论。

我还会三思而后行,将其包含[updatedDateTime]在您的查询中,然后再包含在您的索引中。也许有时两个玩家获得相同的排名,为什么不呢?[updatedDateTime]会让你的指数显着膨胀。

您也可能会考虑按[gameid].

于 2013-11-19T15:11:10.460 回答
1

有点偏题:

问问自己,排行榜中的分数实际上需要有多准确和最新?

作为一名球员,我不在乎我是世界第 142134 号还是第 142133 号。我确实在乎我是否超过了朋友的确切分数(但我只需要将我的分数与其他几个分数进行比较),我想知道我的新高分让我从大约 142000 到大约 90000。(耶!)

因此,如果您想要真正快速的排行榜,您实际上并不需要所有数据都是最新的。您可以每天或每小时计算排行榜的静态排序副本,并在显示玩家 X 的分数时,显示它在静态副本中的排名。

与朋友比较时,最后一分钟的更新确实很重要,但您只处理几百个分数,因此您可以在最新的排行榜中查看他们的实际分数。

哦,我当然关心前 10 名。仅根据他们得分如此之高的事实将他们视为我的“朋友”,并显示这些值是最新的。

于 2013-11-19T13:32:50.717 回答
0

您的聚集索引是复合索引,因此这意味着该顺序由多个列定义。您请求ORDER BY Score哪一列是聚集索引中的第二列。因此,索引中的条目不一定按 的顺序排列Score,例如条目

1, 2, some date
2, 1, some other date

如果您选择 just Score,则顺序为

2
1

需要排序。

于 2013-11-12T06:02:29.860 回答
0

我不会将“分数”列放入聚集索引中,因为它可能会一直更改......并且作为聚集索引一部分的列的更新会很昂贵

于 2013-11-12T06:36:18.320 回答