将小数组存储为 SQL 中的多列

Store small array as multiple columns in SQL

我正在和我的一个朋友争论如何在 SQL 中存储一个小数组 (<10) 的引用。假设有一个 class player 可以在它的库存中容纳 one item 。将其描述为 SQL table 会相当简单:

CREATE TABLE items(
    id        INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    type      VARCHAR(32),
    weight    FLOAT
);
CREATE TABLE players(
    id        INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    name      VARCHAR(128) not null,
    item      INT,
    FOREIGN KEY(item) REFERENCES items(id)
);

现在的问题是:如果玩家可以持有多个物品,但数量固定,那么 更好 将它们存储在额外的 table 然后 JOIN 在他们上面,像这样:

额外Table

CREATE TABLE players(
    id        INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    name      VARCHAR(128) not null
);
CREATE TABLE inventory(
    id        INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    item      INT NOT NULL,
    player    INT NOT NULL,
    FOREIGN KEY(item) REFERENCES items(id),
    FOREIGN KEY(player) REFERENCES players(id)
);

或者只添加额外的列会更好吗? 如果项目的数量是动态的和无限的,这当然不是一个选项:

多列

CREATE TABLE players(
    id        INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    name      VARCHAR(128) not null
    item1     INT,
    item2     INT,
    item3     INT,
    item4     INT,
    FOREIGN KEY(item1) REFERENCES items(id)
    FOREIGN KEY(item2) REFERENCES items(id)
    FOREIGN KEY(item3) REFERENCES items(id)
    FOREIGN KEY(item4) REFERENCES items(id)
);

这样做的好处是不必加入一个 table,它会变得非常大非常快,但如果没有任何玩家携带所有四个项目,则可能会非常快地碎片化你的 table时间。

希望这不是作业问题。

很难在小问题的情况下决定走哪条路。这将取决于系统中存在哪些其他实体,以及它们的使用方式。

第一个对于较小的数据集更有效,更容易维护,但不如第二个灵活,随着实体数量的增加,更规范化的版本将变得更高效。

强烈建议您多读几篇文章或找一本介绍数据库规范化的好书。

编辑:这应该是一个不错的开始: http://holowczak.com/database-normalization/

