一对一数据库关系?

5

在我的空闲时间,我开始编写一个带有数据库后端的小型多人游戏。我想要将玩家登录信息与其他游戏信息(库存、统计数据和状态)分开,但一位朋友提出这可能不是最好的想法。

把所有东西都放在一个表里会更好吗?

10个回答

10

首先,忽略性能问题,只需创建一个最合乎逻辑和易于理解的数据库设计。如果玩家和游戏对象(例如剑、棋子)在概念上是不同的物件,则将它们放在不同的表中。如果玩家可以携带物品,则在“物品”表中放置一个引用“玩家”表的外键。依此类推。

然后,当您拥有数百个玩家和成千上万个物品时,而玩家在游戏中奔跑并执行需要进行数据库搜索的操作时,只要加入适当的索引,您的设计仍然足够快速。

当然,如果您计划同时有成千上万的玩家,每秒钟检查自己的物品,或者进行其他大量的数据库搜索(例如图形引擎渲染每一帧都需要搜索),那么您需要考虑性能问题。但是,在这种情况下,典型的基于磁盘的关系型数据库无论如何设计表格都不够快。


5

关系型数据库基于集合工作,数据库查询可以返回零个(“未找到”)或多个结果。关系型数据库执行查询时并不总是意味着要返回恰好一个结果(在关系上)。

话虽如此,我想提一下现实生活中也存在这样的情况;-)。一对一关系可能是有意义的,例如当表的列数达到临界水平时,拆分此表可能更快。但这些条件确实很少见。

如果你不真正需要拆分表格,请不要这么做。至少我认为,在你的情况下,你必须在两个表格上进行选择才能获取所有数据。这可能比将所有数据存储在一个表中要慢。而且这样做会使你的编程逻辑变得更加难以理解。

Edith说:关于规范化的评论:我从未看到过一个数据库被归一化到极致,也没有人真正推荐这种选项。无论如何,如果你不确定任何列是否稍后会规范化(即放入其他表中),那么使用一个表而不是“表”-一对一-“其他表”的方式更容易。


保持分离的好处之一是,如果您想要更改数据库设置的方式,可以随时添加/删除字段。如果所有字段都内置于同一表中,则更难实现此操作。 - Joe Phillips
我对此表示怀疑。为什么会更容易呢?“ALTER TABLE x DROP COLUMN y”对于两个表来说都是一样的。更新程序也是一样的。哪里才能找到这个便宜呢? - Georgi
“关系型数据库并非旨在通过执行查询始终精确地返回一个结果。‘SELECT * FROM table WHERE primary_key = something’将始终返回一行(如果存在)。集合论不关心集合是否包含0、1或多个元组。” - Matt Rogish
让我解释一下:关系型数据库并不打算通过执行“关系查询”始终精确地返回一个结果。 - Georgi

3

是否将玩家登录信息(包括库存、统计和状态)与其他游戏信息分开,通常是标准化的问题。您可能希望快速查看维基百科(第三范式, 反规范化)的定义。将其分成多个表取决于存储的数据。扩展您的问题将使其具体化。我们要存储的数据包括:

  • 用户名:允许用户登录系统的唯一名称
  • 密码:与用户名关联的密码,用于保证用户的唯一性
  • 电子邮件:用户的电子邮件地址;以便游戏在用户最近未玩时“提醒”用户
  • 角色:角色的可能独立的游戏名称
  • 状态:玩家和/或角色当前是否活动
  • 生命值:角色的示例统计数据

我们可以将其存储为单个表或多个表。

单个表示例

如果玩家在这个游戏世界里只有一个角色,那么单一的表格可能是合适的。例如,
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想在多表格格式中更改他的密码,那么表中的单行将被更新。
其他考虑:
使用单个表还是多个表取决于基础数据。尽管在其他答案中没有描述,但需要注意的是优化通常会将两个表合并为单个表。
还有一个有趣的问题是知道将进行的更改类型。例如,如果您选择为可能具有多个角色并希望通过递增“players”表中的字段来跟踪玩家发出的总命令数的玩家使用单个表,则在单个表示例中将更新更多行。

