
做需求转数据模型这件事我干了快十年发现大多数项目最后出问题不是代码写得烂而是需求到数据模型的这一步垮了。需求评审时大家拍着胸脯说“逻辑很清晰”真到了建表阶段才发现业务规则互相打架、字段语义含糊、关系说不清楚一张订单表里塞了十几个可空字段谁看了都头疼。这篇东西想聊的就是“需求”和“数据模型”之间那段最容易被忽略的路怎么从一段口语化的业务描述里提炼出实体、属性、关系和规则再落成一张能撑住业务十年演进的数据模型。适合产品经理、后端开发、数据仓库工程师还有那些正在被“需求评审很顺利、上线后天天改表”折磨的团队。这里不会给你一堆纸上谈兵的原则我会用一个实际的在线课程订单系统案例把从需求文档到数据库建表的全过程走一遍该给的步骤、该避的坑、该抄的模板都有。1. 需求到数据模型完整拆解思路与设计前提1.1 需求文档里到底要挖出什么实体、属性、关系、规则很多人拿到需求文档第一反应是看功能列表、看页面原型然后就开始琢磨字段了。这种做法特别容易漏东西。数据模型的真正输入不是“页面上显示什么”而是“业务里到底有哪些对象、它们怎么互相作用、每一步的约束是什么”。我在实际工作中会把需求拆成四层去读实体、属性、关系、规则。实体是业务世界里客观存在的对象比如用户、课程、订单、支付流水、退款单属性是这些对象自身的特征比如用户有手机号、课程有定价、订单有支付状态关系是实体之间的关联方式一个用户能有多个订单一个订单包含多门课程规则是最容易被漏掉的层比如“课程下架后不可购买”“退款金额不能超过实付金额”“同一用户可以多次购买同一门课程但不能同时存在两个待支付订单”。这四层里面前三层决定了你建几张表、表里有什么字段规则层决定了你加哪些约束、状态机怎么流转、哪些组合要建唯一索引。如果你在需求阶段只整理出了实体和属性没整理规则那数据模型上线之后基本上逃不过补丁叠补丁的命运。1.2 概念模型、逻辑模型、物理模型各管哪一段数据模型设计通常分三层这个分层不是教科书上的花架子它解决问题的角度完全不同。概念模型是业务视角回答“业务里有哪些东西”逻辑模型是设计视角回答“这些东西之间怎么关联、字段怎么定义”物理模型是实现视角回答“在具体数据库里怎么建表、建索引、定类型”。我见过不少开发直接跳到最后一步打开 Navicat 就 CREATE TABLE边写边想字段。这相当于盖房子不画施工图直接砌墙。你确实能砌出来一栋房子但窗户可能对不齐、承重墙可能打在了奇怪的位置。正确做法是先画概念模型把实体和关系理清楚再转逻辑模型确定每个字段的语义和约束最后才落地物理模型考虑性能。举个日常的例子概念模型里“订单”和“课程”是多对多关系因为一个订单可以包含多门课程一门课程也能出现在多个订单里。到了逻辑模型你要把这个多对多拆成“订单主表 订单明细表”两张表到了物理模型你还要考虑订单明细表是不是需要分区、要不要按订单号建索引。每一层都有自己独特的决策点混在一起做一定会顾此失彼。1.3 把业务规则翻译成数据约束这几条最容易漏业务规则翻译成数据约束这件事我称之为“需求文档里最值钱的部分”但也是最容易被跳过的地方。你可以把数据模型理解成业务规则的执行者规则写在代码里靠自觉遵守换个开发就失效规则写在数据库约束里谁写数据都绕不过去。以在线课程订单系统为例常见的业务规则有这么几条每条都应该落到数据模型上的某个具体位置业务规则数据模型落地方式用户注册手机号必须唯一users.phone 建唯一索引订单号全局唯一且业务可读订单号字段建唯一索引或直接用业务订单号做主键一个订单至少包含一门课程订单明细表加 CHECK 约束或者在应用层保证至少订单表不能出现明细为空的孤儿订单支付金额必须大于 0orders.pay_amount 加 CHECK 约束同一用户同一课程不能有两个待支付订单在订单表加唯一索引user_id, course_id, status但要注意部分唯一MySQL 8.0.19 之前需要靠生成列变通退款金额不能超过实付金额应用层校验 退款表记录原始订单实付金额做对照这些规则里有的是建个唯一索引就完事有的需要应用层配合。但无论如何你必须在建模阶段就把规则写进模型设计文档里而不是等到写业务代码时再想起来。我见过最惨的一个项目退款规则没在数据模型层面考虑结果同一笔订单被用户提交了三次退款申请财务那边差点做出三笔退款。这种问题代码写得再严谨也难防因为入口太多只有数据层兜底才靠谱。2. 核心实操实体识别、关系建模与字段设计2.1 实体识别名词法、事件回溯法、边界划分实体识别看起来简单真正做起来最大的坑是“把不该当实体的东西当实体或者把该当实体的东西漏掉”。我有三个方法配合着用。第一个是名词法。把需求文档里所有名词圈出来用户、课程、订单、优惠券、支付记录、退款单、学习进度……这些名词先列出来再去掉那些明显是属性修饰的名词比如“课程价格”里的“价格”应该挂到课程下而不是单独建一张表。这里的原则是独立名词且有自己的生命周期才配当实体否则就做属性。第二个是事件回溯法。顺着业务主线走一遍用户浏览课程下单支付系统开通课程用户学习申请退款系统审核退款。每一个“动作”背后都有产生的结果。下单会产生订单支付会产生支付流水开通课程会产生订单与课程的关联记录退款会产生退款单。这些动作产生的结果就是实体。这个方法特别适合需求文档写得含糊的场景你只要追问“这个动作发生之后系统里多了一条什么记录”实体就浮出来了。第三个是边界划分。这是我后面专门学的也推荐给你实体不是越多越好不同业务域之间共用的实体要提前分清楚。比如用户和课程是两个业务域订单属于交易域交易域只需要引用用户ID和课程ID不需要把用户表的全部字段拷进来。如果把用户表二十多个字段全部宽表化到订单表里短期查询方便了长期数据一致性就是个无底洞。先确定每个实体属于哪个业务域再决定它跟其他域实体之间是“引用”还是“复制”这一步思考到位了模型的整体质量就有保障。2.2 属性设计的关键决策字段粒度与多值拆分的取舍实体的字段设计天天都在做但不一定每次都做对。核心有两个决策点字段粒度怎么定多值属性拆不拆。字段粒度说的是一个字段该存多细的数据。用订单表举例“收货地址”这个描述可以拆成省、市、区、详细地址四个字段也可以只存一个 address 字符串。拆得细后续做区域统计、物流分单就方便不拆写的时候省事但查询时只能用 LIKE 模糊匹配。我的经验是如果需求里明确提到了“按省统计”“按城市筛选”那就拆如果只是收件人查看存完整字符串也行。但还有一种更稳妥的做法兼顾两者详细地址存一个字段省市区单独存三个字段这部分冗余完全值得。多值属性说的是某个属性在一个实体上可能有多条记录。比如课程的“适用人群”可能是“在职人员、在校学生”两个值。很多人图省事直接在课程表里加一个 apply_people 字段存一个“在职人员,在校学生”的字符串。这在短期看着方便但一旦你后期需要按人群筛选课程就会发现字符串拆分的噩梦开始了。正确的做法是单独建一张课程人群关系表或者至少用 JSON 字段存储配合数据库的 JSON 查询能力。这里我强烈建议凡是需要过滤、统计、关联的属性都别用逗号拼接否则你迟早要写 migration 拆表。2.3 关系建模一对多、多对多到底该建几张表关系建模是整个数据模型设计里最需要“经验感”的部分。一对多最简单用户和订单就是典型的一对多只需在“多”的那一侧加外键。一对一的场景要停下来想一想是不是真的有必要拆表比如用户表和用户扩展信息表如果只是个别字段直接放同一张表加可空字段就行拆表反而增加 JOIN 成本。麻烦的是多对多。拿在线课程订单来说订单和课程就是多对多一个订单包含多门课程一门课程可以被多个订单包含。这种关系在逻辑模型里必须拆出一张中间表也就是订单明细表。很多人纠结中间表要不要带自己的主键我的建议是如果中间表上还有自己的业务行为比如购买价格、退款状态那就一定要带独立主键之后你会用到它的。这个道理等你需要按订单明细维度去对账、退款、开发票时就会理解。还有一种容易犯的经验性错误就是把业务上的一对多误建成多对多。比如课程和讲师一门课程可以有多个讲师一个讲师可以教多门课程看起来是多对多但如果业务规则限定“每门课程最多三位讲师且讲师有主讲和助教之分”那你需要的不是纯多对多而是带角色属性的关联表。设计关系时一定要回到业务规则问清楚这个关系本身有没有属性如果有恭喜你你必须做一张带属性的关联表。2.4 通用字段和软删除要不要 extends BaseEntity——我的答案现在谈数据模型绕不开通用字段的问题created_at、update_at、deleted、version 这些要不要每个表都放。我的答案是每个表都放而且建议在业务表里统一做。created_at 和 updated_at 是必备的这是排查数据问题时的第一现场证据。很多时候用户报“我昨天晚上下的单不见了”你查日志查半天最后发现 update_at 变了才定位到是某个定时任务改了数据。没有时间字段这种问题基本没法查。deleted 字段就是软删除这也是我要重点说的。很多团队一开始坚持物理删除理由是“数据不会错删了就删了”。但现实是一个订单删除后用户投诉说“我以前买过这个课程怎么订单记录没了”你根本没法解释。我这些年做过的绝大多数业务系统最终都回到了软删除这条路。唯一需要提醒的是软删除字段一定要参与所有业务查询的过滤条件否则等于没有另外凡是有软删除的表唯一索引必须把 deleted 也一起放进去否则就会出现“同一手机号删了之后再注册唯一索引直接冲突”的低级事故。version 字段主要是给乐观锁用的。如果你的业务里有“多个终端同时修改同一条数据”的场景比如用户提交订单、后台修改订单状态那 version 几乎是必备的。如果只是简单的单入口写操作version 可以不加避免所有更新语句都要多带一个 WHERE version 的条件增加了复杂度但收益有限。3. 从逻辑模型到物理模型建表落地全流程3.1 主键选型自增、雪花ID还是业务主键主键选型看着是个小问题实际上会影响后面若干年的开发体验和数据架构。我的经验是分层级判断。第一种是业务主键。比如订单号本身全局唯一业务上还要拿出来对账、查询、退款那直接用订单号做主键完全没问题。好处是少了自增主键和业务唯一键的重复校验坏处是业务主键往往比较长作为其他表的外键时占用空间更大。第二种是数据库自增主键这是最普遍的选择。短、快、天然有序适合绝大多数业务表。但你如果计划做分库分表或者要防止竞争对手通过订单 ID 反推你的业务单量自增就不合适了。第三种是雪花 ID / UUID。分库分表、分布式部署场景下通常选它。但要注意UUID 字符串做主键性能堪忧建议用雪花 ID 这类整数型分布式 ID。我实际看到很多公司在业务初期就全表雪花 ID说是“避免以后拆库麻烦”但代价是每个表都多了一个 B 树索引写入性能打了折扣。我的建议是单体应用阶段老老实实用自增真到需要分库分表的那一天加一个 business_id 雪花 ID 字段做分布式场景的标识主键还是自增不要急着推翻重来。3.2 命名规范与字段类型这些规则能让你少改十次表数据模型的命名规范越早统一越好。我见过的团队有一半因为命名不统一吃过苦头。表名用复数还是单数、字段名用驼峰还是下划线、状态字段是叫 status 还是 state、时间字段是叫 created_at 还是 gmt_created这些看起来都是小事但统一起来之后团队成员查表时不用反复翻数据字典效率提升非常明显。字段类型的选择更需要提前约定。金额一律用 decimal这个我反复强调float/double 存金额的坑是那种“平时没事对账那天突然多出一分钱”的慢性病做电商的人应该都知道。时间字段用 datetime 还是 timestamp看你的时区要求但我建议所有跨时区业务统一存 UTC 或带时区的 timestamp否则运营在后台看到的时间和实际差八个小时排查问题定位到凌晨三点谁都不会好受。布尔字段MySQL 里我用 tinyint(1)不要用 bit因为 ORM 映射和导出数据时 tinyint(1) 更省心。字符串长度不要无脑 varchar(255)。长度影响索引效率宁可字段定义短一点并预留合理余量也不要每个字段都 255 然后建联合索引时发现索引超长。下面这张表是我常用的字段类型参考表适合绝大多数业务系统字段含义推荐类型备注自增主键bigint unsigned预留未来数据量增长空间业务ID订单号等varchar(32) 或 bigint按业务字符/纯数字决定金额decimal(10,2)精度必须明确不要用 float手机号varchar(20)存字符串不要用 bigint因为需要处理前缀状态tinyint数字比字符串省空间但注释必须写清楚时间datetime / timestamp区分是否需求时区范围统一描述/备注varchar(500)宽松一点但别给 text除非真的很长JSON 扩展jsonMySQL 5.7 才有版本太低别硬上3.3 索引设计先保证查询走得通再谈跑得快索引的设计准则我总结成一句话先列出这个模型要支撑的高频查询路径再为每条查询路径建索引不要为了“感觉这个字段会查”就乱建。以课程订单模型为例核心查询路径有几条用户查自己的订单列表按订单状态筛选后台按订单号查订单详情按课程维度统计销量按课程和用户维度查询是否已购买。每条路径对应索引大概是这样users.id 主键索引users.phone 唯一索引orders.user_id 普通索引orders.order_no 唯一索引orders.status 普通索引如果过滤条件里频繁出现order_items.order_id 普通索引order_items.course_id 普通索引唯一索引组合orders (user_id, course_id, status) 视业务需要索引设计最忌讳两件事一是有索引但查询没用上比如你建了 user_id 的索引但查询条件写成了 where status 1 and user_id 99而索引是只有 user_id 一个字段那确实还能用但如果 where 条件第一个字段是 status而索引第一列是 user_id那这个索引就失效了。所以联合索引的字段顺序要严格按照查询条件的顺序来设计。二是过多索引拖慢写入一般的经验是单表索引数量控制在 5 个左右最多不要超过 8 个尤其是高并发写入的表每多一个索引就多一份写放大。3.4 冗余字段与反范式该冗余的地方别犹豫数据建模的书上都讲范式但实际业务里完全按第三范式建模会把你自己的查询性能拖垮。我的看法是范式要懂但不是每张表都必须做到第三范式合理的冗余是工程需要。最常见的冗余场景是明细表里冗余主表的关键字段。订单明细表里除了 order_id我还会冗余一份 order_no。这样按订单号查明细时可以直接在明细表上用 order_no 过滤不用先查订单表再映射。还有展示型字段课程名称、课程封面图在订单明细表里冗余一份哪怕课程之后下架改名订单历史也不会受影响。这就是典型的用空间换稳定。但冗余要有克制什么字段能冗余、什么字段不能有一条原则只冗余“几乎不变或允许历史快照”的字段不冗余“经常变化且必须实时一致”的字段。课程价格就不能随便冗余到订单明细里但课程名称可以因为价格要参与对账必须订单一刻的实时值名称是展示用历史快照完全没问题。把握好这条原则反范式就安全了。4. 一次真实需求的数据模型建模过程复盘4.1 原始需求与业务规则清单这里我拿一个实际帮朋友团队做过的“在线课程订单”需求来完整走一遍。原始需求文字不多基本是这么几句话用户注册后可以浏览课程列表选择课程加入购物车然后下单购买。一个订单可以包含多门课程支付成功后课程自动开通到用户的课程表里。课程可以下架下架后新用户不能购买已购买用户不受影响。用户可以申请退款退款需要管理员审核退款金额原路退回。这些文字看着简单但建模之前必须把它们整理成业务规则清单。我建议用清单式的写法每一条都带着编号方便后续评审和校验用户通过手机号注册手机号唯一。课程有上架/下架状态下架课程新用户不可购买。订单可以包含一种或多种课程。订单有状态待支付、已支付、已取消、已退款、部分退款。同一用户同一课程不能同时存在两个待支付订单。支付成功后才开通课程开通记录需要记录开通时间。退款金额不能超过实际支付金额。课程名称、价格需要冗余到订单明细中作为快照。这份清单才是数据模型的真正起点而不是那几句原始描述。你能发现每条清单后续都会对应到某个字段约束或者某张表设计上。当你发现某一条业务规则无处安放的时候就是模型缺了东西的信号。4.2 概念模型与实体拓扑基于上面的规则清单我梳理出来的核心实体有用户user、课程course、订单order、订单明细order_item、支付记录payment、开通记录course_access、退款单refund、购物车cart。实体之间的关系描述起来并不复杂用户和订单是一对多订单和订单明细是一对多课程和订单明细是一对多支付记录挂在订单下退款单挂在订单和订单明细下开通记录挂在用户和课程下。购物车是用户维度的临时数据不需要跟订单发生级联。这个阶段不建议直接开 MySQL 画表可以用 draw.io 或者 dbdiagram 这种工具先画 ER 图重点是确认实体之间关系没有遗漏。这里有个细节值得多说一句开通记录course_access这张表很多人会把它省掉理由是“订单支付成功了就代表有权限了”。但你一旦要承接“课程下架之后已购用户还能继续学”“学习进度追踪”“C 端展示我的课程列表”这些需求就会发现自己每一天都在 join 订单表然后做状态过滤性能差不说逻辑也乱。开通记录表本质上是一个“结果表”它的存在就是让查询路径变得简单直接。这就是概念模型阶段多思考的价值启动成本极低后患却能大量规避。4.3 字段设计、数据字典与 DDL 示例概念模型确认后就可以进入字段设计和物理建表环节。下面我给出核心表的字段设计和示例 DDL以 MySQL 8.x 为例重点是结构实际生产还要根据引擎调整参数。用户表users字段名类型说明idbigint unsigned主键phonevarchar(20)手机号唯一nicknamevarchar(50)昵称avatar_urlvarchar(255)头像statustinyint状态1正常 2禁用created_atdatetime创建时间updated_atdatetime更新时间deletedtinyint软删除0未删除 1已删除课程表courses字段名类型说明idbigint unsigned主键titlevarchar(100)课程标题cover_urlvarchar(255)封面图pricedecimal(10,2)课程售价statustinyint状态1上架 2下架created_atdatetime创建时间updated_atdatetime更新时间deletedtinyint软删除订单表orders字段名类型说明idbigint unsigned主键order_novarchar(32)业务订单号唯一user_idbigint unsigned用户IDtotal_amountdecimal(10,2)订单总金额快照pay_amountdecimal(10,2)实际支付金额statustinyint1待支付 2已支付 3已取消 4已退款 5部分退款paid_atdatetime支付时间created_atdatetime创建时间updated_atdatetime更新时间deletedtinyint软删除订单明细表order_items字段名类型说明idbigint unsigned主键order_idbigint unsigned订单IDorder_novarchar(32)冗余业务订单号方便直查course_idbigint unsigned课程IDcourse_titlevarchar(100)冗余课程名称快照course_pricedecimal(10,2)冗余课程单价快照refund_statustinyint0未退款 1已退款支付记录表payments字段名类型说明idbigint unsigned主键payment_novarchar(32)支付流水号唯一order_idbigint unsigned订单IDuser_idbigint unsigned用户ID冗余方便对账amountdecimal(10,2)支付金额channelvarchar(20)支付渠道statustinyint支付状态paid_atdatetime支付完成时间开通记录表course_access字段名类型说明idbigint unsigned主键user_idbigint unsigned用户IDcourse_idbigint unsigned课程IDorder_item_idbigint unsigned来源订单明细IDgranted_atdatetime开通时间statustinyint开通状态核心表 DDL 示例如下CREATE TABLE orders ( id bigint unsigned NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 业务订单号, user_id bigint unsigned NOT NULL COMMENT 用户ID, total_amount decimal(10,2) NOT NULL COMMENT 订单总金额, pay_amount decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 实际支付金额, status tinyint NOT NULL DEFAULT 1 COMMENT 1待支付 2已支付 3已取消 4已退款 5部分退款, paid_at datetime DEFAULT NULL COMMENT 支付时间, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted tinyint NOT NULL DEFAULT 0 COMMENT 0未删除 1已删除, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_status (status), KEY idx_created_at (created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;这个 DDL 里有几个细节可以解释一下status 用 tinyint 而不是 varchar注释写清楚每个值代表的含义这样后面查数据的人不用翻代码就知道状态值order_no 建唯一索引这是业务上必须保证的user_id 建普通索引因为这是最高频的查询路径deleted 字段放进唯一索引里不是必须的但如果你加了唯一索引且业务上有软删除复用数据的场景一定把 deleted 一起放进去生成列方案。时间字段默认值用 CURRENT_TIMESTAMP能让 insert 少带两个参数。4.4 数据模型评审清单上线前问自己 5 个问题建完表之后我强烈建议停下来做一轮模型评审不要立刻开始写代码。评审不是走形式而是真的拿着业务规则清单逐条对着表结构和字段过一遍。我常用的评审清单是这样的第一每个业务规则能不能在模型上找到对应的落点比如“同一用户同一课程不能同时存在两个待支付订单”这个规则如果只在代码里做了校验数据库层面没兜底那么并发请求来了还是可能插进去两条。要问自己这个规则必须落到数据库约束吗如果要落在哪个索引或哪个约束上第二每张表的每个字段都能说清楚它的含义、取值来源和更新时机吗我说的是每一个字段。当你说不清楚某个字段是干嘛的它要么是过度设计要么就是设计时没想明白业务这比少了字段更麻烦。第三查询路径都覆盖了吗把需求文档里列出的所有查询场景拿出来用户订单列表、后台订单详情、课程销量统计、用户已购课程列表逐个去分析它们会怎么走索引。如果发现某个高频查询要全表扫描才能跑那模型设计还没合格。第四唯一性规则有没有全部列出哪些字段组合在业务上必须唯一手机号、订单号是最明显的但像“退款单里的 order_item_id 是否唯一”这种边角唯一性经常被漏掉。第五删除策略是什么物理删除还是逻辑删除如果逻辑删除所有查询条件里都带上了 deleted 过滤吗唯一索引和软删除字段的组合处理了吗这个问题在评审中经常被忽略但一旦上线踩中就是数据事故。5. 常见问题与排查技巧实录5.1 需求方说“就是加个字段”时真的只是加个字段吗做需求转数据模型最常见的坑就是需求方那句“项目都上线了我就想加个字段很简单”。我见过太多人听到这句话就直接 ALTER TABLE ADD COLUMN然后过两周开始痛不欲生。这话背后的真实需求往往是“我要在列表页展示这个字段”“我要按这个字段筛选统计”“我要把这个字段暴露到某个报表里”任何一个都不是单纯加一个字段就能解决的。我的对策是接到“加字段”的需求先问四个问题字段从哪来是用户填写、系统生成还是从别的表带过来字段更新时机是什么时候创建时写入还是后续变更字段要不要参与筛选和统计要不要显示在历史记录里如果一个字段要参与统计那它就不能随便存一个没规则的字符串如果它要在历史订单里回看那当前值发生变化后历史数据怎么处理是要快照还是实时联查问完这四个问题很多“加个字段”的需求实际变成了“要加一张关联表”或者“要建一个冗余快照字段”。这时候模型的改动量完全不一样但对应的价值也不一样。不过有一点也要认识到如果需求方真的只需要一个查询字段而且用一次就不用了那就别过度设计加个字段然后做好注释就行。数据模型要有原则但也要知道灵活性在哪里。5.2 异常数据排查模型没问题但就是不对的 3 个实际 case聊几个实际经历过的 case都是模型层面埋下的隐患当时排查花了不少时间。第一个 case 是重复支付。订单表里 status 已经是支付成功用户端也跳转了成功页但支付回调因为网络抖动重复触发结果同一条订单生成了两条支付流水。表面看是回调幂等性问题深层原因是 payments 表对 order_id 没有唯一约束。修复方案是加了一个唯一索引同时处理掉已有的重复流水。这类问题的教训是像订单、支付这类关键流水表凡是能用唯一索引约束业务幂等性的一定要加别把希望全寄托在代码。第二个 case 是唯一索引加软删除的冲突。我们的用户表 phone 有唯一索引用户注销走的是软删除。后来发现同一个手机号注册新账号时会直接报唯一索引冲突。改法是用 generated column 生成一个 phone_deleted 字段当 deleted 为 0 时存 phone当 deleted 为 1 时存 phoneid再对这个生成列建唯一索引。这算是 MySQL 里软删除唯一索引的经典解法。第三个 case 是金额精度问题。订单金额用的是 decimal这个没问题问题出在订单明细汇总代码里很多人习惯用 float 接收两个 decimal 字段再加总结果 19.9 19.9 在 float 世界里变成了 39.800000000000004。报表系统里看着还好一旦做金额对账就炸。排查半天最后才发现是中间层类型转换的问题。这个 case 的教训就是全链路都要保证 decimal代码里、接口传输、前端展示任何一个环节转 float 或 double精度就没了。5.3 常见问题速查表从需求文档到建表落地的避坑清单最后整理一个速查表是我这些年做数据模型设计时反复碰到的典型问题按发生环节归类方便你直接对照排查。阶段症状根因建议需求分析需求文档看完了不知道建哪些表只读功能列表没读业务规则用事件回溯法走一遍业务全链路实体识别把“课程价格”单独建了表名词法滥用独立生命周期才算实体属性挂在实体下关系建模订单和课程直接加 course_ids 字段多对多没拆中间表拆订单明细表关系有属性时必须独立建表字段设计金额字段用 float对账多一分钱字段类型选错金额一律 decimal(10,2)主键设计全表 UUID写入性能差过早分布式设计单体时期自增主键分布式前加 business_id唯一索引软删除后唯一索引冲突唯一索引没和 deleted 联动用生成列方案解决软删除误删数据找不回一开始就物理删除业务表统一增加 deleted 字段冗余字段课程改名后历史订单名字跟着变该做快照没做明细表冗余课程名称不冗余价格索引设计查询慢但 explain 没走索引联合索引字段顺序与查询条件不符索引字段排序紧跟高频查询条件版本控制多端同时改一条数据后写覆盖先写缺少乐观锁增加 version 字段这十几条没有一条是特别高深的理论但每一条背后都是我在真实项目里踩过的坑。数据模型这个东西不懂的人觉得就是建几张表懂的人知道它其实承载了一个系统对未来所有业务规则的理解。需求变更是常态但数据模型一旦成型改动成本是指数级上升的。所以前期多花几个小时把规则理清楚、把关系画明白后面省下来的返工时间是几十倍。我个人的体会是数据模型设计最关键的能力不是 SQL 写得多好而是“把模糊的业务描述翻译成精确的数据约束”这种能力。这种翻译能力没有捷径就是多看需求、多建表、多被线上问题打脸慢慢才会有感觉。希望这份从需求到数据模型的全流程拆解能帮你少走一段我当年走过的弯路。