这可能是一个很好的面试问题。

  1. 偏好是意见。这取决于。但是,如果出于许多原因我每个玩家有超过 2 个项目,我会避免使用 "multiple columns" 技术(技术 #2)。其一,如果您为每个玩家设计和编码 n=10 个物品,那么项目经理明天需要多少个物品?当然是 n+1。

  2. 我相信 "Multiple Columns" 技术是 1NF 因为数据是原子的(尽管它需要空值)

"Many writers misunderstand the concept of a repeating group and use it to claim that a certain table is in violation of 1NF."

https://www.simple-talk.com/sql/learn-sql-server/facts-and-fallacies-about-first-normal-form/

仅仅因为它是 1NF 就意味着它是一个很好的解决方案。规范化本身不如应用程序的可用性、可维护性和性能重要。去规范化是性能的常见做法。

  1. 见下文

  2. 你在解决什么问题?你提供了一种技术,但在你提出要解决的问题之前,你无法衡量性能。如果可能更适合写入而不是读取。

为您的应用程序需要回答的问题编写一些示例 SQL。对于您的技巧 #2,我能想到的几乎所有问题都需要使用子选择(或案例陈述)。这些很难维护,我认为(因此不是 'preferrable')让我们为您的两项技术编号 #1 和 #2。这里有(太多)示例 SQL 每个解决方案:

每个玩家有多少物品?

#1。 Select count(inventory.item) from inventory inner join player = 1

#2。确实取决于您的数据库,例如 MySQL 您可能会使用 IFNULL(item1,0) 并对它们求和,或使用 CASE 语句。不会尝试编写此代码。

哪些玩家的物品 id = 9?

  1. select id from players from players inner join inventory on players.id = inventory.player where inventory.item = 9

  2. select id from players where item1=9 or item2=9 or item3=9 ....

哪些玩家拥有物品 ID X 和 Y?

  1. select id from players from players inner join inventory on players.id = inventory.player where inventory.item = X or inventory.item = Y;

  2. select id from players where id in (select id from players where item1 = X or item2 = X....) or id in (select id from players where item1 = Y or item2 = Y ...) or ...

由于物品有重量,哪些玩家的物品重量 > 10?

  1. select distinct players.* from players inner join inventory on players.id = inventory.player inner join items on inventory.item = items.id where items.weight > 10

  2. select distinct id from players where players.item1 in (select id from items where items.weight > 10) or players.item2 in (select id from items where items.weight > 10) or ...

请注意,我没有完成技术 #2 的 SQL。你愿意吗?

痛苦的例子还有很多SQL。哪些球员的总重量最高?删除具有特定 id 的所有项目。我不会回答这些;在我看来,对于每种情况,技术 #2 的 sql 更难维护(对我来说 == 不可取)。

可能有一些技术可以使这些子选择更简单(应用程序代码中的参数化视图、SQL 模板),但这取决于您的平台。

使用索引进行优化也有问题,因为在我看来你需要在玩家的每个项目列上建立索引 table。

如果我认为技术 #2 需要子选择是正确的,我听说连接更有效 (Join vs. sub-query )

使用技巧 #1,(附加 TABLE)只需使用触发器或应用程序代码来执行限制每个玩家 10 个项目的规则。这种规则比所有 SELECT 更容易更改

我现在应该停下来,但是你们两个可以争论的是其他事情。如果您的项目没有属性(或者属性很少被引用),请考虑技巧 #3:

单列分隔列表

CREATE TABLE players( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(128) not null, items VARCHAR(2048) -- or whatever size you need, or TEXT );

INSERT INTO PLAYERS (name, items) values ('player 1', 'itemX, itemY, itemZ');

没有规范化,但谁在乎它是否快!

再做一个 table.

是的,制作多列违反了 1NF。你为什么要遵守这条规则? 考虑:

(1) 10的极限是绝对的吗?听起来这是某种游戏(来自 "player" 这个词)所以也许是。但在大多数应用程序中,此类限制往往属于 "I can't imagine anyone ever having more than ..." 种类。几年前,我在一个保险系统上工作,我们必须记录该保单所涵盖的员工 children。最初的设计者决定创建多个字段,child1,child2,... child8。他显然对自己说,"No one would ever have more than 8 children. That will be plenty." 然后我们有一个员工有 9 children,系统崩溃了。

(2) 假设您想测试玩家是否携带了某个特定的物品。如果有两个 table,你可以写成

select count(*) from player_item where player_id=@pid and item_id=@iid

如果计数>0,则玩家拥有物品。对于一个 table,你必须写

select count(*) from player where player_id=@pid and
  (item1=@iid or item2=@iid or item3=@iid or item4=@iid or item6=@iid or item7=@iid or item8=@iid or item9=@iid or item10=@iid)

即使是简单的 "is it equal" 测试,也需要大量额外代码。你有没有注意到我跳过了第 5 项?一遍又一遍地输入这些重复测试时,这是一个很容易犯的错误。相信我:当只有 3 次重复时,我做了一次。然后,如果所需值位于插槽 1 或插槽 3 中,程序将正常运行,但当值位于插槽 2 中时,程序将失败。在我们的大多数测试中,我们只放入一个项目,因此它似乎可以正常工作。直到我们投入生产,我们才抓住那个。

(3) 假设您认为 10 不是正确的限制,并且您想将其更改为 12。对于两个 table,唯一需要更改的地方就是您所在的代码创建新的,以限制 12 而不是 10。如果你做对了,那 10 是某处的符号变量而不是 hard-coded,所以你更改一个赋值语句。对于一个 table,您必须更改每个读取 table.

的查询

(4) 说到在 table 中搜索给定项目:使用两个 table,您可以在 item_id 上创建索引。请问一个table,你需要item1上的一个索引,item2上的另一个索引,item3上的另一个索引等等。系统需要维护10个索引而不是1个。

(5) 加入将是一场特别的噩梦。您可能想要显示一个玩家拥有的所有物品的列表,其中包含物品记录中的某些值,例如名称。有两个 table,那就是

select item.name from player_item
join item on item.item_id=player_item.item_id where player_id=@pid

有一个table,它是

select item1.name, item2.name, item3.name, item4.name, item5.name, item6.name, item7.name, item8.name, item9.name, item10.name 
from player 
left join item item1 on item1.item_id=player.item1 
left join item item2 on item2.item_id=player.item2
left join item item3 on item3.item_id=player.item3
...

等 10 个连接。而且,如果连接比具有 id 匹配的简单值更复杂,则必须重复所有列和所有条件 10 次。哇!如果以后您决定需要更改条件,则必须确保进行十次相同的更改。

(5) 你如何处理添加和删除?顺序重要吗?比如你用一个table,填了4项,如果把#3删掉会怎么样?我们可以在插槽 3 中放置一个空值吗?或者我们是否需要将值从槽 4 向下移动到槽 3,然后清空槽 4?当我们添加新项目时,它们总是放在最后还是有时我们必须将它们放在中间?当我们向用户显示项目列表时,它们是否必须按某种顺序出现?使用两个 table,我们可以向查询添加一个简单的 "order by name" 或 "order by turn_acquired"。使用一个 table,您必须在内存中构建一个数组并对它们进行排序。好吧,做一次没什么大不了的,但如果在程序中多次出现就很痛苦。

等等