2

库存是一个多对一的关系,因此可能需要拥有自己的表(至少在外键上建立索引)。

您还可以将每个用户的个人资料与公共资料分开处理(即,其他人可以看到您的资料的部分)。这可能会提供更好的缓存命中率,因为许多人将看到您的公共属性,但只有您才能看到您的个人属性。


1

一对一关系只是一个垂直分割,没有其他的。如果由于某种原因您不想将所有数据存储在同一张表中(例如为了在多个服务器之间进行分区),则可以使用它。


目前它只在一台服务器上运行。需要扩展的需求还很远。 - mjard

1

正如你所看到的,关于这个问题的普遍共识是“视情况而定”。性能/查询优化很容易想到 - 正如@Campbell和其他人所提到的。

但我认为,对于你正在构建的特定应用程序数据库,最好从考虑整体应用程序设计开始。对于特定应用程序数据库(与设计用于与许多应用程序或报告工具一起使用的数据库不同),“理想”的数据模型可能是最能映射到你在应用程序代码中实现的领域模型的那个。

你可能不会称呼你的设计为MVC,我也不知道你使用的是什么语言,但我希望你有一些代码或类来将数据访问与逻辑模型包装在一起。我认为你如何看待应用程序设计在这个层面上是衡量你的数据库应该如何理想地结构的最佳指标。

总的来说,这意味着领域对象与数据库表之间的一对一映射是最好的起点。

我认为最好通过例子来说明我的意思:

案例1:一个类/一个表

你在考虑一个Player类。Player.authenticate,Player.logged_in?,Player.status,Player.hi_score,Player.gold_credits。只要所有这些都是单个用户的操作或代表用户的单值属性,则从逻辑应用程序设计的角度来看,单个表应该是您的起点。单个类(Player),单个表(players)。

情况2:多个类/一个或多个表

也许我不喜欢瑞士军刀式的Player类设计,而更喜欢以UserInventoryScoreboardAccount为基础进行思考。User.authenticate,User.logged_in?,User->account.status,Scoreboard.hi_score(User),User->account.gold_credits。

我可能这样做是因为我认为在域模型中将这些概念分开会使我的代码更清晰、更易于编写。在这种情况下,最好的起点是将域类直接映射到各个表:Users、Inventory、Scores、Accounts等。

实现“理想”的方法

我在上面插入了一些词语,以表明将域对象与表进行一对一映射是理想的、逻辑上的设计。

这是我的首选起点。试图一开始就变得太聪明——比如将多个类映射回一个表——往往只会招来麻烦,我宁愿避免(特别是死锁和并发问题)。

但这并不总是故事的结局。有实际考虑因素可能意味着需要单独进行优化物理数据模型的活动,例如并发/锁定问题?行大小过大?索引开销?查询性能等。

在考虑性能时要小心:仅因为它可能是一个表中的一行,可能并不意味着只查询一次即可获取整行。我想多用户游戏场景可能涉及到一系列调用,在不同的时间获取不同的玩家信息,而玩家总是希望获得“当前值”,这可能相当动态。这可能会导致每次仅针对表中的几列进行多次调用(在这种情况下,多表设计可能比单表设计更快)。


1

我建议将它们分开。不是因为任何数据库原因,而是出于开发原因。

当您编写玩家登录模块时,您不希望考虑任何游戏信息。反之亦然。如果表格是不同的,维护关注点的分离将更容易。

归根结底,您的游戏信息模块没有理由去干扰玩家登录表。


0

在这种情况下,将其放入一个表中可能会使查询性能更好。

只有在存在重复数据元素时,您才需要使用下一个表格,并应考虑规范化以减少数据存储开销。


0
我建议您将每种记录类型保存在自己的表中。
玩家登录是一种记录类型,库存是另一种。它们不应该共享一个表,因为大部分信息都不共享。通过将无关信息分离出来,使您的表格保持简单。

0

我同意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才能获取所有信息。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接