新功能要上线,产品经理说要加个字段。你一看表结构,倒吸一口凉气——这表已经30多个字段了,而且好几个字段还是用逗号分隔存一堆值。改还是不改?这是个问题。
类似的问题还有很多,比如:
- 需求变更时,发现表结构难以扩展
- 数据出现不一致,排查了半天才发现是设计缺陷
- 同事看不懂你的表结构,沟通成本极高
其实,这些问题很大程度上都可以在数据库设计阶段解决。
一、好设计 VS 坏设计
先看一个反面教材: #后端 #MySQL
CREATE TABLE user ( id INT PRIMARY KEY, name VARCHAR(255), phone VARCHAR(50), email VARCHAR(255), address VARCHAR(500), hobby VARCHAR(500), friend_list VARCHAR(1000), register_time DATETIME, last_login_time DATETIME, login_count INT, is_delete TINYINT);这个设计有什么问题?
- 一个 friend_list 字段存了所有好友ID,用逗号分隔
- hobby 也是用逗号分隔的多个爱好
- is_delete 字段命名不规范
- 所有字段都允许NULL,没有默认值
- 没有任何索引
这样的设计,项目初期可能没问题,但随着数据量增长,会带来无数麻烦。下面,我们就从几个核心原则开始,学习正确的设计方法。
二、数据库设计的五大黄金原则
1. 规范化设计
什么是规范化? 简单说,就是把相关数据拆分到不同的表中,避免数据冗余和异常。
第三范式(3NF)通俗解释: 每张表只描述一个主题,并且所有字段都必须直接依赖于主键。
例子: 错误设计:
CREATE TABLE order ( order_id INT, customer_name VARCHAR(100), customer_phone VARCHAR(20), product_name VARCHAR(100), product_price DECIMAL(10,2), order_date DATETIME);正确设计:
CREATE TABLE customer ( customer_id INT PRIMARY KEY, name VARCHAR(100) NOT NULL, phone VARCHAR(20) NOT NULL);CREATE TABLE product ( product_id INT PRIMARY KEY, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL );CREATE TABLE order ( order_id INT PRIMARY KEY, customer_id INT NOT NULL, order_date DATETIME NOT NULL, total_amount DECIMAL(12,2) NOT NULL, FOREIGN KEY (customer_id) REFERENCES customer(customer_id) );CREATE TABLE order_item ( item_id INT PRIMARY KEY, order_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL, price DECIMAL(10,2) NOT NULL, FOREIGN KEY (order_id) REFERENCES order(order_id), FOREIGN KEY (product_id) REFERENCES product(product_id) );为什么这样做更好?
- 当客户信息变更时,只需修改一处
- 产品价格变化不会影响历史订单记录
- 避免了数据不一致问题
- 查询更高效,特别是当需要汇总统计时
2. 命名规范:让人一眼看懂
好的命名就像一本说明书,让团队协作更顺畅。遵循以下规则:
- 表名 :使用复数名词,如 users , orders , products
- 字段名 :使用下划线分隔,如 created_at , updated_at
- 主键 :统一使用 id ,或者 表名_id ,如 user_id
- 外键 :使用 关联表名_id ,如 user_id , product_id
- 布尔字段 :使用 is_ 或 has_ 前缀,如 is_deleted , has_paid
- 时间字段 :统一使用 created_at , updated_at , deleted_at
反例:
- u_name , phonenumb , delFlag
- createTime , lastModifyTime (混用驼峰和下划线)
正例:
- user_name , phone_number , is_deleted
- created_at , updated_at
3. 字段类型选择:精准匹配数据
选择合适的数据类型不仅节省空间,还能提高查询效率。下面是一些常见场景的最佳实践:
| 数据类型 | 适用场景 | 示例 | 避免的错误 | | ---
| TINYINT(1) | 布尔值(是/否) | is_active TINYINT(1) DEFAULT 1 | 用 VARCHAR 存"yes"/"no" | | INT | 一般 ID、数量 | user_id INT, view_count INT | 用 BIGINT 存小范围数据 | | BIGINT | 大型系统 ID、高并发计数器 | order_id BIGINT | 小型应用过度使用 | | VARCHAR(N) | 长度可变的字符串,N 应合理设置 | name VARCHAR(50) | 所有字符串都用 VARCHAR(255) | | CHAR(N) | 固定长度字符串 | country_code CHAR(2) | 用 CHAR 存变长内容 | | DECIMAL(M,D) | 金额、精确小数 | price DECIMAL(10,2) | 用 FLOAT/DOUBLE 存金额 | | DATETIME | 需要日期和时间 | created_at DATETIME | 用字符串存时间 | | TIMESTAMP | 需要自动更新的时间戳 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | 混淆 DATETIME 和 TIMESTAMP | | ENUM | 固定选项(谨慎使用) | status ENUM('pending','paid','shipped') | 选项经常变化的场景 | | JSON | 确实需要存储结构化但不常查询的数据 | config JSON | 代替关系型设计 |
重点提醒: 金额一定要用 DECIMAL,而不是 FLOAT/DOUBLE!后者在计算时可能有精度丢失问题。
4. 索引设计:加速查询
索引就像书的目录,能让数据库快速定位数据。但索引不是越多越好,每个索引都会增加写入开销。
基本原则:
- 为经常用于WHERE条件的字段创建索引
- 为JOIN操作的关联字段创建索引
- 为ORDER BY和GROUP BY的字段考虑索引
- 单表索引数量不宜过多(通常不超过5个)
常见场景:
--CREATE INDEX idx_user_name ON users(name); CREATE INDEX idx_user_email ON users(email);--CREATE INDEX idx_order_user_time ON orders(user_id, created_at);--CREATE INDEX idx_product_category_price ON products(category_id, price);索引使用注意事项:
- 索引列不要用函数或表达式,如 WHERE YEAR(create_time)=2023
- 避免在索引列上使用NOT、<>、!=
- LIKE查询中,'%xxx'不会使用索引,'xxx%'会使用
- 联合索引要注意最左前缀原则
5. 软删除 和 硬删除:数据安全策略
硬删除: 直接从数据库移除记录
DELETE FROM users WHERE id = 1001;软删除: 标记记录为已删除,实际数据保留在数据库中
ALTER TABLE users ADD COLUMN is_deleted TINYINT(1) DEFAULT 0;ALTER TABLE users ADD COLUMN deleted_at DATETIME DEFAULT NULL;--UPDATE users SET is_deleted = 1, deleted_at = NOW() WHERE id = 1001;软删除的优势:
- 数据可恢复,降低误操作风险
- 保留历史记录,便于审计
- 保证关联数据完整性
- 便于数据分析
什么情况下用软删除?
- 核心业务数据(用户、订单、交易记录)
- 有法律合规要求的数据
- 需要保留历史状态的数据
什么情况下用硬删除?
- 临时数据、缓存数据
- 敏感数据需要彻底清除
- 存储空间极度紧张且数据价值低
三、高级技巧:为未来做准备
1. 预留扩展字段
为未来可能的需求变化预留一些通用字段:
ALTER TABLE usersADD COLUMN ext_info JSON COMMENT '扩展信息,存储不常用字段',ADD COLUMN version INT DEFAULT 1 COMMENT '乐观锁版本号';注意: 不要过度使用扩展字段,它只适用于少量不常用且无需查询的属性。
2. 分库分表的前期准备
即使初期不需要分库分表,也可以提前做些准备:

