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