一、把“状态”存成整数,结果生产环境炸了锅
坑点:用 1, 2, 3, 4 代表订单状态,没注释、没枚举定义。
真实案例:
某电商公司,DBA 在表里建了个 order_status INT,没人知道 1 是“待付款”还是“已取消”。半年后业务方改需求,把 2 改成“已发货”,原来写死的代码全乱套,导致用户看到订单状态错乱,投诉率飙升。
正确做法:
-- 推荐:用 ENUM 或明确注释 + 字典表
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
status TINYINT NOT NULL COMMENT '状态: 0-待付款, 1-已付款, 2-已发货, 3-已完成, 4-已取消',
-- 或者用字典表关联
status_code VARCHAR(20) NOT NULL COMMENT '关联 sys_dict 表的 code'
);
给小朋友的比喻:就像你给玩具分类,如果用“1号盒”“2号盒”,别人不知道里面是什么;但如果贴上“汽车”“积木”标签,谁都看得懂。
二、主键用字符串,查询慢到怀疑人生
坑点:用 UUID 或业务编码(如 ORD202310240001)做主键。
真实案例:
某物流系统,订单表主键用 VARCHAR(32) 存储 UUID,数据量达到 5000 万后,索引体积巨大,分页查询响应时间从 200ms 飙到 5s,系统几乎不可用。
正确做法:
-- 推荐:自增 BIGINT 或雪花算法生成的 BIGINT
CREATE TABLE logistics_orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY, -- 或 BIGINT 雪花算法
order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '业务单号,单独建唯一索引'
);
-- 业务单号单独建索引,不占主键位置
CREATE INDEX idx_order_no ON logistics_orders(order_no);
给小朋友的比喻:主键就像身份证号码,要短小精悍、唯一且有序;如果每次都用一串很长的英文字母当身份证,办理业务时查找起来就慢多了。
三、时间字段混用 DATETIME 和 TIMESTAMP,时区问题搞到崩溃
坑点:同一张表里同时用 DATETIME 和 TIMESTAMP,应用服务器部署在不同时区区域。
真实案例:
某跨国企业,用户表用 TIMESTAMP 存注册时间(自动转 UTC),订单表用 DATETIME 存下单时间(不转时区)。服务器迁移后,用户看到自己的注册时间“突然变了”,订单时间也对不上,客服被问爆。
正确做法:
-- 推荐:全表统一用 DATETIME + 应用层统一处理时区
CREATE TABLE users (
id BIGINT PRIMARY KEY,
register_time DATETIME NOT NULL COMMENT '注册时间,应用层统一转为 UTC 存储'
);
-- 或者统一用 TIMESTAMP,但要在应用层明确时区处理
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间,应用层指定时区'
);
给小朋友的比喻:时间就像你每天的作息,如果用北京时间记早起时间,用纽约时间记睡觉时间,你自己都搞混了什么时候该起床。最好统一用一种计时方式。
四、字段名用中文拼音,维护人员集体罢工
坑点:字段名用 yonghu_ming、zhangdan_jine 等拼音命名。
真实案例: 某传统企业信息化项目,数据库字段全是拼音,新员工入职一个月看不懂表结构,老员工离职后项目几乎无法维护,最终被迫重写整个数据层。
正确做法:
-- 推荐:全英文命名,见名知意
CREATE TABLE users (
id BIGINT PRIMARY KEY,
username VARCHAR(50) NOT NULL COMMENT '用户名',
account_balance DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '账户余额'
);
给小朋友的比喻:就像你的书包,如果贴上“书”“笔”“橡皮”的标签,谁都看得懂;如果贴上“shu”“bi”“xiangpi”,外人就不知道里面装的是什么了。
五、把所有信息塞进一个 JSON 字段,查询性能断崖式下跌
坑点:为了“灵活”,把用户属性、地址、偏好等全部塞进 extra_info JSON 字段。
真实案例: 某社交 App,用户表有 2 亿条记录,所有扩展字段存 JSON。当需要按“用户偏好标签”筛选推荐内容时,全表扫描 JSON 字段,CPU 打满,查询超时,APP 直接崩盘。
正确做法:
-- 推荐:结构化存储,必要时用冗余字段或独立扩展表
CREATE TABLE users (
id BIGINT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
city VARCHAR(50) COMMENT '常用城市,独立字段便于查询',
-- 极少查询的扩展信息才考虑 JSON
extra_info JSON COMMENT '不常用扩展信息'
);
-- 或者用 EAV 模式(需谨慎评估查询性能)
CREATE TABLE user_attributes (
user_id BIGINT,
attr_key VARCHAR(50),
attr_value VARCHAR(255),
PRIMARY KEY (user_id, attr_key)
);
给小朋友的比喻:JSON 就像一个大口袋,把所有东西都扔进去,找的时候得把口袋翻个底朝天;而独立字段就像把每件东西放进不同的抽屉,拉开就能拿到。
六、忽视字符集,中文数据乱码成谜
坑点:数据库默认字符集 latin1,应用层用 UTF-8,插入中文后显示 ??? 或乱码。
真实案例:
某跨境电商系统,数据库建表时没指定字符集,欧洲用户名字中的特殊字符(如 é, ü)存入后变成乱码,导致用户无法登录,海外业务差点停摆。
正确做法:
-- 推荐:数据库、表、连接层统一 UTF8MB4
CREATE DATABASE company_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE TABLE users (
id BIGINT PRIMARY KEY,
name VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 连接层也统一
-- JDBC: jdbc:mysql://host:port/db?characterEncoding=utf8mb4
给小朋友的比喻:字符集就像语言的翻译规则,如果数据库说“我只懂英文”,应用说“我要传中文”,那就谁也听不懂谁,只能乱码收场。
七、大文本字段放错位置,读写性能双杀
坑点:把 content TEXT(长文本)放在高频访问的主表中。
真实案例:
某内容社区,文章表 articles 包含 content LONGTEXT,每次列表查询都返回完整内容(开发者误以为用 ORM 会自动过滤),导致每次查询 IO 巨大,数据库 IOPS 打满,整个集群响应缓慢。
正确做法:
-- 推荐:大文本字段单独放扩展表,主表只存摘要
CREATE TABLE articles (
id BIGINT PRIMARY KEY,
title VARCHAR(200) NOT NULL,
summary VARCHAR(500) COMMENT '摘要,用于列表展示'
);
CREATE TABLE articles_content (
article_id BIGINT PRIMARY KEY,
content MEDIUMTEXT COMMENT '正文,单独存储'
);
给小朋友的比喻:主表就像快递单,只写地址和电话;内容就像快递里的货物,要单独打包存放,不能把货物绑在快递单上一起送,那样既慢又容易坏。
八、索引乱建,写操作慢到无法忍受
坑点:看到查询慢就加索引,没分析查询模式,一张表建了 15 个索引。
真实案例: 某订单系统,订单表有 10 个索引,写操作(INSERT/UPDATE)性能下降 80%,高峰期订单创建成功率从 99.9% 降到 95%,大量订单超时失败。
正确做法:
-- 推荐:只建必要索引,定期 review 无用索引
-- 1. 主键自动有索引
-- 2. 外键建索引
CREATE INDEX idx_user_id ON orders(user_id);
-- 3. 高频查询条件建索引
CREATE INDEX idx_status_create_time ON orders(status, create_time);
-- 4. 联合索引注意最左前缀原则
-- 5. 定期清理无用索引
SELECT table_name, index_name, cardinality
FROM information_schema.statistics
WHERE table_schema = 'your_db'
ORDER BY table_name, index_name;
给小朋友的比喻:索引就像书的目录,有用的目录能帮你快速找到内容;但如果每页都加目录,书会变得又厚又重,翻页都变慢。不是所有书都需要目录,重要的章节才需要。
九、金额字段用 FLOAT/DOUBLE,算账算出几毛钱的误差
坑点:用 FLOAT 或 DOUBLE 存金额,浮点数精度丢失。
真实案例:
某金融系统,用 FLOAT 存用户余额,经过多次充值、消费、退款后,用户发现账户少了几毛钱,查账发现是浮点运算累积误差,引发监管调查。
正确做法:
-- 推荐:金额用 DECIMAL 精确存储
CREATE TABLE accounts (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
balance DECIMAL(19,4) NOT NULL DEFAULT 0 COMMENT '余额,精确到分',
frozen_amount DECIMAL(19,4) NOT NULL DEFAULT 0 COMMENT '冻结金额'
);
-- 业务层计算也用 BigDecimal(Java)或 Decimal(Python)
-- Java 示例:
BigDecimal balance = new BigDecimal("100.00");
balance = balance.add(new BigDecimal("50.50")); // 正确
// 不要用 float 或 double 做金额计算
给小朋友的比喻:金额就像钱,必须精确到分;如果用近似值(比如 3.14 代替 π),算账时就会差一点点,积少成多就会差很多。DECIMAL 就像用计算器一步步精确算,不会出错。
十、设计时不考虑逻辑删除,数据清理变成灾难
坑点:用 DELETE 物理删除数据,导致数据丢失无法恢复,关联数据不一致。
真实案例:
某教育平台,用户注销账号后直接 DELETE 用户记录,导致该用户的历史课程记录、学习进度、评论全部丢失,用户投诉“我的学习记录没了”,且关联表出现大量孤儿数据。
正确做法:
-- 推荐:逻辑删除,软删除
CREATE TABLE users (
id BIGINT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除: 0-正常, 1-已删除',
deleted_at DATETIME COMMENT '删除时间'
);
-- 查询时过滤已删除数据
SELECT * FROM users WHERE is_deleted = 0;
-- 插入唯一索引时也要排除已删除数据(MySQL 8.0+)
ALTER TABLE users ADD UNIQUE INDEX uk_username_deleted (username, is_deleted);
给小朋友的比喻:逻辑删除就像把书从书架上拿下来放进“待处理箱”,而不是直接扔进碎纸机。万一以后需要(比如用户后悔注销了),还能从箱子里找回来;而物理删除就像碎纸机,一旦碎了就找不回来了。
总结:数据库设计不是“能用就行”
这 10 个坑,每一个都来自真实的生产事故。好的数据库设计不是看你能加多少功能,而是看你能避开多少坑。
核心原则:
- 命名规范:全英文、见名知意、统一风格
- 类型选择:金额用 DECIMAL,时间统一,大文本分离
- 索引策略:少而精,不滥用
- 删除方式:优先逻辑删除
- 字符集:UTF8MB4 保底
- 文档配套:字段注释不能少
设计数据库时,多花 1 小时思考,可能节省 100 小时的事后救火。记住:数据库是应用的骨架,骨架歪了,应用永远站不稳。
