简介:面向Oracle数据库建设提供的一份可落地设计规范,适合系统架构师、开发人员及数据库管理员在项目立项、开发或重构阶段参照使用。文档系统梳理了数据库对象长度、数据完整性、规范化设计与性能权衡、字段类型定义与使用等策略,并覆盖数据库、表空间、表、字段、视图、序列、存储过程、函数、索引、约束等对象的完整命名规则,同时给出金额、税率、姓名、地址等常用字段的类型与长度建议,以及OLTP与OLAP分开设计等关键思路,便于团队统一建模口径、减少数据冗余并提升系统可维护性与运行效率。资源为1个doc格式文档,压缩包大小296KB,内容结构清晰,可直接查阅、复用或作为评审检查单;目前已有267人学习下载,适合需要快速建立或完善数据库设计规范的中大型项目团队参考。
1. 为什么说数据库设计规范是一份“技术债台账”
很多团队把数据库设计规范当成入职培训的PPT,看完就忘,真正建表时照样给你搞出一个user_info_2021_backup和OrderDetail混用的库。做后端和DBA这行久了会明白,数据库设计规范不是约束开发者的条条框框,而是技术债的台账。你每违反一条约定,就等于往台账上记一笔需要未来用双倍工时偿还的利息,比如字段类型选错导致索引失效,或者字符集不统一造成关联查询乱码。这篇文章我按常见的生产环境做法,把数据库设计规范从理论拆到可执行的DDL模板和评审清单,让新手能照着建表,熟手能拿来做Review。
2. 建库建表之前:先定命名、类型与字符集的底层约定
2.1 库、表、字段的命名规范:从“看得懂”到“查得快”
命名规范是数据库设计规范里最容易被忽略却最影响协作的部分。我一般推荐使用小写字母、数字加下划线的方式,禁止驼峰和大小写混用。原因很简单:MySQL在Linux下对表名区分大小写,但Windows下不区分,跨环境迁移时驼峰命名极易引发“表找不到”的诡异错误。库名建议用业务域缩写,比如order、crm,表名用业务实体名加业务域前缀,例如crm_customer,避免不同业务模块出现同名的user表。
字段命名上,主键统一叫id,业务自然键叫xxx_no(比如order_no),关联外键叫business_id这样带业务含义的名字。布尔类型建议用is_xxx或has_xxx,状态字段用status,时间字段统一create_time、update_time。这样做的好处是,哪怕一个新人接手,也能从字段名推断出它的语义,不会出现flag、type这种十个表十个含义的烂命名。
2.2 字段类型选择:用最小的可表示空间换性能
类型选择的核心原则是“够用就行,留一点点余地”。常见做法是:整型用INT,如果存储的主键或雪花ID超过2^31就用BIGINT;金额使用DECIMAL(10,2)或更大精度,禁止使用FLOAT和DOUBLE,因为浮点数在比较运算上会出错。字符串方面,短字符串用CHAR,变长用VARCHAR,但需要注意VARCHAR长度不是字符数,而是字符数乘以字符集的最大字节数,utf8mb4下每个字符最多4字节,所以VARCHAR(100)实际上最多占用400字节。
枚举和布尔类型的选择也值得单独说。MySQL的ENUM类型看起来很方便,但后续要扩展枚举值时就只能改表结构,而且引擎对ENUM的存储做了压缩,一旦排序或比较规则变化容易踩坑。我建议状态字段用TINYINT并在代码层定义常量,枚举的语义放代码里,数据库里只存数字。时间类型用DATETIME而不是TIMESTAMP,虽然TIMESTAMP只有4字节,但它支持的年份范围到2038年就有溢出风险,而且会受时区影响,DATETIME虽然占用8字节,但存储的是字面时间,不随会话时区改变,排错时更直观。
2.3 字符集与排序规则:选错utf8mb4的坑
字符集是数据库设计规范里最常被低估的一项。MySQL 8.0默认字符集是utf8mb4,排序规则是utf8mb4_0900_ai_ci,但很多老库还在用utf8,注意utf8在MySQL里其实是utf8mb3,最多只能存3字节的字符,像 Emoji 表情和部分生僻字根本存不进去。所以新库一律使用utf8mb4,排序规则建议选utf8mb4_0900_ai_ci(MySQL 8)或utf8mb4_general_ci(MySQL 5.7),不要用utf8mb4_bin,除非你明确需要区分大小写。
字符集不一致导致的问题非常隐蔽。比如表A是utf8mb4,表B是utf8,两张表做关联查询时,MySQL会尝试隐式转换字符集,导致索引失效。排查方法是用SHOW CREATE TABLE查看每张表的字符集,或者查询information_schema里TABLES表的TABLE_COLLATION字段。我一般会在初始化数据库时强制所有库、表、字段统一字符集,并把这写进规范文档。
提示:修改已有表的字符集要预估锁表时间。
ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4会重写全表,数据量大时建议用pt-online-schema-change或计划停机窗口执行。
3. 用可执行的DDL模板落地数据库设计规范
3.1 一份兼容MySQL 8的建表DDL模板
直接把规范变成模板是最有效的落地方式。下面这份DDL模板是我平时建表时使用的基准,它覆盖了主键、业务字段、审计字段、索引和表注释的所有约定。
CREATE TABLE `crm_customer` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', `customer_no` varchar(32) NOT NULL COMMENT '客户编号,业务自然键', `name` varchar(64) NOT NULL COMMENT '客户名称', `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1-正常,2-冻结,3-注销', `phone_mobile` varchar(20) DEFAULT '' COMMENT '手机号', `email` varchar(128) DEFAULT '' COMMENT '邮箱', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` 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_customer_no` (`customer_no`), KEY `idx_name` (`name`), KEY `idx_create_time` (`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='客户表';这段DDL里有几个关键设计:主键使用bigint unsigned而不是int,避免未来数据量超出有符号整型上限;业务自然键customer_no加了唯一索引,保证业务上同一客户不会被重复插入;逻辑删除字段deleted使用tinyint,而不是直接物理删除,方便审计和恢复。注意唯一索引uk_customer_no和逻辑删除字段之间是有冲突的:如果同一个客户被删除两次,第二次插入会因为唯一索引冲突而失败。常见解法是把deleted字段改成0表示未删除,删除时改成该行的主键ID,即deleted = id,这样唯一索引(customer_no, deleted)就能容纳多次删除的记录。
3.2 索引设计规范:不是越多越好,是“够用且可解释”
索引设计是数据库设计规范里最能体现功力的部分。我见过最极端的表有20多个索引,每个查询都想照顾到,结果写入时因为维护索引开销极大,查询优化器反而无法选出最佳执行计划。规范要求是:每个表索引数量建议控制在5个以内,每个索引的字段数控制在3个以内,覆盖查询的高频条件。
索引设计的基本原则是“等值在前,排序在后”。对于WHERE a=? AND b=? ORDER BY c这种查询,联合索引应该设计为(a,b,c),而不是将c放在前面。如果有范围查询,比如b > ?,那么b之后的字段无法用于排序或索引覆盖,这点需要结合EXPLAIN反复验证。下面是一个反例和正例:
-- 反例:索引顺序与查询条件不一致 SELECT * FROM crm_customer WHERE status = 1 AND create_time > '2024-01-01' ORDER BY name; -- 如果只建了 (status, name) 索引,create_time 的范围条件会让 name 无法走索引排序 -- 正例:等值字段在前,范围字段居中,排序字段最后 CREATE INDEX idx_status_time_name ON crm_customer(status, create_time, name);我一般要求开发同学在提交建表SQL时必须附上EXPLAIN输出,并且重点看type字段是否出现range或ref而不是ALL,看Extra字段是否出现Using filesort。如果出现Using filesort,说明索引没有覆盖排序,需要调整索引顺序或增加字段。联合索引还有一个容易被忽略的“左前缀”原则:如果查询条件只有create_time而没有status,上面的联合索引就无法生效,因此还要评估单列索引是否存在不可替代性。
3.3 主键、外键与唯一约束:什么时候不该用外键
外键这个设计规范在大多数互联网业务里是被禁止使用的。不是外键本身有问题,而是它会导致两个问题:一是高并发写入时外键检查和行锁会放大锁竞争,二是分库分表后外键约束根本没法跨库执行。所以常见做法是在应用层保证引用完整性,数据库里只保留普通索引来加速关联查询。逻辑上有关联的表,比如订单表和客户表,库表设计只加customer_id字段并建KEY,不建FOREIGN KEY。
唯一约束需要特别小心NULL值。MySQL中唯一索引允许多个NULL值,比如email字段如果没有NOT NULL DEFAULT '',两个用户都可以插入NULL的邮箱而不会触发唯一冲突。因此业务要求“邮箱唯一”时,字段必须定义为NOT NULL DEFAULT '',空字符串只会被一个用户占用,后续插入会撞唯一键。这个细节排查起来相当费劲,很多人查了半天代码也没想通为什么数据重复了。
4. 数据库设计规范在评审与变更中的落地
4.1 设计评审检查表:把规范变成Review清单
有了规范文档和DDL模板还不够,必须把它变成代码评审里卡得住的检查项。我常用的评审表分为四类:命名合规、类型合规、索引合理、容量预估。下面这个表格可以在团队里直接复用:
| 检查项 | 规范要求 | 违规示例 |
|---|---|---|
| 表名命名 | 小写+下划线,带业务域前缀 | UserInfo、user_info_2024 |
| 主键类型 | bigint unsigned或bigint | int主键 |
| 金额类型 | DECIMAL或INT(分) | FLOAT、DOUBLE |
| 逻辑删除 | deletedtinyint,默认0 | 物理删除或status=-1兼作删除 |
| 时间字段 | DATETIME,不允许字符串 | varchar(20)存时间 |
| 每表索引数 | ≤5个,不允许冗余联合索引 | 单表10个索引 |
| 字符集 | utf8mb4,排序规则统一 | utf8、latin1 |
| 大字段 | TEXT/BLOB不得直接放在查询频繁的表 | 把日志内容放业务表 |
评审时不仅要看建表语句,还要看把数据量放大100倍之后是否还能跑。比如一个VARCHAR(2000)的字段在InnoDB里可能会被压缩到溢出页,如果频繁查询该字段,性能就会下降。这时候应该把大字段拆到附属表,或者用JSON类型存储但避免用JSON字段做条件过滤。
4.2 用SQL查询元数据校验规范执行情况
与其靠人工Review两眼一摸黑,不如直接查元数据把违规项捞出来。MySQL的information_schema能拿到所有库表的元数据。下面这段SQL可以找出所有不是utf8mb4的表:
SELECT table_schema AS '库名', table_name AS '表名', table_collation AS '排序规则' FROM information_schema.tables WHERE table_schema = 'your_db_name' AND table_collation NOT LIKE 'utf8mb4%';再比如排查表中是否存在没有主键的表,可以直接这样查:
SELECT t.table_schema, t.table_name FROM information_schema.tables t LEFT JOIN information_schema.table_constraints c ON t.table_schema = c.table_schema AND t.table_name = c.table_name AND c.constraint_type = 'PRIMARY KEY' WHERE t.table_schema = 'your_db_name' AND c.constraint_name IS NULL;这两条SQL脚本建议放在每周巡检任务里,跑出来的结果直接粘贴到技术周报,推动责任方修正。还需要检查的一个隐藏指标是自增主键的饱和度:如果表的主键是int unsigned,最大值到42.9亿,用下面的SQL可以查看当前自增值距离上限还差多少:
SELECT table_name, auto_increment, (auto_increment / 4294967295) * 100 AS '容量使用百分比' FROM information_schema.tables WHERE table_schema = 'your_db_name' AND auto_increment IS NOT NULL ORDER BY auto_increment DESC;4.3 拆分与归档:当表数据量突破边界
数据库设计规范不仅要管“建表时”,还要管“表变大了以后”。比如业务表超过2000万行或者单表容量超过20GB时,就该考虑归档和拆分。归档的常规做法是定时把status=3的历史数据迁移到历史表或冷存储,比如crm_customer_archive。拆分则分垂直和水平两种,垂直拆分是把大字段和不常查的字段拆到另一张表,水平拆分一般按customer_no%16分16个库或表。
这里有个规范要求:任何拆分都必须保留原来的查询路由口径。如果你按用户ID拆分,那查询就必须带上用户ID,否则全分片扫描。所以拆分前要明确定位“必带条件的查询”和“全局查询”,全局查询走ES或宽表层,而不是直接打全分片。我遇到过因为拆分后漏传分片键导致跨库查询慢到30秒的案例,最终不得不对应用层加一道参数校验,强制缺少分片键的请求直接报错。
5. 分库分表后,数据库设计规范要怎么变
分库分表之后,数据库设计规范里有一部分会失效,比如自增主键会重复,UNIQUE KEY无法跨库生效。这时需要把主键生成方式切换成雪花ID或号段模式,同时将唯一约束下放到应用层。表名也会多出_0000这种后缀,原来的表名规范列就必须补充通配规则和路由键定义。我一般在分片方案里额外加两条硬性要求:一是每张分片表的字段结构必须完全一致,用同一套DDL脚本发布;二是分片键必须是查询的必选条件,应用层的ORM或DAO里通过注解或包装类强制注入。
数据迁移操作用pt-archiver做分批删除是最稳的方案。比如要清理半年之前的日志数据,按主键ID分批删除,避免一次性锁大量行:
pt-archiver \ --source h=127.0.0.1,P=3306,D=log_db,t=access_log \ --where "create_time < '2024-01-01'" \ --limit 1000 \ --bulk-delete \ --commit-each这个命令的参数含义是:--limit 1000指定每批处理1000行,--bulk-delete用批量删除代替逐行删除,--commit-each表示每批都提交事务。注意执行环境需要安装Percona Toolkit,执行前建议先加--dry-run参数预览会删除的行数,避免误删。跑完后再对目标表执行OPTIMIZE TABLE回收空间,但此操作会锁表,生产环境建议使用pt-online-schema-change或放在维护窗口执行。
本文还有配套的精品资源,点击获取