SQL反模式 —— 乱穿马路

255 阅读3分钟

SQL反模式 —— 乱穿马路

注意:本文的示例为 mysql。

​ 在设计数据库字段时,我们通常会逗号分隔字符的方式(如1,2,3)来避免为多对多关系的两张表创建交叉表,这种方式是一种反模式,我们可以叫它乱穿马路(Jaywalking),因为乱穿马路也是避免过十字路口的一种方式。

1.1 问题:一列存储多个值

​ 在我们的数据库中,我们用 Products 表来存储产品与用户人之间的关系,目前一个产品只有一个联系人。

​ 根据需求设计表设计如下:


CREATE TABLE Accounts (
  acount_id INT(11) PRIMARY KEY COMMENT '账号信息',
  account_name VARCHAR(255) COMMENT '账号名称',
  account_id INT(11) COMMENT '账号id',
  -- ...  // 省略其他字段
); 

CREATE TABLE Products (
  product_id INT(11) PRIMARY KEY COMMENT '产品id',
  product_name VARCHAR(255) COMMENT '产品名称',
  account_id INT(11) COMMENT '账号id',
  -- ...  // 省略其他字段
);

​ 随着时间的推移,老板可能告诉你,现在一个产品可以对应多个联系人了,但是我们的表设计一个产品只能关联一个账号。这就产生了问题:接下来我们该怎么做?

1.2 反模式:格式化的逗号分割列表

​ 为了将对数据库表结构的改动控制在最小范围内,通常情况下,我们会将 account_id 的类型修改为 VARCHAR,这样就可以列出该列中的多个账号 ID,每个账号 ID 之间用逗号分隔。

​ 表字段修改后如下如下:

CREATE TABLE Products (
  product_id INT(11) PRIMARY KEY COMMENT '产品id',
  product_name VARCHAR(255) COMMENT '产品名称',
  account_id VARCHAR(255) COMMENT '账号id',
  -- ...  // 省略其他字段
);

​ 这样设计没有创建额外的表或者列,而仅仅改变了一个字段的数据类型。

1.3 此时我们如何查询?

1.3.1 查询指定账号产品

​ 查询 account_id 为 16 的产品信息:

SELECT * FROM Products t1 INNER JOIN Accounts ON t1.account_id = t2.account_id WHERE t1.account LIKE account_id '%16%'

以上语句很可能返回错误的结果,id 为16或者166.

1.3.2 更新指定产品的账号

​ 更新 product_id 为1的产品的账号信息在末尾追加一个新的ID。

UPDATE Products SET account_id = CONCAT(account_id,',20') WHERE product_id = 1;

​ 若要删除一个账号信息需要执行两条sql:先提取老的列表信息,再存储更新后的列表。

1.3.3 错误分隔符

​ 因为更新的 ID 信息是前端提取传过来,此时若传的是字符串 ,20,那么此时 account_id 里面存入的值可能是这样: `1,3,,20,这样就会出现一些问题,除非你事先验证用户的输入。

1.3.4 列表长度限制

​ 你在一个 VARCHAR(255) 的结构中能存多少数据了?我曾经就遇见过,然后线上服务报错。这时候还得改为 TEXT 来存储。

1.4 合理使用反模式

​ 有时出于性能优化的考量,可能在数据库的结构中需要使用反规范化的设计,比如冗余一个字段存储用户姓名。将列表存储为以逗号分隔的字符串也是反规范化的一个例子。你需要谨慎使用反规范化的数据库设计。尽可能地使用规范化的数据库设计,因为那样的设计能让你的产品代码更灵活,并且也能在数据库层保持数据完整性。

1.5 解决方案:创建一张交叉表

​ 将 account_id 字段存入一张单独的表中,我们定义该表叫 Contacts。

CREATE TABLE Contacts (
  product_id INT(11) PRIMARY KEY COMMENT '产品id',
  account_id VARCHAR(255) COMMENT '账号id',
);

Contacts 表实现了两张表之间的多对多的关系。此时产品要绑定一个联系人只需要新建一条数据,删除一个联系人则删除一条数据。同时查询时我们可以创建索引,用逗号隔开的反模式没法使用到索引值。