对账差了三分钱
老王的对账系统每月差几毛几分,怎么都对不上。最后抓到真凶:金额字段用了 double。浮点数是二进制近似存储,0.1 都表示不精确,千万笔订单的误差累积起来就是一笔糊涂账。表设计的债,上线那天开始计息——这篇把最贵的坑一次列全。
金额:DECIMAL 或整数分
amount DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT "订单金额" -- 精确小数
-- 或:amount_cents BIGINT NOT NULL DEFAULT 0 COMMENT "金额(分)"两条路都行:DECIMAL 精确直观,整数「分」连乘除误差都没有(Java 侧配合 long 或 BigDecimal),核心原则只有一条——float 和 double 别碰金额。
手机号与状态字段
- 手机号用 varchar(20):别用 int(存不下 11 位数字之外还有国际区号),别用 bigint(丢前导零、加不进 +86)。就按字符串存,它本质是标识不是数字;
- 状态用 tinyint + 字典表:status char(10) 存中文「已支付」是老项目的经典手笔——改文案要改数据、聚合要函数、索引又宽又慢。tinyint 只占 1 字节,语义交给字典与常量枚举;
- IP 用 varchar(45) 兼容 IPv6,密码哈希 varchar(100+),二维码 token 这类用 varchar 别 char(char 定长有尾部补位行为,存储省的读时还得剥)。
时间与主键
| 字段 | 推荐 | 理由 |
|---|---|---|
| 业务时间 | datetime(3) | 无时区转换歧义;毫秒精度;范围 9999 年,没有 2038 问题 |
| 记录创建/更新 | datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | 数据库自己管,应用忘了赋值也有兜底 |
| 主键 | bigint 自增 / 趋势递增雪花 ID | B+ 树顺序追加写(第 3 篇);UUID 随机插入引发页分裂,二级索引全部跟着膨胀 |
timestamp 也能用(4 字节更省 + 自带时区转换),但 2038 上限和时区行为是隐患——跨时区业务慎用,国内单时区业务用着也没毛病,团队统一即可。
字符集:全库 utf8mb4
MySQL 的 utf8 是残废的 three-byte 实现,存不了 emoji 和部分生僻字——用户昵称存个「😀」直接报错或截断。铁律:库、表、连接三层全 utf8mb4,COLLATE 统一(推荐 utf8mb4_0900_ai_ci)。第 5 篇讲过的「JOIN 字符集不一致索引失效」,根子就在建表没对齐。
NULL 的两笔账
能 NOT NULL + 默认值的列就别允许 NULL:一是 NULL 参与比较和聚合的语义反直觉(count(col) 不数 NULL 行、= NULL 永远为假要用 IS NULL);二是 NULL 列让索引统计、优化器估算更复杂,key_len 还要多 1 字节标记(第 2 篇算过)。确实表示「未知」的语义才用 NULL,别拿 NULL 当默认值偷懒。
设计清单
- 金额 DECIMAL 或整数分,禁 float/double;
- 手机号、编号、卡号这类标识用字符串;状态用 tinyint + 字典;
- 时间 datetime(3),主键 bigint 趋势递增,禁 UUID 主键;
- utf8mb4 三层统一,COLLATE 一致;
- NOT NULL + 默认值优先;每表必有小整数 status、create_time、update_time 三件套;
- 字段数控制、大字段(长文本/JSON)拆扩展表,避免行溢出拖累热查询。
表设计规范立完,接下来是查询侧最经典的两大慢性病:深分页和 count 统计——下一篇一并开方。
咖啡凉了,记得趁热喝。
评论 (0)