亲身经历
最近接了个外包,和另外两个哥们一起开发。因为他们的时间更充裕,所以前期的表结构都是他们来设计,我没有参与。等我空下来开始做我的部分时,两眼一抹黑,光看表名完全不知道是什么东西。
我不理解,但是大受震撼。
比如说:
DC_COURSE_TESTPAPER 课程下的问卷
DC_COURSE_PAPER_HISTORY 问卷与学生的关联表,也就是问卷下发给哪些学生
DC_COURSE_PAPER_HISTORY_ALL 问卷与问题的关联表,也就是问卷包含哪些问题
光看表名能知道是什么意思吗?难受啊兄弟们。
这让我深刻意识到了:哪怕是一些"高级开发",也并不知道怎么去设计一个好的表结构。
于是决定花点时间写一篇文章,和大家一起探讨如何更好的设计表结构。
所有观点都是我结合多年的经验得来,不一定正确,如有错误之处欢迎大家指正。

表名:第一眼就要知道是干什么的
1. 有意义的前缀 + 清晰的表名
前缀在大型系统中是有必要的,可以区分不同业务模块,但关键是前缀要有明确含义,表名要语义清晰。
❌ 不好的命名
bash
DC_COURSE_TESTPAPER # TESTPAPER是问卷还是试卷?
TB_USER_INFO # TB前缀无意义,INFO太泛泛
T_ORDER_DTL # DTL是detail的缩写?
DATA_TBL_001 # 完全看不懂
✅ 好的命名
bash
DC_COURSE_QUESTIONNAIRES # DC表示Distance Course远程课程系统
SYS_USER_PROFILES # SYS表示系统核心模块
ORDER_ITEMS # 订单商品明细
LMS_STUDENT_SCORES # LMS表示Learning Management System
区别在哪里?
- 不好的:缩写让人猜测,TESTPAPER、INFO、DTL这些词汇模糊不清
- 好的:前缀有明确业务含义,表名用完整英文词汇表达准确含义
什么时候需要前缀?
- 多个业务系统共用数据库:
USER_
,ORDER_
,PRODUCT_
- 区分不同数据类型:
LOG_
,CONFIG_
,TEMP_
- 大型项目的模块划分:
CRM_
,ERP_
,CMS_
2. 用完整的英文单词而不是拼音
❌ 不好的命名
bash
kecheng_wenjuan # 拼音
user_xinxi # 中英混合
订单_items # 中英混合
✅ 好的命名
bash
course_questionnaires # 纯英文,语义清晰
user_profiles # 纯英文
order_items # 纯英文
原因
- 英文是编程的通用语言,团队成员更容易理解
- 避免编码问题
在一些专业且复杂的业务系统(比如医疗行业)中,会提倡会拼音来作为字段名,但是表名还是英文为主,这个后面展开讲。
3. 表名要体现业务含义,不要只是技术实现
❌ 不好的命名
data_table_001
temp_storage
middle_table
relation_mapping
✅ 好的命名
bash
student_scores # 学生成绩
file_uploads # 文件上传记录
course_enrollments # 课程报名
user_preferences # 用户偏好设置
除非是临时用的表,不参与任何业务逻辑,只是用来做数据处理或者测试。
字段命名:见名知意
1. 布尔字段用 is_ 开头
❌ 不好的命名
perl
active # 是激活还是活跃?
delete # 删除状态还是删除动作?
flag # 什么标志?
✅ 好的命名
bash
is_active # 是否激活
is_deleted # 是否已删除
is_verified # 是否已验证
也有些团队会用if_
作为前缀,这也没什么毛病。
好处
用 is_
开头有几个明显的好处:
-
一眼就能看出是布尔值 - 看到
is_active
就知道这个字段要么是true
要么是false
,不用再去猜 -
避免歧义 - 像
active
这样的名字,你搞不清楚它表示的是状态还是动作。is_active
就明确表示状态 -
代码可读性更好 - 写代码的时候,
if (user.is_active)
比if (user.active)
更容易理解
2. 时间字段统一后缀
❌ 不好的命名
create_time
update_date
delete_at
register_datetime
✅ 好的命名
bash
created_at # 创建时间
updated_at # 更新时间
deleted_at # 删除时间
registered_at # 注册时间
好处
-
一眼就能看出是时间字段 - 看到
created_at
就知道这是时间类型,不用去查表结构 -
避免命名混乱 - 有的用
_time
,有的用_date
,有的用_datetime
,团队里每个人习惯不一样,最后搞得乱七八糟 -
_at
在英语里表示"在某个时间点",比_time
更准确 -
逻辑删除的最佳实践 - 特别推荐用
deleted_at
做逻辑删除字段。这样设计有几个好处:- 能看出来删除的具体时间
- 可以追踪删除操作的历史
- 支持数据恢复(把
deleted_at
设为NULL
就行) - 比用
is_deleted
这种布尔字段更灵活
额外经验
除了基本的命名规范,还有一些实用的经验:
-
逻辑删除用时间字段 - 用
deleted_at
比is_deleted
好,能看出来删除时间,支持数据恢复和历史追踪 -
状态字段用枚举 - 不要用数字 1、2、3 表示状态,用
status
字段,值用'pending'
、'approved'
、'rejected'
这样的英文单词 -
金额字段用 decimal - 不要用
float
或double
,用decimal(10,2)
这样的类型,避免浮点数精度问题 -
密码字段要加密 - 密码字段名用
password_hash
或encrypted_password
,不要直接叫password
-
软删除要加索引 - 如果经常查询未删除的数据,给
deleted_at
字段加索引,提高查询性能
3. 外键字段统一 _id 后缀
❌ 不好的命名
bash
user # 这是用户ID还是用户对象?
course # 课程ID?
teacher_key # 什么key?
✅ 好的命名
bash
user_id # 用户ID
course_id # 课程ID
teacher_id # 教师ID
这个好处,应该不用过多赘述了。
4. 额外的经验
除了上面这些基本的命名规范,还有一些实用的经验:
-
逻辑删除用时间字段 - 用
deleted_at
比is_deleted
更好,不仅能看出来删除时间,还能用于数据恢复和历史追踪 -
状态字段用枚举 - 不要用数字 1、2、3 表示状态,用
status
字段,值用'pending'
、'approved'
、'rejected'
这样的英文单词 -
金额字段用 decimal - 不要用
float
或double
,用decimal(10,2)
这样的类型,避免浮点数精度问题 -
密码字段要加密 - 密码字段名用
password_hash
或encrypted_password
,不要直接叫password
表结构设计:关系清晰、适度冗余
1. 一对多关系:外键放在多的一边
让我们用用户和订单的业务关系来举例:
用户表 (users)
字段名 | 类型 | 说明 |
---|---|---|
id | BIGINT | 主键 |
username | VARCHAR(50) | 用户名 |
VARCHAR(100) | 邮箱 | |
created_at | TIMESTAMP | 创建时间 |
订单表 (orders)
字段名 | 类型 | 说明 |
---|---|---|
id | BIGINT | 主键 |
user_id | BIGINT | 用户ID(外键) |
order_no | VARCHAR(32) | 订单号 |
total_amount | DECIMAL(10,2) | 总金额 |
status | VARCHAR(20) | 订单状态 |
created_at | TIMESTAMP | 创建时间 |
这样设计的好处:
- 通过 user_id 就知道订单属于哪个用户
- JOIN 一下就能拿到用户的所有订单
- 新增订单字段不影响用户表
2. 多对多关系:中间表命名要体现关系
多对多关系的中间表命名要根据具体情况来选择:
情况1:有业务含义的关系表 比如学生和课程的关系,不只是简单关联,还有报名时间、状态等业务信息:
❌ 不好的设计
bash
student_course_rel # rel是什么关系?
sc_mapping # 缩写看不懂
middle_table # 完全不知道什么意思
✅ 好的设计
课程报名表 (course_enrollments)
字段名 | 类型 | 说明 |
---|---|---|
id | BIGINT | 主键 |
student_id | BIGINT | 学生ID |
course_id | BIGINT | 课程ID |
enrolled_at | TIMESTAMP | 报名时间 |
status | VARCHAR(20) | 报名状态 |
情况2:纯粹的关联关系表 如果只是单纯的多对多映射,没有额外的业务属性,用mapping
也是可以的:
用户角色关联表 (user_role_mappings)
字段名 | 类型 | 说明 |
---|---|---|
user_id | BIGINT | 用户ID |
role_id | BIGINT | 角色ID |
文章标签关联表 (article_tag_relations)
字段名 | 类型 | 说明 |
---|---|---|
article_id | BIGINT | 文章ID |
tag_id | BIGINT | 标签ID |
如何选择命名?
- 有业务含义的关系:用具体的业务名词,如
enrollments
、orders
、friendships
- 纯粹的映射关系:可以用
mappings
、relations
或直接用实体1_实体2s
- 关键是保持团队内命名风格的统一
3. 适当的字段冗余:提升查询效率
有时候为了避免复杂的JOIN查询,适当冗余是非常有必要的。
最典型的就是冗余上级ID:
订单详情表 (order_items)
字段名 | 类型 | 说明 |
---|---|---|
id | BIGINT | 主键 |
order_id | BIGINT | 订单ID |
user_id | BIGINT | 用户ID(冗余) |
product_id | BIGINT | 商品ID |
quantity | INT | 购买数量 |
price | DECIMAL(10,2) | 商品单价 |
为什么要冗余 user_id?
- 查询用户的所有购买记录时,直接查 order_items 表就行
- 不需要先通过 orders 表再关联到 order_items
- 一个查询代替了两表JOIN
商品评论表 (product_reviews)
字段名 | 类型 | 说明 |
---|---|---|
id | BIGINT | 主键 |
product_id | BIGINT | 商品ID |
category_id | BIGINT | 商品分类ID(冗余) |
user_id | BIGINT | 用户ID |
rating | TINYINT | 评分 |
content | TEXT | 评论内容 |
为什么要冗余 category_id?
- 按分类统计评分时,不需要JOIN商品表
- 查询某分类下的所有评论更高效
什么时候该冗余ID?
- 经常需要跨层级查询的场景
- 统计和报表查询频繁的字段
- 读多写少的关联关系
- 上级ID基本不会变动的情况
回到最初的案例
现在我们用上文讲的一些原则来重新设计开头的表:
❌ 原来的设计
bash
DC_COURSE_TESTPAPER # 什么鬼?
DC_COURSE_PAPER_HISTORY # HISTORY是历史?
DC_COURSE_PAPER_HISTORY_ALL # ALL又是什么意思?
✅ 重新设计
课程问卷表 (course_questionnaires)
字段名 | 类型 | 说明 |
---|---|---|
id | BIGINT | 主键 |
course_id | BIGINT | 课程ID |
title | VARCHAR(200) | 问卷标题 |
description | TEXT | 问卷描述 |
status | ENUM | 状态:draft/published/closed |
created_at | TIMESTAMP | 创建时间 |
问卷分发记录表 (questionnaire_assignments)
字段名 | 类型 | 说明 |
---|---|---|
id | BIGINT | 主键 |
questionnaire_id | BIGINT | 问卷ID |
student_id | BIGINT | 学生ID |
assigned_at | TIMESTAMP | 分发时间 |
status | ENUM | 状态:assigned/started/completed |
问卷题目表 (questionnaire_questions)
字段名 | 类型 | 说明 |
---|---|---|
id | BIGINT | 主键 |
questionnaire_id | BIGINT | 问卷ID |
content | TEXT | 题目内容 |
question_type | ENUM | 题目类型 |
sort_order | INT | 排序 |
现在再看:
course_questionnaires
- 一眼就知道是课程问卷questionnaire_assignments
- 问卷分发记录questionnaire_questions
- 问卷题目
是不是瞬间清晰了?
当然,因为业务简单,这里就不统一加前缀了。
写在最后
数据库表结构是项目的重中之重。
好的表设计让能让整个团队开发起来更顺畅,减少沟通成本,而且代码维护起来更容易,新人上手也更简单。
投入时间做好表结构设计,绝对是值得的投资。
没有人愿意每天面对DC_COURSE_PAPER_HISTORY_ALL
这样的表名写代码。
最后一句话:代码是写给人看的,表结构也是建给人用的。
