在空闲时间,我开始编写一个带有数据库后端的小型多人游戏。我希望将玩家登录信息与游戏中的其他信息(库存、统计数据和状态)区分开来,一位朋友提出这可能不是最好的主意。
把所有东西放在一张桌子上会更好吗?
在空闲时间,我开始编写一个带有数据库后端的小型多人游戏。我希望将玩家登录信息与游戏中的其他信息(库存、统计数据和状态)区分开来,一位朋友提出这可能不是最好的主意。
把所有东西放在一张桌子上会更好吗?
从忽略性能开始,并尽可能创建最合乎逻辑且易于理解的数据库设计。如果玩家和游戏对象(剑?棋子?)在概念上是不同的东西,那么将它们放在不同的表中。如果玩家可以携带东西,则在引用“玩家”表的“东西”表中放置一个外键。等等。
然后,当你有成百上千的玩家和数以千计的东西时,玩家在游戏中跑来跑去,做一些需要数据库搜索的事情,那么,如果你添加适当的索引,你的设计仍然足够快。
当然,如果您计划同时拥有数千名玩家,每个人每秒都在清点自己的东西,或者可能会进行其他一些巨大的数据库搜索负载(搜索图形引擎渲染的每一帧?),那么您将需要考虑表现。但在这种情况下,一个典型的基于磁盘的关系数据库无论如何都不够快,无论您如何设计表。
关系数据库在集合上工作,数据库查询在那里不提供(“未找到”)或更多结果。关系数据库并不打算通过执行查询(在关系上)总是提供一个结果。
说我想提一下,也有现实生活;-)。一对一的关系可能是有意义的,即如果表的列数达到临界水平,则拆分该表可能会更快。但这种情况确实很少见。
如果您真的不需要拆分表格,请不要这样做。至少我假设在您的情况下,您必须对两个表进行选择才能获取所有数据。这可能比将所有内容存储在一张表中要慢。通过这样做,您的编程逻辑至少会变得不那么可读。
伊迪丝说: 关于规范化的评论:嗯,我从来没有见过一个数据库规范化到它的最大值,没有人真的推荐它作为一个选项。无论如何,如果您不完全知道以后是否会规范任何列(即放入其他表中),那么使用一个表而不是“表”-一对一-“其他表”也更容易做到这一点"
是否保留
玩家登录信息 [独立] 与其他游戏信息(库存、统计数据和状态)
通常是标准化的问题。您可能想快速查看维基百科(第三范式,非规范化)定义。是否将它们分成多个表取决于存储的数据。扩展您的问题将使这一点变得具体。我们要存储的数据是:
我们可以将其存储为单个表或多个表。
单表示例
如果玩家在这个游戏世界中只有一个角色,那么一张桌子可能是合适的。例如,
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);
在查看玩家和角色的信息时,这种格式确实需要连接;但是,它使更新记录不太可能导致不一致。后一种说法通常在提倡使用多个表时使用。例如,如果您有一个玩家 (LotRFan) 有两个字符 (gollum, frodo) 的单表格式,您的用户名、密码、电子邮件字段将重复。如果玩家想更改他的电子邮件/密码,您需要修改这两条记录。如果只修改了一条记录,则该表将变得不一致。但是,如果 LotRFan 想在多表格式中更改他的密码,则表中的单行会被更新。
其他注意事项
使用单表还是多表取决于底层数据。这里没有描述但在其他答案中注意到的是优化通常需要两个表并将它们组合成一个表。
同样有趣的是了解将要进行的更改的类型。例如,如果您选择为可能有多个角色的玩家使用单个表格,并希望通过增加玩家表格中的字段来跟踪玩家发出的命令总数;这将导致在单个表示例中更新更多行。
无论如何,库存是一种多对一的关系,因此可能值得拥有自己的表(至少在其外键上建立索引)。
您可能还希望将每个用户的个人内容与公共内容(即其他人可以看到的内容)分开。这可能会提供更好的缓存命中率,因为很多人会看到您的公共属性,但只有您看到您的个人属性。
1-1 关系只是垂直分割。没有其他的。如果您出于某种原因不会将所有数据存储在同一个表中(例如跨多个服务器进行分区等),请执行此操作
如您所见,对此的普遍共识是“视情况而定”。正如@Campbell 和其他人所提到的,性能/查询优化很容易浮现在脑海中。
但我认为对于您正在构建的那种特定于应用程序的数据库,最好从考虑整体应用程序设计开始。对于特定于应用程序的数据库(与设计用于许多应用程序或报告工具的数据库相反),“理想”数据模型可能是最能映射到应用程序代码中实现的域模型的数据模型。
您可能不会将您的设计称为MVC,而且我不知道您使用的是什么语言,但我希望您有一些代码或类可以用逻辑模型包装数据访问。你如何看待这个级别的应用程序设计是我认为你的数据库应该如何构建的最佳指标。
一般来说,这意味着域对象到数据库表的一对一映射是最好的起点。
我认为最好用例子来说明我的意思:
您认为是Player类。Player.authenticate、Player.logged_in?、Player.status、Player.hi_score、Player.gold_credits。只要所有这些都是针对单个用户的操作,或者代表用户的单值属性,那么从逻辑应用程序设计的角度来看,单个表应该是您的起点。单班(玩家),单桌(玩家)。
也许我不喜欢瑞士军刀Player类的设计,但更喜欢从User、Inventory、Scoreboard和Account的角度来考虑。User.authenticate、User.logged_in?、User->account.status、Scoreboard.hi_score(User)、User->account.gold_credits。
我这样做可能是因为我认为在域模型中分离这些概念将使我的代码更清晰、更容易编写。在这种情况下,最好的起点是将域类直接映射到各个表:用户、库存、分数、帐户等。
我在上面漏掉了一些词来表明域对象到表的一对一映射是理想的、合乎逻辑的设计。
这是我首选的起点。试图变得太聪明——比如将多个类映射回一个表——往往只会招致我希望避免的麻烦(尤其是死锁和并发问题)。
但这并不总是故事的结局。有一些实际考虑可能意味着需要单独的活动来优化物理数据模型,例如并发/锁定问题?行大小变得太大?索引开销?查询性能等
但是在考虑性能时要小心:仅仅因为它可能是一张表中的一行,可能并不意味着它只查询一次即可获得整行。我想多用户游戏场景可能涉及在不同时间获取不同玩家信息的一系列调用,并且玩家总是想要可能非常动态的“当前值”。这可能会转化为每次只对表中的几列进行多次调用(在这种情况下,多表设计可能比单表设计更快)。
我建议将它们分开。不是出于任何数据库原因,而是出于开发原因。
当您编写玩家登录模块时,您不想考虑任何游戏信息。反之亦然。如果表是不同的,则维护关注点分离会更容易。
这归结为这样一个事实,即您的游戏信息模块与玩家登录表没有任何关系。
在这种情况下,最好将其放在一个表中,因为您的查询性能会更好。
当存在重复数据元素时,您真的只想使用下一个表,您应该通过规范化来减少数据存储开销。
我建议您将每种记录类型保存在自己的表中。
玩家登录是一种记录。库存是另一个。他们不应该共享一个表格,因为大多数信息都没有共享。通过分离不相关的信息来保持表格简单。
我同意Thomas Padron-McCarthy 的回答:
解决成千上万同时用户的情况不是您的问题。让人们玩你的游戏是个问题。从最简单的设计开始 - 完成游戏、工作、可玩且易于扩展。如果游戏起飞,那么你就会去规范化。
还值得一提的是,可以将索引视为包含更窄数据视图的单独表。
人们已经能够推断出暴雪为魔兽世界使用的设计。似乎玩家正在进行的任务与角色一起存储。例如:
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 才能获得所有这些信息。