- 使用BIGINT作为主键类型
- 避免自增ID,考虑使用雪花算法等分布式ID
- 业务字段避免跨分片JOIN
- 考虑按时间或业务维度设计分片键
3. 事务边界设计
在设计表结构时,就要考虑事务边界:
--UPDATE accounts SET balance = balance -UPDATE accounts SET balance = balance + 100 WHERE user_id = 1002;良好的表设计应该让一个业务操作尽可能在一个事务内完成,避免分布式事务。
四、评论系统的设计
假设我们要设计一个文章评论系统,支持多级评论(评论可以回复评论),应该如何设计?
需求分析:
- 用户可以对文章发表评论
- 评论可以被回复,形成多级结构
- 需要统计每篇文章的评论数量
- 需要支持点赞功能
- 需要支持敏感词过滤
设计思路:
- 实体分析 :用户、文章、评论、点赞
- 关系分析 :
- 一个用户可以有多条评论
- 一条评论属于一篇文章
- 一条评论可以有多个回复
- 一条评论可以有多个点赞
- 表结构设计 :
CREATE TABLE articles ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(255) NOT NULL, content TEXT NOT NULL, user_id BIGINT NOT NULL, comment_count INT DEFAULT 0 COMMENT '评论数量,冗余字段提高查询效率', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, is_deleted TINYINT(1) DEFAULT 0) COMMENT='文章表';CREATE TABLE comments ( id BIGINT PRIMARY KEY AUTO_INCREMENT, article_id BIGINT NOT NULL COMMENT '所属文章 ID', user_id BIGINT NOT NULL COMMENT '评论用户 ID', parent_id BIGINT DEFAULT 0 COMMENT '父评论 ID,0表示一级评论', content VARCHAR(1000) NOT NULL COMMENT '评论内容', like_count INT DEFAULT 0 COMMENT '点赞数量', depth TINYINT DEFAULT 1 COMMENT '评论深度,1表示一级评论', path VARCHAR(255) DEFAULT '' COMMENT '路径,格式: 0,10,25 表示层级关系', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, is_deleted TINYINT(1) DEFAULT 0, KEY idx_article (article_id, created_at), KEY idx_parent (parent_id), FOREIGN KEY (article_id) REFERENCES articles(id) ) COMMENT='评论表';CREATE TABLE comment_likes ( id BIGINT PRIMARY KEY AUTO_INCREMENT, comment_id BIGINT NOT NULL, user_id BIGINT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uniq_comment_user (comment_id, user_id), FOREIGN KEY (comment_id) REFERENCES comments(id) ) COMMENT='评论点赞表';设计说明:
- 评论表使用了 parent_id + depth + path 的组合,支持高效查询评论树
- path 字段存储层级路径,如 0,10,25 表示ID为25的评论是ID为10的评论的子评论
- article.comment_count 是冗余字段,用于避免每次查询都COUNT
- 点赞表使用唯一索引防止重复点赞
- 所有表都有软删除标记和时间戳
查询所有一级评论及直接回复:
SELECT c1.*, c2.*FROM comments c1 LEFT JOIN comments c2 ON c2.parent_id = c1.id AND c2.depth = 2 WHERE c1.article_id = 100 AND c1.depth = 1 AND c1.is_deleted = 0 ORDER BY c1.created_at DESC, c2.created_at ASC;这种设计既支持高效查询,又能适应业务变化,是经过实战检验的可靠方案。
五、工具推荐:提高设计效率
- 数据库设计工具 :
- MySQL Workbench(免费)
- Navicat Data Modeler(付费)
- dbdiagram.io(在线免费)
- SQL规范检查 :
- Alibaba Java Coding Guidelines(包含SQL规范)
- SonarQube(支持SQL质量检查)
- 版本管理 :
- Flyway
- Liquibase
- 一定要使用版本控制管理数据库变更脚本!
六、总结
- 规范化是基础 :遵循第三范式,避免数据冗余
- 命名要一致 :统一的命名规范是团队协作的基石
- 类型要精准 :根据实际需求选择最合适的数据类型
- 索引要克制 :只为必要的查询条件创建索引
- 软删除更安全 :核心业务数据优先考虑软删除
- 为未来留余地 :考虑扩展性,但不要过度设计
- 文档不可少 :每个表、每个字段都要有清晰的注释