范式与库表设计

好的表结构是性能与可维护性的地基。设计阶段多花一小时,往往能省下上线后的无数次迁移。本章讲清从 ER 建模到落地建表的完整过程。

设计流程

  1. 需求分析:梳理实体(名词)与关系(动词),明确查询模式。
  2. 概念建模:画 ER 图,确定实体、属性与基数(1:1、1:N、M:N)。
  3. 逻辑设计:转成关系表,定义主键、外键与约束。
  4. 规范化:按范式消除冗余,再按性能需要适度反范式
  5. 物理设计:选择字段类型、字符集、分区与索引。

三大范式

范式要求消除的问题
第一范式 1NF每列原子、不可再分重复组、逗号拼接的列表
第二范式 2NF非主属性完全依赖主键部分依赖(联合主键场景)
第三范式 3NF非主属性不传递依赖冗余的派生列
-- 违反 3NF:city_code 依赖 city,而 city 依赖 id(传递依赖)
-- 拆分为独立表消除冗余
CREATE TABLE cities (
    code VARCHAR(8) PRIMARY KEY,
    name VARCHAR(64) NOT NULL
);

CREATE TABLE users (
    id        BIGINT PRIMARY KEY,
    name      VARCHAR(64) NOT NULL,
    city_code VARCHAR(8)  NOT NULL REFERENCES cities (code)
);

反范式:为查询而冗余

范式减少写入异常,但会带来更多连接,影响读性能。读多写少的场景常有意冗余

  • 订单表冗余 user_name,避免每次列表查询都连接用户表。
  • 统计表冗余 order_count,用触发器或异步任务维护。
  • 宽表冗余维度字段,服务于 OLAP 分析。
Tip

反范式的前提是有明确的维护策略:冗余字段由谁在何时更新?若无法保证一致性,宁可不冗余。工程上常见「以范式为底,以反范式为性能补丁」。

主键选型

方案优点缺点
自增 BIGINT有序、紧凑、索引友好暴露业务量、分库分表易冲突
UUID全局唯一、可客户端生成随机写入导致页分裂、占用大
雪花 ID趋势递增、全局唯一依赖时钟、实现稍复杂
Warning

随机主键(如 UUID v4)在 InnoDB 中会造成页分裂与空间碎片,显著降低写入性能。需要分布式唯一 ID 时,优先选择趋势递增的雪花类 ID,或使用有序 UUID(如 ULID/UUIDv7)。

字段与类型

  • 够用就好:能用 INT 别用 BIGINT,能用 VARCHAR(n) 别用 TEXT(需权衡)。
  • 金额用 DECIMALFLOAT/DOUBLE 存在精度误差,不可用于金额。
  • 时间用 TIMESTAMP/DATETIME:统一时区约定(推荐存 UTC)。
  • 状态用 TINYINT + 注释:比字符串省空间、比较快。
  • 布尔用 TINYINT(1)(MySQL)或原生 BOOLEAN(PostgreSQL)。
  • 避免 NULL 泛滥NULL 参与索引与比较时有特殊语义,能设默认值就设。
CREATE TABLE payments (
    id         BIGINT        PRIMARY KEY,
    order_id   BIGINT        NOT NULL,
    amount     DECIMAL(12,2) NOT NULL,          -- 金额用定点数
    currency   CHAR(3)       NOT NULL DEFAULT 'CNY',
    status     TINYINT       NOT NULL DEFAULT 0, -- 0 待支付 1 成功 2 失败
    created_at TIMESTAMP     NOT NULL DEFAULT CURRENT_TIMESTAMP
);

关系基数与中间表

多对多关系需要中间表(关联表),并常在其上附加关系属性:

CREATE TABLE user_roles (
    user_id    BIGINT NOT NULL,
    role_id    BIGINT NOT NULL,
    granted_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 关系属性
    PRIMARY KEY (user_id, role_id)
);

小结

  • 先规范化消除冗余,再按查询模式适度反范式,并明确一致性维护策略。
  • 主键优先趋势递增;避免随机主键带来的写入放大。
  • 类型选择「够用、精确、有默认值」;金额用 DECIMAL,时间统一 UTC。
  • 多对多用中间表,并把关系自身的属性放在中间表上。