1. 项目概述为什么EER图不是“画完就扔”的草稿而是数据库落地前最关键的防线你有没有遇到过这样的场景花三天时间把MySQL表结构敲定、字段类型配好、索引也加了上线跑了一周业务方突然说“这个订单要支持分拆成多个子单”或者“用户现在要能同时属于多个部门还得记录加入时间”——你打开ER图一看发现当初设计的user_id和department_id是简单外键一对一绑定根本撑不住这种变化。这时候再改轻则加字段、改约束、写迁移脚本重则要动主键逻辑、重建关联表、补历史数据连带着接口、缓存、报表全得跟着动。我做过7个中型数据库重构项目其中5个的返工根源都出在最初那张没画透的ER图上。这不是理论问题是每天都在发生的现实损耗。所谓Enhanced Entity-RelationshipEER图不是ER图的“豪华升级版”而是对真实业务复杂性的必要建模工具。它强制你提前面对三类关键问题实体之间到底是“属于”还是“参与”一个对象能否同时具备多种身份当规则发生冲突时系统该听谁的比如“员工”和“管理员”——是所有管理员都是员工继承关系还是存在独立于员工体系的外部管理员并列关系EER里的泛化/特化、弱实体、复合属性、多值属性这些符号每一个都对应着MySQL里一句具体的DDL语句、一个约束条件、甚至一段应用层校验逻辑。我试过用纯文字描述“客户可以有多个手机号但必须至少有一个是主号且主号不能重复”结果开发同事实现时漏掉了唯一性约束上线后出现两个客户共用同一个主号风控系统直接告警。而如果这张EER图里明确标出“phone_number”是多值属性“is_primary”是派生属性并用虚线箭头指向“customer_id”那么CREATE TABLE语句里就会自然带出PRIMARY KEY (customer_id, phone_number)和CHECK (is_primary IN (0,1))错误在设计阶段就被卡住了。这篇文章讲的就是如何把EER图从一张“看起来很专业”的示意图变成一份可执行、可验证、可追溯的数据库设计契约。它不依赖任何特定建模工具全程用MySQL原生命令验证每一步设计不堆砌学术定义每个符号都对应一个真实SQL案例不回避取舍难题比如“用JSON字段存地址还是拆成province/city/district三张表”我会告诉你我在三个不同项目里分别怎么选、为什么这么选、后来踩了什么坑。如果你正在设计新系统或者要接手一个别人留下的混乱数据库这篇内容就是你开工前最该花两小时读完的实操手册。2. EER核心建模要素与MySQL映射逻辑符号不是装饰是代码的蓝图EER图里的每一个图形元素都不是为了好看而存在。它们是数据库工程师和业务方之间的“通用语言”更是生成DDL语句的原始输入。我见过太多团队把EER图当PPT素材画完就锁进共享文件夹结果开发时各凭理解写SQL最后表结构和业务逻辑对不上。下面这六个核心要素我按实际使用频率排序每个都配上MySQL的具体实现方式、参数选择依据以及我踩过的典型坑。2.1 泛化/特化Generalization/Specialization解决“一类东西多种身份”的建模难题这是EER里最容易被误用的部分。很多人看到“动物→猫/狗/鸟”就以为泛化只用于生物分类。其实业务系统里更常见的是“用户→普通用户/企业用户/VIP用户”、“订单→线上订单/线下订单/退货订单”。关键在于识别是否共享同一套主键体系、是否需要统一查询入口、是否存在互斥约束。以“用户类型”为例我通常采用共享主键类型字段检查约束的组合方案CREATE TABLE users ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, email VARCHAR(100), user_type ENUM(individual, enterprise, vip) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 共享字段放这里 CHECK (user_type IN (individual, enterprise, vip)) ); CREATE TABLE individual_users ( user_id BIGINT PRIMARY KEY, real_name VARCHAR(30) NOT NULL, id_card VARCHAR(18) UNIQUE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE TABLE enterprise_users ( user_id BIGINT PRIMARY KEY, company_name VARCHAR(100) NOT NULL, unified_social_credit_code CHAR(18) UNIQUE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE );提示为什么不用单一表加JSON字段因为JSON无法建立索引unified_social_credit_code这种强唯一性字段必须走B树索引才能保证查询性能。我曾在一个金融项目里妥协用JSON存企业资质结果风控扫描时全表扫描耗时47秒改成拆表后降到0.03秒。注意ON DELETE CASCADE是关键。当删除一个users记录时必须同步清理其在子表中的扩展信息否则会留下脏数据。但切记——不要在生产环境随意开启CASCADE务必配合应用层事务控制。我吃过一次亏某次批量删除用户时忘记关闭级联结果把关联的10万条订单记录也删了回滚花了23分钟。2.2 弱实体Weak Entity处理“离开主体就失去意义”的依附关系弱实体的核心特征是没有独立的标识符其存在完全依赖于强实体主键由强实体主键自身部分键共同构成。典型例子“订单项order_item依赖于订单order”、“评论comment依赖于文章post”。MySQL实现时弱实体表的主键必须包含外键列CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id BIGINT NOT NULL, status TINYINT DEFAULT 0, FOREIGN KEY (user_id) REFERENCES users(id) ); -- order_items是弱实体没有自己的id主键order_idproduct_id CREATE TABLE order_items ( order_id BIGINT NOT NULL, product_id BIGINT NOT NULL, quantity INT NOT NULL DEFAULT 1, unit_price DECIMAL(10,2) NOT NULL, PRIMARY KEY (order_id, product_id), -- 复合主键含外键 FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE, FOREIGN KEY (product_id) REFERENCES products(id) );实测下来很稳的关键点弱实体的外键列必须参与主键且不能为NULL。我曾在一个电商项目里把order_id设为普通索引而非主键组成部分结果出现两条order_id1001, product_id2001的重复记录导致库存扣减错乱。修复时不得不加唯一索引但已存在的脏数据清理又引发业务中断。2.3 多值属性Multivalued Attribute告别“用逗号分隔”的野蛮存储“用户有多个兴趣标签”、“商品有多个规格参数”——这类需求如果用tags VARCHAR(500)存java,python,mysql等于给未来埋雷。EER图里用双椭圆表示多值属性MySQL里必须拆成独立关联表。以用户标签为例-- 正确三张表清晰表达N:N关系 CREATE TABLE users ( id BIGINT PRIMARY KEY, name VARCHAR(50) ); CREATE TABLE tags ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(30) NOT NULL UNIQUE, category ENUM(tech, lifestyle, career) DEFAULT tech ); CREATE TABLE user_tags ( user_id BIGINT NOT NULL, tag_id BIGINT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (user_id, tag_id), -- 复合主键防重复 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE RESTRICT );提示ON DELETE RESTRICT在这里比CASCADE更安全。删除一个标签时如果还有用户在用数据库直接报错逼你先处理业务逻辑比如迁移到新标签而不是静默删掉所有关联关系。2.4 复合属性Composite Attribute把“地址”这种整体概念拆解成可查询的原子字段EER图里用矩形嵌套表示复合属性比如“地址”包含省、市、区、街道。如果存成address TEXT搜索“北京市朝阳区的用户”就得全表扫描。正确做法是拆解CREATE TABLE users ( id BIGINT PRIMARY KEY, name VARCHAR(50), province VARCHAR(20), -- 省 city VARCHAR(20), -- 市 district VARCHAR(20), -- 区 street VARCHAR(100), -- 街道 postal_code CHAR(6) -- 邮编 ); -- 为高频查询加联合索引 CREATE INDEX idx_province_city_district ON users(province, city, district);我试过用JSON存地址表面看灵活但实际业务中90%的查询都是按省市区过滤JSON字段无法利用B树索引性能差距巨大。在日活50万的社区App里按城市筛选用户列表JSON方案平均响应2.8秒拆解字段后降到0.15秒。2.5 派生属性Derived Attribute让数据库自己算而不是靠应用层维护派生属性是能通过其他属性计算得出的值比如“订单总金额∑(订单项数量×单价)”、“用户等级根据积分区间动态计算”。EER图里用虚线椭圆表示MySQL里通常用生成列Generated Column或视图View实现。推荐用生成列因为它物理存储、可索引、一致性高CREATE TABLE orders ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, status TINYINT DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE order_items ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, product_id BIGINT NOT NULL, quantity INT NOT NULL DEFAULT 1, unit_price DECIMAL(10,2) NOT NULL, amount DECIMAL(12,2) GENERATED ALWAYS AS (quantity * unit_price) STORED, -- 生成列 FOREIGN KEY (order_id) REFERENCES orders(id) ); -- 订单总金额用视图聚合避免实时计算开销 CREATE VIEW order_summary AS SELECT o.id as order_id, o.user_id, SUM(oi.amount) as total_amount, COUNT(oi.id) as item_count FROM orders o JOIN order_items oi ON o.id oi.order_id GROUP BY o.id, o.user_id;注意生成列必须是STORED物理存储VIRTUAL仅计算无法建索引。我曾在一个报表系统里误用VIRTUAL导致按amount范围查询时全表扫描改成STORED后查询速度提升40倍。2.6 联系的基数约束Cardinality Constraints用外键和CHECK把“必须有”“最多一个”刻进数据库EER图里用1、N、M等符号标注联系的基数MySQL里必须用外键约束和CHECK来落实。比如“一个用户必须有且仅有一个默认收货地址”CREATE TABLE users ( id BIGINT PRIMARY KEY, default_address_id BIGINT UNIQUE, -- 唯一约束保证“最多一个” FOREIGN KEY (default_address_id) REFERENCES addresses(id) ON DELETE SET NULL ); CREATE TABLE addresses ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, is_default TINYINT DEFAULT 0, address_detail VARCHAR(200), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, CHECK (is_default IN (0,1)) ); -- 应用层还需保证每个user_id在addresses表中is_default1的记录有且仅有一条 -- 这需要触发器或应用层事务控制MySQL原生不支持“每组中唯一值”约束这里暴露了一个现实MySQL不支持FULLTEXT外键或复杂条件唯一约束。所以“每个用户有且仅有一个默认地址”这种需求必须靠应用层逻辑数据库触发器兜底。我在支付系统里写过触发器DELIMITER $$ CREATE TRIGGER ensure_single_default_address BEFORE INSERT ON addresses FOR EACH ROW BEGIN IF NEW.is_default 1 THEN UPDATE addresses SET is_default 0 WHERE user_id NEW.user_id AND is_default 1; END IF; END$$ DELIMITER ;虽然触发器有性能开销但比起应用层漏校验导致的资金错误这点代价完全值得。3. 从EER图到MySQL落地的完整实操流程手把手带你走通每一步画EER图只是开始真正考验功力的是如何把图上的符号一步步变成可运行、可验证、可维护的MySQL代码。我总结了一套六步法已在12个项目中验证有效。整个过程不依赖PowerDesigner或ER/Studio等商业工具全部用VS Code MySQL CLI完成确保最小学习成本。3.1 第一步用文本草图快速捕捉核心实体与联系15分钟别一上来就开建模软件。拿出纸笔或用Typora写纯文本聚焦三个问题有哪些核心名词哪些名词之间有强关联关联的业务规则是什么例如做“在线教育平台”实体 - User学生/老师/管理员 - Course课程 - Chapter章节 - Video视频 - Order订单 - Payment支付 联系 - User → Course学生可选多门课课程可被多个学生选N:N - Course → Chapter一门课有多个章节章节属于一门课1:N - Chapter → Video一个章节有多个视频视频属于一个章节1:N - User → Order一个用户有多个订单1:N - Order → Course一个订单可买多门课N:N需中间表这一步的关键是拒绝过早设计字段。我见过太多人一上来就写user.name VARCHAR(50)结果讨论半天发现“name”应该拆成first_name/last_name或者要支持国际化昵称。先理清“谁和谁有关”再细化“他们之间有什么”。3.2 第二步用Mermaid语法绘制可执行EER图30分钟把文本草图转成Mermaid代码好处是纯文本、版本可控、可直接渲染、无商业软件依赖。以下是我标准化的Mermaid模板适配MySQLerDiagram USER ||--o{ ORDER : places USER ||--|{ ADDRESS : has USER ||--|{ COURSE : enrolls in COURSE ||--o{ CHAPTER : contains CHAPTER ||--o{ VIDEO : has ORDER ||--|{ ORDER_ITEM : includes COURSE ||--|{ TAG : has USER { bigint id PK varchar(50) username enum user_type student, teacher, admin } ORDER { bigint id PK bigint user_id FK datetime created_at } COURSE { bigint id PK varchar(100) title decimal(10,2) price } ORDER_ITEM { bigint order_id PK,FK bigint course_id PK,FK int quantity }提示Mermaid的PKFKPK,FK标注直接对应MySQL的PRIMARY KEY和FOREIGN KEY声明。复制这段代码到VS Code安装Mermaid Preview插件就能实时看到EER图修改文本即更新图表。3.3 第三步逐实体生成DDL重点验证约束逻辑60分钟对每个实体写出CREATE TABLE语句并强制回答三个问题这个表的主键是什么为什么选这个组合哪些字段必须非空空值会导致什么业务异常哪些字段需要唯一约束唯一性是全局的还是局部的如email全局唯一username按租户唯一以ORDER_ITEM为例-- 分析主键必须是(order_id, course_id)因为同一订单不能重复购买同一门课 -- quantity必须0否则出现0件商品的无效订单项 -- 外键on delete cascade订单删除时自动清理订单项 CREATE TABLE order_items ( order_id BIGINT NOT NULL, course_id BIGINT NOT NULL, quantity INT NOT NULL DEFAULT 1 CHECK (quantity 0), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (order_id, course_id), FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE, FOREIGN KEY (course_id) REFERENCES courses(id) ON DELETE RESTRICT );实操心得在写CHECK约束时一定要用具体数值测试边界。比如CHECK (quantity 0)我立刻在MySQL里执行INSERT INTO order_items (order_id, course_id, quantity) VALUES (1, 1, 0); -- 应该报错 INSERT INTO order_items (order_id, course_id, quantity) VALUES (1, 1, -1); -- 应该报错亲眼看到报错信息才敢提交代码。很多团队跳过这步结果上线后CHECK没生效比如MySQL版本太低不支持问题拖到线上才发现。3.4 第四步用真实业务场景驱动SQL验证90分钟写完所有DDL别急着导入。用5个典型业务场景手写SQL验证设计是否合理新增一个学生选两门课生成一个订单INSERT INTO users (username, user_type) VALUES (zhangsan, student); INSERT INTO courses (title, price) VALUES (MySQL实战, 199.00), (Python入门, 99.00); INSERT INTO orders (user_id, created_at) VALUES (1, NOW()); INSERT INTO order_items (order_id, course_id, quantity) VALUES (1, 1, 1), (1, 2, 1);查询张三的所有已购课程及订单号SELECT u.username, c.title, o.id as order_id FROM users u JOIN orders o ON u.id o.user_id JOIN order_items oi ON o.id oi.order_id JOIN courses c ON oi.course_id c.id WHERE u.username zhangsan;删除一门课程检查关联订单项是否被阻止ON DELETE RESTRICTDELETE FROM courses WHERE id 1; -- 应该报错Cannot delete or update a parent row给课程打标签验证多值属性实现INSERT INTO tags (name) VALUES (database), (programming); INSERT INTO course_tags (course_id, tag_id) VALUES (1, 1), (1, 2);查询“数据库”标签下的所有课程SELECT c.title FROM courses c JOIN course_tags ct ON c.id ct.course_id JOIN tags t ON ct.tag_id t.id WHERE t.name database;注意每个SQL都要在MySQL命令行里实际执行观察返回结果、执行计划EXPLAIN、错误信息。我坚持手写验证是因为ORM自动生成的SQL往往掩盖了索引缺失、JOIN顺序错误等问题。有一次验证时发现course_tags表没建索引WHERE t.name database走了全表扫描立刻补上CREATE INDEX idx_tag_name ON tags(name);。3.5 第五步生成基础数据字典与约束说明30分钟把EER图和DDL变成团队可读的文档。我用Markdown表格自动生成包含四列字段名、类型、是否为空、约束说明。例如orders表字段名类型是否为空约束说明idBIGINT否主键自增user_idBIGINT否外键引用users.id级联删除statusTINYINT否取值0-待支付1-已支付2-已取消3-已完成created_atTIMESTAMP否默认当前时间这份文档不是摆设。我要求所有PRPull Request必须附带数据字典变更说明比如“新增refund_reason字段VARCHAR(200)允许为空用于记录退款原因”。新人入职第一天就让他照着这份字典用SQL查出所有状态为“已支付”的订单再查出这些订单对应的用户姓名——10分钟内就能摸清核心表关系。3.6 第六步用pt-online-schema-change做零停机变更20分钟设计再完美上线后也可能要改。我坚持用Percona Toolkit的pt-online-schema-change因为它能在不锁表的情况下修改结构。比如要给users表加avatar_url字段pt-online-schema-change \ --alter ADD COLUMN avatar_url VARCHAR(255) DEFAULT NULL \ --execute \ Dyour_db,tusers \ --chunk-indexuser_id \ --max-loadThreads_running25 \ --critical-loadThreads_running50实操心得--chunk-index必须指定一个高选择性索引如主键否则分块迁移会极慢--max-load和--critical-load是保命参数当数据库负载过高时自动暂停避免拖垮线上服务。我在一个千万级用户表上加字段全程无感知监控显示QPS波动小于0.3%。4. 常见问题与排查技巧实录那些只有亲手踩过才知道的坑EER设计不是纸上谈兵是充满陷阱的实战。下面这些坑我都亲自踩过有的花了3天排查有的导致线上故障。我把它们整理成速查表按发生频率排序附上根因分析和我的解决方案。4.1 问题速查表高频故障与根治方法问题现象根本原因我的诊断步骤解决方案预防措施插入数据时报“Duplicate entry for key”复合主键设计错误或唯一索引覆盖不全1.SHOW CREATE TABLE table_name查看索引定义2.SELECT * FROM table_name WHERE col1val1 AND col2val2检查是否真有重复3.EXPLAIN SELECT ...确认查询是否走索引1. 删除错误索引2. 重建符合业务逻辑的唯一约束如UNIQUE KEY (user_id, tag_id)在EER图阶段对所有“N:N关联表”强制要求主键两个外键禁止额外IDJOIN查询性能骤降EXPLAIN显示typeALL缺少驱动表的索引或JOIN字段类型不一致1.EXPLAIN FORMATJSON SELECT ...查看详细执行计划2.SHOW INDEX FROM table_name检查索引字段顺序3.SELECT COLUMN_NAME, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS核对JOIN字段类型1. 在被驱动表的JOIN字段上建索引2. 统一字段类型如BIGINTvsINT所有外键字段必须与主表主键类型、长度、符号位完全一致DELETE级联失效子表数据残留外键定义时未指定ON DELETE CASCADE或存储引擎非InnoDB1.SELECT CONSTRAINT_NAME, UPDATE_RULE, DELETE_RULE FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_NAMEchild_table2.SHOW TABLE STATUS LIKE table_name查看引擎1.ALTER TABLE child_table DROP FOREIGN KEY fk_name2.ALTER TABLE child_table ADD FOREIGN KEY (fk_col) REFERENCES parent_table(pk_col) ON DELETE CASCADE新建表时强制ENGINEInnoDB并在建表SQL末尾添加注释-- 必须级联删除JSON字段查询慢无法利用索引JSON字段本身不可索引-操作符需全表解析1.EXPLAIN SELECT * FROM table WHERE json_col-$.key val2.SELECT COUNT(*) FROM table WHERE json_col IS NOT NULL评估数据量1. 将高频查询字段拆出为独立列2. 或用生成列GENERATED ALWAYS AS (json_col-$.key) STORED并建索引EER图中对JSON字段标注“仅存档不用于查询”并在数据字典中加警示⚠️此字段不可索引CHECK约束不生效插入非法数据成功MySQL 5.7及以下版本不支持CHECK或SQL_MODE未启用严格模式1.SELECT sql_mode检查是否含STRICT_TRANS_TABLES2.SELECT VERSION()确认MySQL版本1. 升级到MySQL 8.02. 或在应用层增加校验逻辑在项目初始化SQL中强制执行SET sql_mode STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE;4.2 三个血泪教训关于“看起来合理”的设计教训一用TINYINT存状态结果不够用我在第一个项目里给订单状态设status TINYINT预设0-5代表“创建/支付/发货/签收/完成/取消”。上线半年后运营要加“部分发货”“冻结中”“仲裁中”三个状态TINYINT最大值255虽够但业务方看不懂数字开发也容易混淆。最终改成ENUM(created,paid,shipped,partially_shipped,frozen,arbitrating,received,completed,cancelled)虽然ENUM有迁移风险但可读性救了团队。结论状态字段优先用ENUM其次用VARCHAR最后才考虑数字。教训二认为“反正有索引字段随便加”为支持模糊搜索在users表加了fulltext_index结果发现INSERT性能下降40%。查SHOW PROCESSLIST发现大量FULLTEXT INITIALIZATION等待。原来全文索引会为每个INSERT做倒排索引更新。解决方案把全文检索剥离到ElasticsearchMySQL只存结构化数据。现在所有新项目EER图里凡标“全文搜索”的字段右侧必加注释“→ 同步至ESMySQL不建FT索引”。教训三忽略时区导致定时任务错乱created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP看着没问题但服务器时区是UTC应用层传的是东八区时间结果所有“今日订单”统计都少8小时。根治方案所有时间字段统一用DATETIME应用层传入ISO8601字符串如2023-10-01T12:00:0008:00MySQL不做时区转换。EER图里对每个时间字段我都会手写标注“存储为UTC DATETIME应用层负责时区转换”。4.3 性能压测验证用sysbench模拟真实流量设计完成不等于可用。我用sysbench对核心表做压力测试验证EER设计的承压能力# 准备10万测试用户 sysbench oltp_insert --tables1 --table-size100000 --threads16 prepare # 模拟高并发订单创建重点压order_items表 sysbench oltp_insert \ --tables1 \ --table-size10000 \ --threads64 \ --time300 \ --report-interval10 \ run关键观察指标QPS是否稳定低于500 QPS就要警惕说明索引或表结构有瓶颈95%延迟是否50ms超过则检查EXPLAIN优化JOIN或添加覆盖索引InnoDB Row Lock Waits是否突增突增说明热点行竞争需拆分表或调整事务粒度有一次测试发现order_items的INSERT延迟飙升SHOW ENGINE INNODB STATUS显示大量lock wait。根因是PRIMARY KEY (order_id, course_id)中order_id是递增的导致所有新订单都往B树最右页插入产生争抢。解决方案把主键改为PRIMARY KEY (course_id, order_id)让写入分散到不同页。调整后QPS从320提升到1800。5. 工具链与协作规范让EER设计成为团队肌肉记忆再好的设计如果团队不遵循也会沦为废纸。我把EER工作流固化成一套轻量级工具链无需额外部署所有成员用VS Code就能协同。5.1 核心工具链文本即一切EER图绘制VS Code Mermaid Preview插件免费实时渲染DDL管理Git仓库根目录下/schema/文件夹按模块存放.sql文件如/schema/users.sql,/schema/orders.sql数据字典/docs/data_dictionary.md用Markdown表格维护每次DDL变更必须同步更新验证脚本/scripts/verify_business_scenarios.sql存放3.4节的5个核心场景SQLCI流水线自动执行提示在Git Hooks里加pre-commit检查禁止提交未更新数据字典的DDL。脚本很简单# .git/hooks/pre-commit if git diff --cached --quiet schema/; then echo ✅ schema files unchanged else if ! git diff --cached --quiet docs/data_dictionary.md; then echo ✅ data dictionary updated else echo ❌ schema changed but data_dictionary.md not updated! exit 1 fi fi5.2 团队协作铁律三条红线红线一没有Mermaid EER图的PR一律拒绝合并图不必精美但必须包含所有实体、联系、基数标注。我见过最简陋的图是一张截图上面手写标注“USER 1--N ORDER”但足够驱动开发。关键是“有图”不是“美图”。红线二所有外键必须明确写出ON DELETE/ON UPDATE行为禁止FOREIGN KEY (col) REFERENCES table(col)这种裸外键。必须是ON DELETE CASCADE或ON DELETE RESTRICT让约束意图100%透明。红线三新增字段必须在数据字典中标注“用途”和“是否可空”比如last_login_at DATETIME NULL COMMENT 用户最后一次登录时间用于活跃度分析。没有COMMENT的字段会被视为设计缺陷。5.3 设计评审Checklist10分钟快速过一遍每次设计评审我只问这5个问题每个问题10秒内必须答出这个表的主键是什么为什么不是其他组合哪个字段可能为空空值代表什么业务含义如果删除主表一条记录子表会发生什么级联阻止置空这个表最常被WHERE的字段是哪个有没有索引这个表的数据量预估多少一年后会不会超千万行答不出第3或第4条的必须回去重设计。这比看几百行DDL更高效。我个人在实际操作中的体会是EER图的价值不在于它画得多漂亮而在于它迫使你在敲下第一个CREATE TABLE之前把业务规则、数据流向、异常场景全都想清楚。我经手的项目里凡是跳过EER直接写SQL的后期返工率100%而坚持用这套六步法的上线后表结构修改率低于5%。最后再分享一个小技巧把Mermaid EER图导出为PNG贴在Confluence首页标题就写“本系统数据契约——最后更新于XXXX年XX月XX日”。每次有新需求先看这张图再决定是扩展现有实体还是新增关联团队共识自然就形成了。