摘要:最近接了个外包,和另外两个哥们一起开发。因为他们的时间更充裕,所以前期的表结构都是他们来设计,我没有参与。等我空下来开始做我的部分时,两眼一抹黑,光看表名完全不知道是什么东西。
最近接了个外包,和另外两个哥们一起开发。因为他们的时间更充裕,所以前期的表结构都是他们来设计,我没有参与。等我空下来开始做我的部分时,两眼一抹黑,光看表名完全不知道是什么东西。
我不理解,但是大受震撼。
比如说:
DC_COURSE_TESTPAPER 课程下的问卷 DC_COURSE_PAPER_HISTORY 问卷与学生的关联表,也就是问卷下发给哪些学生 DC_COURSE_PAPER_HISTORY_ALL 问卷与问题的关联表,也就是问卷包含哪些问题光看表名能知道是什么意思吗?难受啊兄弟们。
这让我深刻意识到了:哪怕是一些“高级开发”,也 #技术分享并不知道怎么去设计一个好的表结构。
于是决定花点时间写一篇文章,和大家一起探讨如何更好的设计表结构。
所有观点都是我结合多年的经验得来,不一定正确,如有错误之处欢迎大家指正。
前缀在大型系统中是有必要的,可以区分不同业务模块,但关键是前缀要有明确含义,表名要语义清晰。
❌ 不好的命名
DC_COURSE_TESTPAPERTB_USER_INFOT_ORDER_DTLDATA_TBL_001✅ 好的命名
DC_COURSE_QUESTIONNAIRESSYS_USER_PROFILESORDER_ITEMSLMS_STUDENT_SCORES区别在哪里?
不好的:缩写让人猜测,TESTPAPER、INFO、DTL这些词汇模糊不清好的:前缀有明确业务含义,表名用完整英文词汇表达准确含义什么时候需要前缀?
多个业务系统共用数据库: USER_ , ORDER_ , PRODUCT_区分不同数据类型: LOG_ , CONFIG_ , TEMP_大型项目的模块划分: CRM_ , ERP_ , CMS_❌ 不好的命名
kecheng_wenjuanuser_xinxi订单_items✅ 好的命名
course_questionnairesuser_profilesorder_items原因
英文是编程的通用语言,团队成员更容易理解避免编码问题在一些专业且复杂的业务系统(比如医疗行业)中,会提倡会拼音来作为字段名,但是表名还是英文为主,这个后面展开讲。
❌ 不好的命名
data_table_001 temp_storage middle_table relation_mapping✅ 好的命名
student_scoresfile_uploadscourse_enrollmentsuser_preferences除非是临时用的表,不参与任何业务逻辑,只是用来做数据处理或者测试。
❌ 不好的命名
activedeleteflag✅ 好的命名
is_activeis_deletedis_verified也有些团队会用 if_ 作为前缀,这也没什么毛病。
好处
用 is_ 开头有几个明显的好处:
一眼就能看出是布尔值 - 看到 is_active 就知道这个字段要么是 true 要么是 false ,不用再去猜避免歧义 - 像 active 这样的名字,你搞不清楚它表示的是状态还是动作。 is_active 就明确表示状态代码可读性更好 - 写代码的时候, if (user.is_active) 比 if (user.active) 更容易理解❌ 不好的命名
create_time update_date delete_at register_datetime✅ 好的命名
created_atupdated_atdeleted_atregistered_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 字段加索引,提高查询性能❌ 不好的命名
usercourseteacher_key✅ 好的命名
user_idcourse_idteacher_id这个好处,应该不用过多赘述了。
除了上面这些基本的命名规范,还有一些实用的经验:
逻辑删除用时间字段 - 用 deleted_at 比 is_deleted 更好,不仅能看出来删除时间,还能用于数据恢复和历史追踪状态字段用枚举 - 不要用数字 1、2、3 表示状态,用 status 字段,值用 'pending' 、 'approved' 、 'rejected' 这样的英文单词金额字段用 decimal - 不要用 float 或 double ,用 decimal(10,2) 这样的类型,避免浮点数精度问题密码字段要加密 - 密码字段名用 password_hash 或 encrypted_password ,不要直接叫 password| 字段名 | 类型 | 说明 | | ---
| id | BIGINT | 主键 | | user_id | BIGINT | 用户 ID(外键) | | order_no | VARCHAR(32) | 订单号 | | total_amount | DECIMAL(10,2) | 总金额 | | status | VARCHAR(20) | 订单状态 | | created_at | TIMESTAMP | 创建时间 |
这样设计的好处:
通过 user_id 就知道订单属于哪个用户JOIN 一下就能拿到用户的所有订单新增订单字段不影响用户表情况1:有业务含义的关系表 比如学生和课程的关系,不只是简单关联,还有报名时间、状态等业务信息:
❌ 不好的设计
student_course_relsc_mappingmiddle_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_id | BIGINT | 文章 ID | | tag_id | BIGINT | 标签 ID |
有时候为了避免复杂的 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基本不会变动的情况DC_COURSE_TESTPAPERDC_COURSE_PAPER_HISTORYDC_COURSE_PAPER_HISTORY_ALL| 字段名 | 类型 | 说明 | | ---
| id | BIGINT | 主键 | | course_id | BIGINT | 课程 ID | | title | VARCHAR(200) | 问卷标题 | | description | TEXT | 问卷描述 | | status | ENUM | 状态:draft/published/closed | | created_at | TIMESTAMP | 创建时间 |
| 字段名 | 类型 | 说明 | | ---
| 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 这样的表名写代码。
最后一句话:代码是写给人看的,表结构也是建给人用的。
来源:墨码行者