社区后端MySQL表设计实战:用户、文章、互动模块建表与优化
这段时间我在手撸一个社区类后端的MySQL表设计,产品需求是用户注册登录、发布文章、点赞、收藏、评论、关注这一整套常规互动功能。表面看就是增删改查,但真等到表建完、接口写完、业务跑起来,你才会发现到处都是坑:重复点赞怎么防、评论树怎么查、计数对不上怎么办、列表越翻越慢。所以我想把用户、文章、互动三块核心的建表方案和优化思路完整梳理一遍,也把我踩过的坑讲清楚。这套方案适合正在做社区/博客/内容类产品的后端开发,也适合想从CRUD往表结构设计方向进阶的同学。
我先把方法放在前面:表设计最大的坑不是不会写SQL,而是设计时没有把业务场景想清楚。字段冗余一点、少一点还好说,最怕的是关系划分和索引设计有问题,后面所有查询都跟着难受。下面我会按模块拆,每张表都给出字段、原因和建表参考,最后再集中讲索引和性能优化。整个流程我说人话、不堆概念,建表语句拿过去稍作调整就能用。
1. 需求与模块拆分:先把数据边界划清楚
1.1 社区产品的最小闭环:用户、内容、互动
先别急着写SQL。我拿到项目后的第一件事,是把这个社区产品的最小闭环在纸上画出来:一个用户注册进来,完善了资料,发了一篇文章,这篇文章可以被分类、打标签;另一个用户看到这篇文章,可以点赞、收藏、评论,也可以关注作者。然后再往周边扩展:文章有浏览量,个人主页有粉丝数、关注数、获赞数,后台可能要审核文章,运营可能要置顶、下架。画完这个闭环就会发现,整个系统的数据模型其实就三大块:用户(账号、资料)、内容(文章、分类、标签)、互动(点赞、收藏、评论、关注)。
这个拆分的价值在于,它能帮你在建表时把“领域边界”划清楚。比如“文章的点赞数”这个字段,到底应该放文章表,还是单独建一张点赞记录表?如果放到文章表里的 like_count 只是冗余的汇总值,那么真正的原始数据必须有一张点赞明细表,否则你没法判断一个用户到底点没点过赞,也没法做“我赞过的文章”这类功能。所以拆模块不是给表起名字,而是在想清楚每一条数据该落在哪、从哪来、被谁查询。
1.2 多对多关系和扩展字段:从需求反推怎么拆表
模块拆完之后,下一步是把功能点和表的关系都列出来。一对一的场景最简单,比如用户和用户资料,可以拆成1:1的两张表,也可以合在一张表,看业务复杂度;一对多也很常见,一个用户写多篇文章、一篇文章下有多个评论,这种一般只需要在“多”的一方加上外键字段,比如 article.author_id、comment.article_id;真正容易出问题的是多对多,比如标签和文章的关系、用户和用户之间的关注关系,这些必须引入中间表才能解耦。
拿标签来说,如果你图省事,想用 article.tags 存一个逗号分隔的字符串,比如“后端,MySQL,优化”,那后面做“按标签找文章”的功能时就会非常痛苦:需要一个一个 LIKE 匹配,索引基本失效,数据多了直接慢查询。正确做法是拆出 tag 表和 article_tag 中间表,中间表里只需要 article_id、tag_id 和主键,两张业务表的关系就彻底松开了。关注关系也一样,用户和用户之间是同表多对多,必须建一张 user_follow 表,用 follower_id 和 following_id 表示“谁关注了谁”。
1.3 先画ER图再写SQL,省掉后面的大重构
有很多刚从 CRUD 阶段出来的同学,喜欢想到哪建到哪,先建 user 表,写到一半感觉要加字段,就又 ALTER TABLE,结果表结构改来改去,索引也加得七零八落。我的习惯是,表设计之前一定先画一张简单的 ER 图,不用特别专业,能用方框和连线把实体、属性、关系画出来就行。PowerDesigner 里画数据库 E-R 图是标准玩法,但如果你不想装那么重的软件,用 draw.io、Navicat 的模型功能,甚至纯手绘都能达到效果。
画 ER 图最大的价值,不是那张图本身,而是强迫你把“实体之间是什么关系”先想明白。很多问题在图上很容易发现:比如你发现评论和文章的关系画完之后,又想支持“评论的评论”,那就得再确认父评论关联的是哪张表;比如你发现用户资料如果要支持多条收货地址,那 user 表里就不该出现 address 字段,而是应该单独建一张地址表。等图确认没问题了,再开始写 CREATE TABLE,后面返工的概率会小很多。特别是你打算把设计稿给别人评审的时候,一图胜千言,对方看关系比看字段快得多。
这里插一句,网上那些“第1关:数据库表设计-用户信息表”之类的闯关式练习,其实就是帮你在最小范围内练这个能力。做这种练习时别只填空,多想想为什么要设 username、password、status 这些字段,换一个业务场景哪些要变,思维才能真正长进。
2. 用户模块:主键、账号、资料扩展字段怎么落
2.1 主键选型的现实判断:自增、雪花ID、还是UUID
用户表是整个社区系统的地基,主键选型是个绕不开的问题。我的建议是,绝大多数小团队做社区项目,直接用自增 bigint 主键就够用,不要为了“未来可能分库分表”而提前上雪花ID或UUID。原因很简单:自增ID占用空间最小,InnoDB 聚集索引按顺序插入,页分裂少,写入性能最好,而且在项目早期,你能非常方便地知道总用户量大概是什么量级。至于担心对外暴露用户ID后被遍历,这不是主键本身的问题,你可以在对外接口上返回一个随机字符串或者加一层业务ID。
如果确实有分布式部署、多机房写入的需求,再用雪花ID方案。雪花ID的本质是时间戳+机器ID+序列号合成一个64位数字,趋势递增,又能在分布式环境下全局唯一,比 UUID 友好得多。我不建议拿 UUID 当主键,字符串类型占用空间大,更重要的是它无序,会导致 B+ 树频繁页分裂,随机写入性能明显变差。如果你用了 UUID 又希望它不暴露内部信息,那就让它作为业务字段存在,主键仍然用自增ID,两者各司其职。
2.2 user表字段设计:账号/密码/状态/时间戳
用户主表的字段设计,我建议遵循“能省则省、频率分离”的原则,先把登录和基础展示必须的字段放进来。下面是一个可以落地的建表参考:
这里有几个细节值得展开。username、email、phone 三个字段都建了唯一索引,因为它们是登录凭证,必须在数据库层面保证不重复,不能只靠应用层先查一遍再插入,并发下很容易穿帮。password_hash 我建议用 varchar(128),因为不只存密文,可能还要存盐和算法标识,给足空间;千万别存明文密码,也别用简单 MD5,至少上 bcrypt、scrypt 这类慢哈希算法。created_at 和 updated_at 是每张表都要有的标配,建表时就写好默认值和自动更新,后面查问题、做统计都方便。
2.3 为什么要把资料拆成独立表:垂直拆分的价值
用户资料字段往往很杂:性别、生日、所在地、个人简介、职业、学校、个人网站链接……如果一股脑全塞进 user 主表,表会越来越宽,而多数页面展示用户信息时只会用到 id、nickname、avatar 这几个高频字段,其他低频字段基本都在个人主页才查。这种“宽表”在数据量小的时候感觉不出来,一旦用户量上来,任何一次全字段查询的 IO 都会跟着变大,缓存命中率也会受影响。
我习惯把低频资料拆到 user_profile 表,和 user 主表做 1:1 关联,字段大概有 user_id、gender、birthday、bio、location、occupation、website、其他扩展字段。这样主表保持轻量,资料表可以按需加载;以后业务要加“个性签名还是职业认证”,直接在资料表加字段,不用动主表。这个思路在数据库里叫垂直拆分,提前想好,等于给将来留了接口。
看一下实际业务中,user 和 user_profile 是 1:1,不少同学会问为什么不直接合并。答案是:合并以后 CRUD 写起来更简单,但优化空间变小。真实项目中,“用户主页”是一个访问频率高但字段大的查询场景,如果主表轻量,这个查询就能走覆盖索引或者直接命中缓存。为了这点操作便利牺牲扩展性,不值得。
3. 文章模块:分类、标签、状态和内容存储
3.1 文章主表:高频字段留在主表,内容怎么存
文章表是内容模块的核心,所有内容类功能都围着它转。我的设计思路是,把文章列表页和详情页需要的字段拆开考虑:列表页需要的是标题、封面、摘要、作者、分类、发布时间、阅读数、点赞数、评论数这些高频字段;详情页才会用到正文。因此文章主表的字段大致如下:
关于正文存储,很多新手会纠结 TEXT、LONGTEXT 还是文件/对象存储。我的一般原则是:普通博客、帖子级别的正文用 LONGTEXT 完全没问题,MySQL 单行能存下 4GB 的文本,实际开发中不会真的有人往里面塞这么大的文本。如果平台有图片、视频或者文章总量特别大,就把文件本身放到对象存储或者云存储,数据库里只保存 URL,文章表的结构反而会更干净。还有一种做法是正文存独立文件、数据库只放索引和元数据,但这会明显增加系统的复杂度,初期我不建议这么搞,等日活文章量达到几十万以上再考虑。
3.2 分类和标签:一对多、多对多怎么建
分类和标签是社区内容里最容易混淆的两个概念。我的理解是:分类是树形结构,一级分类下面有二级分类,同一篇文章有且仅有一个最细粒度的分类,偏管理;标签是扁平的,多对多关系,一篇文章可以打多个标签,一个标签也可以挂多篇文章,偏运营和聚合。所以分类要建一张 category 表,字段里有 parent_id 表示层级,文章表里存 category_id 就能实现“文章属于哪个分类”;标签则必须要有中间表。
分类表很简单,id、parent_id、name、sort,一级分类的 parent_id 是0。文章表通过 category_id 关联。标签这里要特别提醒,中间表一定要加联合唯一索引,防止同一篇文章被打两次相同的标签:
中间表查询时尽量以 article_id 为第一列建联合索引,因为“查一篇文章有哪些标签”是最频繁的操作;反过来“查某个标签下的文章列表”则用 (tag_id, article_id) 这一路上的索引。两张索引都建上,也是在为两边都能快速查询做支撑。
3.3 草稿、审核、发布与软删除的状态设计
文章状态用 tinyint 就够了,0 草稿、1 待审核、2 已发布、3 下架,代码里再定义常量枚举映射,不要直接用字符串。状态字段最重要的是要跟所有对外查询条件绑定,比如文章列表只能查 status=2 的已发布文章,作者后台可以查自己全部状态的文章。如果你只在一两个接口里记得过滤,后面每次新增查询都容易漏,所以设计时就要把状态作为组合索引的一部分,从索引层面就引导你过滤。
“删除文章”我建议用软删除,也就是把状态置为下架或者加一个 is_deleted 字段,而不是直接 DELETE。原因有两个:一是内容平台经常有“误删恢复”和“违规申诉”的场景,物理删了就没有恢复余地;二是内容数据是有分析价值的,哪怕是已删除的文章,也可能在运营复盘时需要统计。如果担心表数据无限膨胀,可以定时把超过 N 天的已删除数据归档到历史表。
实际项目中,我最常踩的一个坑是“状态和软删除混用”。比如一边用 status 表示审核流程,一边又加 is_deleted 表示是否删除,两条线交叉查询时很容易写错。比较简单的方式是:status 只管业务状态,0/1/2/3 语义固定;删除操作也通过 status=3 表达,只在必要场景才真正物理清理。这样查询条件始终是“status=预期值”,心智负担小很多。
4. 互动模块:关注、点赞、收藏、评论的取舍
4.1 点赞用一张通用记录表还是按业务拆表
互动模块是整个社区里最容易做成“看起来都很简单、写起来全是坑”的部分。点赞功能,最原始的需求就是“一个用户可以给一篇文章点赞,也可以取消”。如果只有这一个点赞对象,建一张 user_like_article 表最清晰:user_id、article_id、created_at,联合唯一索引 (user_id, article_id) 防止重复点赞。但社区以后很可能还要支持“给评论点赞”“给视频点赞”,这时候再每种业务各建一张表,代码里就要写很多重复逻辑。
我见过两种主流做法。一种是通用点赞表:like_record(id, user_id, target_type, target_id, status, created_at),target_type 用 1 表示文章、2 表示评论,这种表扩展性好,加一种新业务不需要建新表;缺点是 target_type 是魔法数字,查询“某篇文章的点赞列表”时要同时过滤 type 和 target_id,索引设计要仔细。另一种是独立表,表结构最简单,每个表都专注一类业务,后期如果想统一做“我所有赞过的内容”,反而要跨表处理。
我的取舍标准是:如果确定只有文章和评论两种点赞对象,独立建两到三张表完全可以;如果可能扩展成多内容形式的社区,就直接上通用表。下面给出通用表的写法:
这里提醒一下,既然有了 status 字段表示取消,就不要在用户取消点赞时物理删数据,直接 UPDATE status=0。业务查询“是否点过赞”时,先按唯一索引查有没有记录,再判断 status 是 1 还是 0。这样操作记录可追溯,也能统计总共发生过多少次点赞行为,对运营有参考价值。
4.2 关注关系表的“唯一索引+状态位”设计
关注关系是用户之间的多对多,最常见的设计就是一张 user_follow 表。follower_id 是主动关注方,following_id 是被关注方,这两个字段名很重要,别叫 from_user、to_user,语义不清晰,后面写SQL容易反应不过来。表里再加 created_at 和 status,唯一索引建在 (follower_id, following_id) 上,保证同一个人不能重复关注同一个人。
关注功能里有几个非常现实的查询:“我关注了谁”、“谁关注了我”、“我是否关注了 TA”、“TA 的粉丝列表”。前两个本质是同一张表的不同维度查询,所以索引除了唯一索引之外,还要再给 following_id 建一个二级索引,否则查粉丝列表会全表扫描。互相关注的判断,是要用“他关注了我”和“我关注了他”两条记录结合,不要试图在表里直接存一个双向字段,数据冗余容易不一致;真要提“互相关注”这个状态,就在查询层用两次查询组装,或者再单独缓存。
关于取消关注,我推荐直接物理删除记录。可能有些人会沿用点赞的 status 方案,但关注行为不太需要历史追溯,物理删除还能保证表的记录数就是有效关注关系数,查询都不用在状态上费劲。全部逻辑都保持“有记录 = 已关注”,系统复杂度低很多。如果一小段时间内用户反复关注、取关、再关注,物理删除会造成索引的间隙变化,但量级小,完全可以接受。
4.3 评论表:树形结构到底怎么建才不会卡死
评论是一个看起来简单、细节非常多的模块。最简单的一版,可以只要 article_id、user_id、content、created_at,所有评论平铺;但社区一般都要支持“回复某条评论”,这就变成树形结构了。很多同学一听到树形结构就想着递归,其实在评论场景下,我建议在表里加 parent_id 和 root_id 两个字段:
- parent_id 表示直接父评论,一级评论的 parent_id=0;
- root_id 表示这条评论属于哪条根评论,一级评论的 root_id 是自己。
这样设计的好处是:查“某篇文章的所有评论”时,可以一次性按 article_id 把全部记录查出来,然后 root_id 相同的归到一棵树,parent_id 用来在服务端把树组装好。量级在几千条评论时完全没问题,不用写递归SQL。如果真到了海量评论,再考虑引入更重的方案,比如预排序树、嵌套集,但那种复杂度普通社区没必要提前承担。
评论表的 content 别用 TEXT,varchar(1000) 就够,评论本来不该太长,设置一个合理上限对产品也是保护。每次新增评论时,需要一个事务里同时更新 article.comment_count,否则列表页的评论数会跟实际评论条数对不上。虽然这种计数可以后面用异步任务补偿,但前期直接在同一事务里更新最不容易出错。
5. 索引与优化:从“能跑”到“好跑”
5.1 索引设计的常见原则与最左前缀
表建好了,能不能扛住真实流量,索引设计是关键。这里得说点基础:MySQL InnoDB 里,索引本质是 B+ 树,组合索引有“最左前缀”原则,也就是查询条件必须命中索引的最左列,索引才能被有效利用。举例,如果你建了组合索引 (author_id, status, created_at),那么查询 author_id=? 时能用索引,author_id=? AND status=? 也能用,author_id=? AND status=? ORDER BY created_at 也能用;但如果你直接查询 status=?,前面没有 author_id,索引就用不上。
基于这个原则,文章的列表查询,我的建议是把“筛选力度大”的字段放在组合索引前面。author_id、category_id 是强筛选字段,适合放前面;status 在后边起过滤作用;created_at 放在最后还能帮 ORDER BY 避免 Filesort。比如查“某个分类下已发布文章按时间倒序”,就用 (category_id, status, created_at) 这个组合索引;查“某个作者的文章列表”就用 (author_id, status, created_at)。不要单独给 status 建索引,区分度太低,查出来的行太多,MySQL优化器甚至可能放弃索引全表扫描。
每建一个索引,都是在用写入性能和存储空间换查询性能。所以建索引前先问自己三个问题:这个查询频率高不高?这个索引能不能同时覆盖多个查询?有没有更高效的组合方式?比如点赞记录表常见的查询是“某个用户给哪些文章点过赞”和“某篇文章有哪些人点过赞”,前者用 (user_id, target_type, target_id),后者用 (target_type, target_id, status),两个索引覆盖了两种主要查询,就够了。
5.2 用 EXPLAIN 和慢查询日志定位问题
写完索引别急着觉得万事大吉,我一般在测试环境就会跑一遍典型的查询前置检查,用 EXPLAIN 看执行计划。EXPLAIN 输出里的 type 至少要达到 range 或 ref,const/eq_ref 当然更好;如果看到 ALL 全表扫描,就要警觉。key 表示实际走了哪个索引,rows 是预估扫描行数,Extra 里如果出现 Using filesort 或 Using temporary,说明排序和分组没有利用索引,数据量大时就是性能瓶颈。
慢查询日志也是排查 MySQL 性能问题的基础工具。可以在配置文件里打开 slow_query_log,设置 long_query_time = 1,跑一段时间后去看慢SQL,通常能暴露两类问题:一类是查询条件里的字段没有索引,比如某个表的 status 列被单独查询导致全表扫;另一类是 OR、LIKE '%xx' 或者函数包裹字段,导致索引失效。把这些慢 SQL 一条条收集起来,逐个优化,比凭空猜索引效率高太多。
举个我实际遇到的情况:一开始 article 表只给 author_id 建了单列索引,列表查询 EXPLAIN 时 key 显示走了 idx_author_id,但 Extra 里有 Using filesort,因为 ORDER BY created_at 不在索引里。改成组合索引 (author_id, status, created_at) 之后,Extra 变成了 Using index condition,排序也不再走文件排序。几个百万行数据的模拟测试下来,查询耗时明显下降。这就是“组合索引覆盖 WHERE 和 ORDER BY”的价值。
5.3 大数据量下的分页、统计与缓存策略
社区列表页有一个经典性能问题:深度分页。前端要第 10000 页,SQL 写 LIMIT 200000, 20,MySQL 会把前 20 万行全部扫出来再丢掉,越翻越慢。优化办法是改成“基于排序键的分页”:先记录上一页最后一条的 id 或游标,下一页直接 WHERE id < 上一页末尾id ORDER BY id DESC LIMIT 20。首页排序字段和过滤条件保持一致时,这种游标分页方案性能非常稳定。实际项目里,我喜欢用这个方案替换深分页,而且产品侧几乎感知不到变化。
统计字段也是需要提前考虑的点。view_count、like_count、comment_count 这类计数器,如果每次都执行 COUNT(*) 去统计,热点数据会被打得很惨。常规做法是:在文章表里冗余计数,写入时在事务里同步增加或减少;读时直接取字段。但这样还是会把压力集中在 MySQL 写路径上,量再大一点,就需要引入 Redis:点赞时先 INCR 缓存里的计数,后台定期把缓存值批量回写数据库。不过缓存方案要接受“短暂不一致”,比如刚点完赞,文章列表里数字可能还没变,这个抖动通常可以靠定时任务或者消息队列补偿。
等单表数据量到了千万量级,可以用一张归档表把历史冷数据迁走,比如把半年以前的已下架文章移到 archive_article 表,主表只留热点数据,查询性能立竿见影。这种冷热分离比动不动分库分表简单得多,也很适合社区这类读多写少、老数据访问频率低的场景。
5.4 读写分离与缓存:什么时候加才不添乱
如果业务再往前跑,访问量上来了,第一个要做的不是分库分表,而是读写分离。MySQL 主从复制,主库负责写入,从库负责读,把读压力从主库剥离。很多框架和中间件都支持这件事,Spring Boot 项目里也能通过多数据源方案实现。但读写分离不是没代价的,最大的问题是主从延迟,用户刚发完文章,立刻在列表页查询,可能会因为从库还没同步到而看不到。解决办法要么是“读己之写”强制走主库,要么是接受短暂延迟,在UI上不加提醒。
缓存要不要加,也是需要前置条件的。一个列表页如果每秒钟被请求几百次,而且数据不要求实时,那 Redis 缓存就是必需品,可以把文章列表的 id 集合和分页结果缓存起来;但如果访问量没到那个级别,先加缓存反而是负担,你要处理缓存穿透、雪崩、一致性问题,代码复杂度陡增。我见过不少小项目,明明 MySQL 扛得动,却非要在前面加 Redis,结果每次发文章还要操心缓存怎么失效,纯属自找麻烦。优化这个东西,永远是量变到质变,先能用,再把慢的地方逐一优化。
6. 常见问题与排查技巧实录
6.1 避免重复点赞/收藏的底线方案
重复点赞是社区项目里最容易出现的并发问题。用户手快连点两下点赞按钮,或者前端做了重试,后端同时收到两个请求,先 SELECT 再 INSERT 很容易插两条记录。底线方案是数据库唯一索引,比如 like_record 表里的 uk_user_target (user_id, target_type, target_id),有了唯一索引,重复插入时数据库会直接报错。业务代码里捕获这个重复键异常,把它转换成“你已经点过赞了”之类的友好返回,不会影响用户体验。
但要注意,唯一索引只能保证“不会重复点赞”,不能保证“取消后再次点赞”这个场景的顺畅。如果表里同时保留 status 字段,就按“存在记录且 status=0 则 UPDATE 成 1,不存在则 INSERT”的逻辑处理。为了效率,可以先走 UPDATE 尝试,如果影响行数为 0,再考虑 INSERT,两条SQL之间用唯一索引兜底。这样并发下也不会乱。
6.2 统计字段不一致:缓存、异步和定时对账
我见过很多社区项目,前期计数都是同步更新的,用户点个赞,立刻 UPDATE article SET like_count=like_count+1,数据很准。但访问量一大,这种同步更新就成了热点行更新,数据库写锁竞争特别激烈。后来我调整成:先更新 Redis 里的计数,点赞行为只操作缓存,再用定时任务每隔几分钟把缓存里的增量同步到 MySQL。好处是接口响应快,坏处是中间任何一步出错,比如进程重启、任务挂掉,MySQL 里的计数就会和真实点赞记录不一致。
解决不一致,光靠“细心”是没用的,我习惯再加一道定时对账任务。任务里统计 like_record 表里 status=1 的真实记录数,和 article.like_count 做对比,发现不一致就自动修正。这种对账任务可以每天跑一次,成本很低,但能避免大数据量的统计误差积累成线上事故。记着一个原则,任何缓存或者异步方案,都应该有一条最终的“正确数据回写路径”,也就是数据最终要能在某一时刻和源数据对上。
6.3 后续演进:什么时候才需要分库分表
最后一个经常被问的问题:这表到底能撑多少数据量,什么时候要分库分表。我的判断标准很简单:先看单表数据量是不是到了千万甚至亿级,再看有没有明显的写入热点,比如所有关注关系都集中写一张表,最后看单条 SQL 是不是已经无法通过加索引、加缓存解决。如果只是数据量大但读多写少,先做冷热分离、归档历史数据;如果写入确实扛不住,再考虑分表,优先按业务维度拆分,比如互动数据拆成独立库,用户和文章继续留在原库;如果还不够,按 user_id 哈希或范围把用户表拆成分片表。
分库分表不是银弹,它会带来非常多的复杂度:跨库查询、分布式事务、主键全局唯一、数据迁移、扩容等问题都会冒出来。我个人的体会是,社区项目里真正需要分库分表的比例很低,大多数瓶颈都在索引设计不当和缓存缺失上。先把单库单表优化到位,把数据归档做好,很多项目撑到百万日活都不会有问题;真到了必须分片那天,你的团队也一定已经有了足够的能力和资源去解决那些新问题。
最后再分享一个我做表设计时的小习惯:每张表我都会强制加上 created_at 和 updated_at,不偷懒;每条业务记录,如果可以软删,我一定加一个状态字段而不是直接 DELETE;凡是涉及计数、统计的地方,我都会先想清楚它的“最终一致性方案”是什么,而不只是看当前写代码方不方便。这套社区后端的表设计做完之后,我最深的感受是,表结构其实不是数据库的问题,而是你对业务理解的映射——业务边界清楚,表就清楚;业务需求模糊,表建得再华丽也是空中楼阁。希望这篇复盘能给你一些参考,哪怕你只是照着字段改一改,也比上来就 CREATE TABLE 要少踩很多坑。