← 返回题目列表

多对多关系和标签系统在数据库中如何设计?

高频 中等 第 3 / 33 题 更新于 2026/07/29
数据库设计多对多标签关联表

简化版

多对多关系通常用中间关联表设计,而不是在一列里用逗号拼接多个 ID。关联表保存两边主键,并建立联合唯一约束和双向查询索引;标签系统还要考虑标签去重、租户隔离、排序、状态和统计字段。

详细版

比如文章和标签是多对多:一篇文章有多个标签,一个标签也属于多篇文章。推荐设计三张表:

article(id, title, ...)
tag(id, name, ...)
article_tag(article_id, tag_id, created_at)

关联表里通常需要:

  • unique(article_id, tag_id) 防止重复绑定。
  • index(tag_id, article_id) 支持按标签查文章。
  • 如果是多租户,唯一约束要带 tenant_id
  • 如果需要排序,可以加 sort_no
  • 如果要统计标签热度,可以维护计数字段或离线统计。

不要用 tag_ids = '1,2,3',它会让查询、约束、索引和维护都变差。

完整版教学

一、多对多关系为什么需要中间表

关系型数据库的一行一列适合保存一个原子值。多对多关系天然不是一个字段能优雅表达的:一篇文章可以有多个标签,一个标签也可以关联多篇文章。

如果把多个标签 ID 拼成字符串,例如 tag_ids='1,2,3',短期看少了一张表,长期会失去索引、约束和查询能力。想查标签 2 下所有文章时,只能做字符串匹配,还可能误匹配标签 12。

中间表把“关联关系”本身建模成数据,这是最标准也最可维护的做法。

article 1 ---- n article_tag n ---- 1 tag

记忆钩子:多对多不要塞字段,要把“关系”也当成一张表。

二、基础三表结构怎么设计

以文章标签为例,基础结构是文章表、标签表、关联表。关联表的核心字段是两边 ID。

create table tag (
  id bigint primary key,
  name varchar(64) not null
);

create table article_tag (
  article_id bigint not null,
  tag_id bigint not null,
  created_at timestamp not null,
  primary key (article_id, tag_id)
);

如果关联关系还有额外属性,比如绑定来源、排序、操作人,就放在中间表。不要把这些属性硬塞到文章表或标签表里。

三、联合唯一约束防止重复绑定

多对多最常见脏数据是重复关联。同一篇文章被绑定同一个标签两次,页面展示会重复,统计也会变大。

所以关联表要有联合唯一约束。可以直接用复合主键 (article_id, tag_id),也可以单独用自增 id,再加唯一约束。

方案优点适合场景
复合主键简洁,天然防重复关联表只表达关系
自增主键 + 唯一约束方便被其他表引用关联关系有复杂属性

无论哪种方案,都要保证 (article_id, tag_id) 不重复。否则业务代码再小心,也难免出现并发重复插入。

四、索引要支持两个查询方向

标签系统通常有两个方向的查询:查一篇文章有哪些标签,查一个标签下有哪些文章。只建一个方向的索引,另一个方向就可能慢。

primary key (article_id, tag_id);
create index idx_article_tag_tag on article_tag(tag_id, article_id);

第一条支持按文章查标签,第二条支持按标签查文章。假设一张关联表有 1000 万行,如果没有 tag_id 开头的索引,按标签找文章会扫描大量数据。

如果还要按绑定时间排序,可以设计 (tag_id, created_at)(tag_id, article_id),具体看查询模式。

五、标签名称去重要结合业务边界

标签名是否全局唯一,要看系统边界。个人笔记系统里,每个用户可以有自己的标签;SaaS 系统里,每个租户可以有自己的标签;公共内容平台可能要求全站标签统一。

多租户场景常见设计:

unique (tenant_id, name)

这表示同一个租户内标签名不能重复,不同租户可以都有“重要客户”。如果误用全局唯一,A 租户创建了标签,B 租户就不能创建同名标签,会非常奇怪。

六、标签统计可以冗余,但要说明一致性

很多标签页面会显示文章数,如“Redis 128 篇”。这个计数可以实时 count(*),也可以在标签表里冗余 article_count

实时统计准确但大表下可能慢;冗余计数查询快,但要处理一致性。比如绑定标签时 +1,解绑时 -1,失败重试要幂等,否则计数会漂。

绑定标签成功 -> article_tag 插入 -> tag.article_count + 1
解绑标签成功 -> article_tag 删除 -> tag.article_count - 1
定时任务 -> 重新 count 校准

面试回答时要主动说“冗余计数需要对账或修正”,这会显得更可靠。

七、常见误区与追问

  • 误区:多对多可以用逗号拼 ID。 拼接字段无法保证外键关系、唯一约束和高效索引,后续维护成本很高。
  • 误区:关联表不用唯一约束,代码判断即可。 并发插入下代码判断可能失效,数据库唯一约束才是底线。
  • 误区:只需要 article_id 索引。 按标签反查文章也很常见,需要 tag_id 开头的索引。
  • 追问:标签名唯一怎么设计? 看业务边界,常见是 (tenant_id, name)(user_id, name) 联合唯一。
  • 追问:关联表需要自增主键吗? 纯关系表可用复合主键;关联关系还会被引用或有复杂属性时可加自增主键。
  • 追问:标签文章数怎么维护? 可冗余计数提升查询,但要用幂等更新、事务或定时校准保证一致性。

八、面试中可以这样落地

如果问“文章标签怎么设计”,可以回答三表模型:文章表、标签表、文章标签关联表。关联表加联合唯一,分别支持按文章查标签和按标签查文章。

create table article_tag (
  article_id bigint not null,
  tag_id bigint not null,
  created_at timestamp not null,
  primary key (article_id, tag_id),
  index idx_tag_article (tag_id, article_id)
);

如果是 SaaS,再把 tenant_id 加到三张表和唯一约束里;如果要展示热度,再维护统计字段并做定期校准。

九、加强记忆

多对多设计记住“三表、唯一、双向索引、边界去重、统计一致性”。不要用逗号拼 ID,因为它把关系型数据库最擅长的约束和索引都绕开了。面试时用文章标签举例最清楚,既能讲表结构,也能讲并发重复、索引方向和计数冗余。