在我的空闲时间,我开始编写一个带有数据库后端的小型多人游戏。我想要将玩家登录信息与其他游戏信息(库存、统计数据和状态)分开,但一位朋友提出这可能不是最好的想法。
把所有东西都放在一个表里会更好吗?
在我的空闲时间,我开始编写一个带有数据库后端的小型多人游戏。我想要将玩家登录信息与其他游戏信息(库存、统计数据和状态)分开,但一位朋友提出这可能不是最好的想法。
把所有东西都放在一个表里会更好吗?
首先,忽略性能问题,只需创建一个最合乎逻辑和易于理解的数据库设计。如果玩家和游戏对象(例如剑、棋子)在概念上是不同的物件,则将它们放在不同的表中。如果玩家可以携带物品,则在“物品”表中放置一个引用“玩家”表的外键。依此类推。
然后,当您拥有数百个玩家和成千上万个物品时,而玩家在游戏中奔跑并执行需要进行数据库搜索的操作时,只要加入适当的索引,您的设计仍然足够快速。
当然,如果您计划同时有成千上万的玩家,每秒钟检查自己的物品,或者进行其他大量的数据库搜索(例如图形引擎渲染每一帧都需要搜索),那么您需要考虑性能问题。但是,在这种情况下,典型的基于磁盘的关系型数据库无论如何设计表格都不够快。
关系型数据库基于集合工作,数据库查询可以返回零个(“未找到”)或多个结果。关系型数据库执行查询时并不总是意味着要返回恰好一个结果(在关系上)。
话虽如此,我想提一下现实生活中也存在这样的情况;-)。一对一关系可能是有意义的,例如当表的列数达到临界水平时,拆分此表可能更快。但这些条件确实很少见。
如果你不真正需要拆分表格,请不要这么做。至少我认为,在你的情况下,你必须在两个表格上进行选择才能获取所有数据。这可能比将所有数据存储在一个表中要慢。而且这样做会使你的编程逻辑变得更加难以理解。
Edith说:关于规范化的评论:我从未看到过一个数据库被归一化到极致,也没有人真正推荐这种选项。无论如何,如果你不确定任何列是否稍后会规范化(即放入其他表中),那么使用一个表而不是“表”-一对一-“其他表”的方式更容易。
是否将玩家登录信息(包括库存、统计和状态)与其他游戏信息分开,通常是标准化的问题。您可能希望快速查看维基百科(第三范式, 反规范化)的定义。将其分成多个表取决于存储的数据。扩展您的问题将使其具体化。我们要存储的数据包括:
我们可以将其存储为单个表或多个表。
单个表示例
如果玩家在这个游戏世界里只有一个角色,那么单一的表格可能是合适的。例如,CREATE TABLE players(
id INTEGER NOT NULL,
username VARCHAR2(32) NOT NULL,
password VARCHAR2(32) NOT NULL,
email VARCHAR2(160),
character VARCHAR2(32) NOT NULL,
status VARCHAR2(32),
hitpoints INTEGER,
CONSTRAINT pk_players PRIMARY KEY (uid)
);
-- track information on the player
CREATE TABLE players(
id INTEGER NOT NULL,
username VARCHAR2(32) NOT NULL,
password VARCHAR2(32) NOT NULL,
email VARCHAR2(160),
CONSTRAINT pk_players PRIMARY KEY (id)
);
-- track information on the character
CREATE TABLE characters(
id INTEGER NOT NULL,
player_id INTEGER NOT NULL,
character VARCHAR2(32) NOT NULL,
status VARCHAR2(32),
hitpoints INTEGER,
CONSTRAINT pk_characters PRIMARY KEY (id)
);
-- foreign key reference
ALTER TABLE characters
ADD CONSTRAINT characters__players
FOREIGN KEY (player_id) REFERENCES players (id);
库存是一个多对一的关系,因此可能需要拥有自己的表(至少在外键上建立索引)。
您还可以将每个用户的个人资料与公共资料分开处理(即,其他人可以看到您的资料的部分)。这可能会提供更好的缓存命中率,因为许多人将看到您的公共属性,但只有您才能看到您的个人属性。
一对一关系只是一个垂直分割,没有其他的。如果由于某种原因您不想将所有数据存储在同一张表中(例如为了在多个服务器之间进行分区),则可以使用它。
正如你所看到的,关于这个问题的普遍共识是“视情况而定”。性能/查询优化很容易想到 - 正如@Campbell和其他人所提到的。
但我认为,对于你正在构建的特定应用程序数据库,最好从考虑整体应用程序设计开始。对于特定应用程序数据库(与设计用于与许多应用程序或报告工具一起使用的数据库不同),“理想”的数据模型可能是最能映射到你在应用程序代码中实现的领域模型的那个。
你可能不会称呼你的设计为MVC,我也不知道你使用的是什么语言,但我希望你有一些代码或类来将数据访问与逻辑模型包装在一起。我认为你如何看待应用程序设计在这个层面上是衡量你的数据库应该如何理想地结构的最佳指标。
总的来说,这意味着领域对象与数据库表之间的一对一映射是最好的起点。
我认为最好通过例子来说明我的意思:
你在考虑一个Player类。Player.authenticate,Player.logged_in?,Player.status,Player.hi_score,Player.gold_credits。只要所有这些都是单个用户的操作或代表用户的单值属性,则从逻辑应用程序设计的角度来看,单个表应该是您的起点。单个类(Player),单个表(players)。
也许我不喜欢瑞士军刀式的Player类设计,而更喜欢以User、Inventory、Scoreboard和Account为基础进行思考。User.authenticate,User.logged_in?,User->account.status,Scoreboard.hi_score(User),User->account.gold_credits。
我可能这样做是因为我认为在域模型中将这些概念分开会使我的代码更清晰、更易于编写。在这种情况下,最好的起点是将域类直接映射到各个表:Users、Inventory、Scores、Accounts等。
我在上面插入了一些词语,以表明将域对象与表进行一对一映射是理想的、逻辑上的设计。
这是我的首选起点。试图一开始就变得太聪明——比如将多个类映射回一个表——往往只会招来麻烦,我宁愿避免(特别是死锁和并发问题)。
但这并不总是故事的结局。有实际考虑因素可能意味着需要单独进行优化物理数据模型的活动,例如并发/锁定问题?行大小过大?索引开销?查询性能等。
在考虑性能时要小心:仅因为它可能是一个表中的一行,可能并不意味着只查询一次即可获取整行。我想多用户游戏场景可能涉及到一系列调用,在不同的时间获取不同的玩家信息,而玩家总是希望获得“当前值”,这可能相当动态。这可能会导致每次仅针对表中的几列进行多次调用(在这种情况下,多表设计可能比单表设计更快)。
我建议将它们分开。不是因为任何数据库原因,而是出于开发原因。
当您编写玩家登录模块时,您不希望考虑任何游戏信息。反之亦然。如果表格是不同的,维护关注点的分离将更容易。
归根结底,您的游戏信息模块没有理由去干扰玩家登录表。
在这种情况下,将其放入一个表中可能会使查询性能更好。
只有在存在重复数据元素时,您才需要使用下一个表格,并应考虑规范化以减少数据存储开销。
解决数千个同时用户的情况不是你的问题。让人们玩你的游戏才是问题所在。从最简单的设计开始 - 完成、可运行、可玩和易于扩展的游戏。如果游戏火了,那么再进行反规范化。
值得一提的是,索引可以被看作包含数据更窄视图的单独表。
人们已经能够推断出暴雪公司用于《魔兽世界》的设计。似乎玩家正在进行的任务与角色一起存储。例如:
CREATE TABLE Players (
PlayerID int,
PlayerName varchar(50),
...
Quest1ID int,
Quest2ID int,
Quest3ID int,
...
Quest10ID int,
...
)
最初你最多可以接受10个任务,后来扩展到25个,例如:
CREATE TABLE Players (
PlayerID int,
PlayerName varchar(50),
...
Quest1ID int,
Quest2ID int,
Quest3ID int,
...
Quest25ID int,
...
)
这与分离式设计形成对比:
CREATE TABLE Players (
PlayerID int,
PlayerName varchar(50),
...
)
CREATE TABLE PlayerQuests (
PlayerID int,
QuestID int
)
从概念上讲,后者要容易处理得多,并且它允许任意数量的任务。另一方面,您必须加入Players和PlayerQuests才能获取所有信息。