简介:数据库设计规范V1.0文档(2007年制定)面向开发组全体人员,用于统一软件项目数据库设计、维护阶段的命名与编程规范,可解决命名不统一、沟通和维护困难等问题。文档完整覆盖目的、范围、术语、设计概要、命名规范、数据库对象命名、脚本注释、数据库操作原则及常用字段命名参考。命名规范为核心部分,要求对象名称一律小写、全长不超过30个字母、单词间用下划线连接,同时对数据库、日志、配置文件、表空间以及表、字段、索引等对象的命名格式给出明确示例。数据库操作原则上,强调使用SQL脚本修改数据库并在脚本中添加注释,表结构调整需先更新数据字典再实施,有助于保障数据一致性与可追溯性。包体为1个doc文件,约135KB,便于快速查阅和团队分享。当前已有274人学习,适合正在制定或优化数据库设计标准的开发人员、DBA及项目管理者参考借鉴。
1. 数据库设计规范不是文档墙:先弄清这份 doc 到底该管什么
数据库设计规范这份文档,最容易出现的结局是:写的时候很认真,评审的时候没人看,半年后连写规范的人自己都忘了第几条在说什么。我对这类文档的要求只有一条——它不能是口号集,必须是一份能对着建表语句逐条打勾的契约。字段怎么命名、主键用什么类型、时间存成什么格式、哪些地方必须有唯一约束,这些如果不提前定死,项目一旦进入多人协作,就会冒出三张结构完全不同的订单表,联表查询时两张表的关联字段一个 BIGINT 一个 VARCHAR,SQL 跑一次慢一次。这份规范真正要管的,是让建表、改表、写 SQL 的人都能用同一套语言说话,让后来接手的人不用靠猜。
2. 把规范拆成可执行条款:命名、类型、索引与约束的默认值
一份能落地的数据库设计规范,拆开看其实就是四件事:字段怎么命名、类型怎么选、索引怎么建、约束怎么设。这四个问题在评审时几乎每次都会吵,因为每个人都有自己的习惯。规范的价值不是选出“最好”的方案,而是给出一个默认答案,让所有人都照着走。下面我把最容易产生分歧的地方展开讲。
2.1 命名规范:让表名和字段名变成自解释的注释
命名这件事,最常见的翻车方式不是没用规范,而是规范写得太细,细到没人能记住。比如强制要求每个字段带模块前缀、禁止三个字母以下的缩写、表名必须用复数,这些规则在写代码时就成了负担。我一般只定几条硬规则,剩下交给代码评审去兜底。
第一,表名统一小写、下划线分隔、用业务域加表名的形式,比如 order_info、customer_account、pay_transaction。这里最关键的不是单数还是复数,而是全库必须统一。我就见过同一个库里既有 orders 又有 order_info,两个组各建各的,联表时先要猜哪个才是主表。第二,字段名小写下划线,外键字段必须和被引用表的主键字段同名。这样写 JOIN 的时候,看到 user_id 就知道它连的是 user_info.id,不用再翻表结构。第三,索引和约束的命名自成体系:普通索引 idx_表名_字段,唯一索引 uk_表名_字段,外键约束 fk_表名_被引用表。这套规则最大的好处是排障时看一眼索引名就知道它是干什么的,不用对着一张表 SHOW INDEX 猜半天。
-- 命名规范示例:一张订单表的骨架 CREATE TABLE order_info ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT '自增主键', order_no VARCHAR(32) NOT NULL COMMENT '业务订单号,全局唯一', customer_id BIGINT NOT NULL COMMENT '客户ID,关联 customer_info.id', PRIMARY KEY (id), UNIQUE KEY uk_order_info_order_no (order_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表';这里每个字段都带 COMMENT,这是规范里另一个不能妥协的点。没有注释的字段在半年后就是黑匣子,尤其是 order_status 这种字段,0 和 1 到底哪个是已支付,只有写第一版代码的人知道。规范里应该写明:字段必须有注释,状态类字段的注释要列出枚举含义。另外,字段名里直接写 order_status 而不是 status,也是为了一眼能看出归属,避免多表 JOIN 时 status 这个字段名撞车。
2.2 数据类型选择:先选对,再谈优化
类型选择是我在评审时看得最仔细的部分,因为类型选错,后面索引、查询都跟着错。常见错误包括:金额用 DOUBLE、时间用 VARCHAR、状态用 VARCHAR 存中文、主键用字符串 UUID。这些写法在做原型时没问题,一旦数据量上来,每一条都对应一个性能事故。
金额字段统一 DECIMAL,不要用 DOUBLE 和 FLOAT。二进制浮点数在累加和比较时会有精度误差,库存、对账这类场景一旦错一分钱,排查成本高到没法估量。普通业务金额用 DECIMAL(18,2),涉及汇率、积分的场景扩到 DECIMAL(20,4)。
时间字段统一 DATETIME,禁止用字符串存时间。MySQL 的 DATETIME 只受年份范围限制,TIMESTAMP 在 2038 年会有溢出问题,虽然离现在还远,但设计规范里直接定 DATETIME 可以少解释很多。时区问题在应用层处理,数据库只管存一个绝对时刻。如果需要毫秒精度,用 DATETIME(3)。
状态字段用 TINYINT,不用 VARCHAR,也不建议用 MySQL 的 ENUM。ENUM 在 MySQL 里加一个新枚举值需要做 DDL,改起来麻烦,而且不同表之间同样的语义无法复用。TINYINT 配合字段注释里的枚举说明,是最省事的方案。
-- 类型选择示例:订单状态、金额和时间的推荐写法 order_status TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态:0待支付 1已支付 2已取消', pay_time DATETIME DEFAULT NULL COMMENT '支付时间,未支付为空', order_amount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '订单金额,单位元'这套写法的逻辑是:每个字段都尽量用最贴近业务语义的存储形态,同时给查询留下索引空间。pay_time 之所以允许 NULL,是因为“未支付”是一个合法状态,用 0000-00-00 或者 1970 这种日期去表示反而会让条件查询写得别扭。状态字段给默认值 0,则避免了应用层忘记赋值导致的数据异常。
2.3 索引设计:按访问路径建,不按字段习惯建
索引规范是数据库设计规范里最能直接转化为性能收益的部分,也是最容易被忽视的部分。很多团队的习惯是:建完表,把所有 where 条件里出现过的字段各建一个索引。这种“平均用力”的做法,在数据量小的时候看不出问题,一旦单表过千万行,写入慢、占用大、优化器还不一定选对索引。
联合索引是最容易踩坑的地方。一个联合索引 (customer_id, order_status, created_at) 能覆盖三种查询:只按 customer_id 查、按 customer_id 加 order_status 查、三个条件同时查。但如果查询只按 order_status 过滤,这个联合索引帮不上忙,因为最左前缀原则决定了索引从左侧开始匹配。所以索引设计的第一件事,是先列出这个表最高频的几条查询语句,再根据语句反推索引。
-- 反例:where 里出现过的字段各建一个索引 ALTER TABLE order_info ADD INDEX idx_customer_id (customer_id); ALTER TABLE order_info ADD INDEX idx_order_status (order_status); ALTER TABLE order_info ADD INDEX idx_created_at (created_at); -- 推荐:按高频查询路径建联合索引,保留独立的索引 ALTER TABLE order_info ADD INDEX idx_customer_status_time (customer_id, order_status, created_at); ALTER TABLE order_info ADD INDEX idx_order_status (order_status);反例里的三个单列索引,如果查询条件是 customer_id 和 order_status 一起出现,MySQL 可能只会选其中一个索引,另一个条件要靠回表再过滤,效率比联合索引差很多。推荐写法里保留独立的 order_status 索引,是因为后台管理端经常要按订单状态做全表统计,这时候 customer_id 条件不存在,联合索引用不上。这个取舍必须在规范文档里写清楚:索引不是越多越好,而是每一条都要能说清楚它服务哪个查询路径。
2.4 约束与默认值:把数据库当成最后一道防线
应用层代码可以写得很严谨,但代码总会有漏洞、总会有绕过逻辑的批量脚本。数据库设计规范里,约束和默认值就是把关的最后一环。这一块不需要多,但要足够硬。第一条,业务字段默认 NOT NULL,只有像备注、支付时间这样真正允许为空的字段才留 NULL。全表大量可空字段会让查询条件写起来很别扭,经常要加 IS NOT NULL 才能过滤掉脏数据。第二条,所有字段必须有 DEFAULT,状态类给 0,时间类给 CURRENT_TIMESTAMP,数值类给 0。第三条,业务上不允许重复的字段必须建唯一索引,比如 order_no、user_name、phone,不能只靠应用层判断。
物理外键是我明确建议禁用的。外键约束在高并发写入时会导致额外的锁检查和级联操作,而且分布式架构下外键根本没法跨库生效。保持表间关联的一致性是应用层的事,数据库只负责提供索引和约束。存储过程和触发器同理,规范里直接禁止把业务逻辑写进数据库。这些逻辑放进数据库后,既难做版本管理,又会让排查问题的人不得不同时看懂 SQL 和代码两个世界。
3. 从规范到建表 SQL:可直接照抄的模板与三个影响后续的选型
文档里的条款再多,落地时还是要落到一串 CREATE TABLE。实际操作中,我更推荐直接给出一套建表模板,让大家在模板上改字段,而不是从零开始写。模板能保证公共字段、字符集、主键方案保持一致,也方便评审时一眼看出谁在模板上做了额外修改。
3.1 通用建表模板:把公共字段统一在一个 DDL 里
CREATE TABLE `order_info` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '自增主键', `order_no` VARCHAR(32) NOT NULL COMMENT '业务订单号,全局唯一', `customer_id` BIGINT NOT NULL COMMENT '客户ID,关联 customer_info.id', `order_amount` DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '订单金额,单位元', `order_status` TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态:0待支付 1已支付 2已取消', `pay_time` DATETIME DEFAULT NULL COMMENT '支付时间,未支付为空', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记:0未删除 1已删除', PRIMARY KEY (`id`), UNIQUE KEY `uk_order_info_order_no` (`order_no`), KEY `idx_customer_status_time` (`customer_id`, `order_status`, `created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='订单主表';这个模板里有几个字段值得说明。created_at 和 updated_at 是所有业务表都应该有的公共字段,updated_at 使用 ON UPDATE CURRENT_TIMESTAMP 后,只要这一行被更新,时间就会自动刷新,省去应用层手动维护。deleted 字段做逻辑删除,是多数互联网业务的选择,因为物理删除会丢失审计线索,也会让历史统计口径对不上。order_no 作为业务唯一键,和主键 id 分开,是为了将来哪怕分库分表,业务上也还能按订单号唯一定位数据。
3.2 主键选型:自增、雪花还是业务主键
主键是建表时最纠结的选型,因为不同方案各有取舍。BIGINT 自增主键写入性能最好,因为是顺序追加,InnoDB 的 B+ 树不会频繁分裂,但它只在单库内保证唯一,一旦分库,两个库的自增 id 会撞。雪花 ID 适合分布式场景,生成的 ID 全局唯一且趋势递增,但相比自增主键,写入时索引页的随机性会略高,TP 会差一点。业务主键直接用订单号,查询时省一次回表,但订单号往往带业务含义,长度和格式一变就要改表。
-- 分库场景下的主键设计:保留自增id,同时用业务号建唯一索引 CREATE TABLE `pay_transaction` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '内部自增主键', `transaction_no` VARCHAR(64) NOT NULL COMMENT '全局交易号,分库后仍全局唯一', ... PRIMARY KEY (`id`), UNIQUE KEY `uk_pay_transaction_no` (`transaction_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付流水表';我一般建议默认用 BIGINT 自增主键,但每条业务数据都必须设计一个业务唯一号并建唯一索引。这样做的原因是:自增主键对内部管理友好,业务唯一号对跨系统交互友好。将来真要分库,只需要改分库键和路由层,业务侧的交易号不用变,查询也不需要依赖全局自增序列。
3.3 字符集与排序规则:初始化时定死,不要在事后迁移
字符集这个问题,九成以上的事后麻烦都源于建库时没指定,沿用了实例默认设置。MySQL 5.x 时代默认 utf8mb3,存不了 emoji 和生僻字,等业务反馈“用户昵称变成问号”时再迁移,全表索引要重建,停机窗口和风险都很大。规范里应该有一句硬话:库、表、列三级全部显式指定 utf8mb4。这里有一个细节,MySQL 8.0 的默认排序规则是 utf8mb4_0900_ai_ci,5.7 默认是 utf8mb4_general_ci,两个排序规则在不同版本间如果混用,做表关联时字符集 COLLATE 不一致会直接报错。
排序规则的选择要在规范里一并写清楚。utf8mb4_general_ci 速度快但不按 Unicode 标准排序;utf8mb4_unicode_ci 更精确但性能略低;如果业务要求大小写敏感或区分重音,要单独选用 _bin 或 _cs。实际业务里,用户名、手机号这些唯一字段,通常是要求“不区分大小写”的,直接用默认的 _ci 排序规则就能满足。最怕的是有人建表时写 DEFAULT CHARSET=utf8,有人写 DEFAULT CHARSET=utf8mb4,联表 JOIN 时字符集不同,MySQL 会做一次隐式转换,索引照样失效。
4. 把规范落到真库的 4 个避坑记录:现象、原因与解决
规范写得再好,不经过真库检验都是纸上谈兵。下面这四条是我在线上环境和评审中反复遇到的坑,每条都按现象、原因、解决的方式来写,可以直接对照你手头的表自查。
4.1 同一个字段在两张表类型不一致:联表查询从毫秒变全表扫描
现象:用户表和订单表联表,按 customer_id 关联,SQL 在测试环境跑几十毫秒,上到生产几千万数据后就变成好几秒,甚至直接把数据库 CPU 打满。检查执行计划时发现,关联字段在两张表里一个 BIGINT、一个 VARCHAR,MySQL 为了比较两个不同类型的列,会对其中一列做隐式 CAST,索引列一旦被套上 CAST 函数,B+ 树就用不上了,优化器只能选择全表扫描。
原因:两张表设计时由不同人负责,一张用 BIGINT 存用户 ID,另一张为了兼容历史数据用 VARCHAR。跨表关联时类型不匹配,是隐式转换最常见也是危害最大的场景。解决方式没有技巧,把两张表的关联字段改成完全相同的数据类型,VARCHAR 向 BIGINT 改,还是 BIGINT 向 VARCHAR 改,取决于哪边的改动面小。规范里应该明确写一条:跨表关联的字段,类型和排序规则必须完全一致,不一致在评审阶段就驳回。
4.2 时间字段用字符串保存:范围查询永远走不上索引
现象:某张流水表的时间字段建成了 VARCHAR,格式类似2024-05-01 12:30:00。日常按天查询时,因为字典序碰巧和时间序一致,数据量小时没问题。当表涨到千万行后,按时间范围查询越来越慢,设计规范里推荐了 DATETIME,但没人执行。
原因:字符串时间格式一旦出现2024/5/1 12:30:00、20240501123000这类变体,字符串排序就彻底乱了,范围查询要么结果错,要么没法用索引。即便所有字符串都严格按照 ISO 格式写入,索引效率也远不如原生日期类型,因为 DATETIME 在索引里是按数字排序的,字符串还要逐字节比较。解决方式是在迁移时统一改为 DATETIME 列,应用层在写入时用参数化方式传日期对象,不在 SQL 里拼字符串。
-- 字符串时间转 DATETIME 后再建索引 ALTER TABLE operation_log MODIFY create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'; ALTER TABLE operation_log ADD INDEX idx_create_time (create_time);这里必须提醒一个顺序问题:先改字段类型,再建索引。如果反着来,索引还在旧列上,改列类型时 MySQL 会重建表,索引也不会自动迁移。大表改列类型建议先在备库演练,确认锁表时间和可用窗口。
4.3 逻辑删除字段的命名和语义不统一:数据修复只能靠猜
现象:A 表用is_deleted,1 表示已删除;B 表用deleted,也是 1 表示已删除;C 表干脆叫status,9 表示已删除。统计报表时,有的地方过滤了删除数据,有的地方没过滤,结果同一张订单表三种统计口径,产出的数字对不上。
原因:逻辑删除是后补的规范,各表由不同人建,字段命名和语义全靠习惯。解决方式:全库统一一个字段名deleted,TINYINT NOT NULL DEFAULT 0,0 为未删除、1 为已删除,所有查询默认过滤 deleted=0。这里有一个隐藏很深的坑:如果表上有唯一索引,比如uk_phone,用户注销后再注册,新数据 deleted=0,老数据 deleted=1,唯一索引不会冲突。但如果用户注销后又注册再注销多次,就有多行 deleted=1,继续插入新用户还不冲突,因为唯一索引只对 deleted=0 的活跃行生效。假如业务上需要保留多次注册记录,唯一索引就得改成(phone, deleted)的复合唯一索引。
4.4 重复索引和冗余索引并存:写库变慢还占空间
现象:某个核心表的写入事务延迟增加,磁盘空间也比预期涨得快。DBA 排查时发现,同一张表上同时有idx_customer_id和idx_customer_id_status,而后者已经覆盖了前者;还有两个索引的字段顺序完全相同,只是索引名不同,明显是上一次优化时忘记删旧的。
原因:开发同学每次遇到慢查询就加一个索引,没有回头看同一张表上已有的索引。InnoDB 里每多一个索引,写入时就要多维护一份排序结构,索引越多写入越慢。解决方式是用SHOW INDEX FROM 表名把现有索引全列出来,人工逐个核对:如果某个单列索引是某个联合索引的最左前缀,并且没有独立的查询路径用到这个单列索引,就 DROP 掉。
-- 查看一张表的所有索引,按冗余程度人工核对 SHOW INDEX FROM order_info;这项工作最可靠的做法是把它写进发布清单:每次给表加索引前,先跑一遍这条命令,确认新索引没有覆盖现有索引,也没有被现有索引覆盖。把这条命令当成查询的固定环节,就不会出现“加索引不加检查”的重复劳动。
5. 用 information_schema 给库表做体检:规范验收的三种方式
规范执行得好不好,不能靠人肉抽查。information_schema 是 MySQL 自带的元数据库,把规范里的硬性条款翻译成 SQL 查询,就能在任意时间点给所有库表做一次体检。我习惯在每次新版本上线前跑一遍这三个查询,把它们当成数据库代码的 CI。
第一个查询,找出全库没有主键的表。InnoDB 没有主键时会选择第一个非空唯一索引,如果没有就生成隐藏主键,这对复制、日志和更新性能都有影响。用 information_schema.tables 和 table_constraints 做一次左连接,就能发现漏网之鱼。
SELECT t.table_schema, t.table_name FROM information_schema.tables t LEFT JOIN information_schema.table_constraints c ON c.table_schema = t.table_schema AND c.table_name = t.table_name AND c.constraint_type = 'PRIMARY KEY' WHERE t.table_schema = 'your_db' AND t.table_type = 'BASE TABLE' AND c.constraint_name IS NULL;第二个查询,检查字符集不一致的表和列。规范要求统一 utf8mb4,但实际库里总有老表还停在 utf8mb3。这个查询会把所有不符合要求的列名列出来,方便排期迁移。
SELECT table_name, column_name, character_set_name, collation_name FROM information_schema.columns WHERE table_schema = 'your_db' AND character_set_name IS NOT NULL AND character_set_name <> 'utf8mb4';第三个查询,检查是否所有字段都有 COMMENT。没有注释的字段,在半年后就是一颗定时炸弹。元数据里看的一清二楚。
SELECT table_name, column_name FROM information_schema.columns WHERE table_schema = 'your_db' AND column_comment = '' AND column_name NOT IN ('id', 'created_at');这三个查询跑完后,把结果贴在工单里,明确责任表后再统一修。我更习惯的做法是,把这三条 SQL 存成一个脚本,每次结构变更评审时都跑一遍,执行 DDL 前先扫一遍,而不是等问题反馈到线上再补救。时间长了会发现,最值钱的不是那本数据库设计规范文档,而是把文档条款变成自动化检查的过程。希望帮到你。
本文还有配套的精品资源,点击获取