强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

MySQL 完全指南 / 第 9 章:表设计

第 9 章:表设计

好的表设计是系统稳定的基石。本章涵盖范式、反范式、分区表和字符集选择。


9.1 数据库三大范式

9.1.1 第一范式(1NF):字段原子性

每个字段不可再分。

-- ❌ 违反 1NF:地址字段包含了省、市、区
CREATE TABLE bad_design (
    id INT PRIMARY KEY,
    address VARCHAR(200)  -- "北京市朝阳区建国路88号"
);

-- ✅ 符合 1NF:拆分为独立字段
CREATE TABLE good_design (
    id INT PRIMARY KEY,
    province VARCHAR(50),  -- 北京市
    city VARCHAR(50),      -- 朝阳区
    detail_address VARCHAR(200) -- 建国路88号
);

9.1.2 第二范式(2NF):消除部分依赖

非主键字段必须完全依赖主键,不能只依赖主键的一部分。

-- ❌ 违反 2NF:course_name 只依赖 course_id,不依赖 student_id
CREATE TABLE student_course_bad (
    student_id INT,
    course_id INT,
    course_name VARCHAR(100),   -- 部分依赖
    score INT,
    PRIMARY KEY (student_id, course_id)
);

-- ✅ 符合 2NF:拆分为多表
CREATE TABLE courses (
    id INT PRIMARY KEY,
    name VARCHAR(100)
);

CREATE TABLE student_scores (
    student_id INT,
    course_id INT,
    score INT,
    PRIMARY KEY (student_id, course_id)
);

9.1.3 第三范式(3NF):消除传递依赖

非主键字段不能依赖其他非主键字段。

-- ❌ 违反 3NF:department_name 依赖 department_id,department_id 依赖 id
CREATE TABLE employees_bad (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    department_id INT,
    department_name VARCHAR(100)  -- 传递依赖
);

-- ✅ 符合 3NF
CREATE TABLE departments (
    id INT PRIMARY KEY,
    name VARCHAR(100)
);

CREATE TABLE employees (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    department_id INT
);

9.2 反范式设计

9.2.1 为什么需要反范式

严格遵循范式会导致过多的 JOIN,影响查询性能。适度冗余可以提升读取性能

9.2.2 常见反范式手段

手段说明示例
冗余列在子表中存储父表的常用字段orders 表冗余 user_name
派生列存储计算结果users 表存储 order_count
预计算汇总预先聚合daily_sales 汇总表
-- 范式设计:每次查询订单需要 JOIN 用户表
SELECT o.order_no, u.username, o.total_amount
FROM orders o
JOIN users u ON o.user_id = u.id;

-- 反范式设计:orders 表冗余 username
CREATE TABLE orders (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    order_no VARCHAR(32) NOT NULL,
    user_id BIGINT UNSIGNED NOT NULL,
    user_name VARCHAR(50) NOT NULL COMMENT '冗余用户名',
    total_amount DECIMAL(12,2) NOT NULL
);
-- 查询无需 JOIN
SELECT order_no, user_name, total_amount FROM orders;

9.2.3 范式 vs 反范式决策

因素范式化反范式化
写入频率高(冗余需同步更新)
读取频率高(避免 JOIN)
数据一致性要求极高允许短暂不一致
表关联数量

💡 实践建议:核心交易表用范式(一致性优先),报表/展示表用反范式(性能优先)。


9.3 命名规范

类目规范示例
数据库名小写 + 下划线myapp_db
表名小写 + 下划线,复数名词users, order_items
列名小写 + 下划线user_name, created_at
主键id表名单数_idid, user_id
外键列关联表名单数_iduser_id, order_id
索引idx_表名_列名idx_users_email
唯一索引uk_表名_列名uk_users_email
布尔字段is_ 前缀is_active, is_deleted
时间字段_at 后缀created_at, updated_at
状态字段_status 或 约定status, order_status

⚠️ 避免的命名

  • 不能使用 MySQL 保留字(如 order, group, key
  • 不要用拼音或拼音缩写
  • 不要用驼峰(MySQL 默认不区分大小写)

9.4 表设计最佳实践

9.4.1 每张表必备字段

CREATE TABLE example (
    id          BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
    created_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updated_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    deleted_at  DATETIME DEFAULT NULL COMMENT '软删除时间'
);

9.4.2 字段设计原则

-- 1. 尽量 NOT NULL + DEFAULT
-- ❌ 允许 NULL
phone VARCHAR(20) NULL

-- ✅ NOT NULL + 默认值
phone VARCHAR(20) NOT NULL DEFAULT '' COMMENT '手机号'

-- 2. 使用最小够用的类型
-- ❌ 所有字段用 VARCHAR(255)
-- ✅ 根据实际长度选择

-- 3. 金额用 DECIMAL 或 BIGINT 存分
-- ❌ FLOAT/DOUBLE
-- ✅ DECIMAL(12,2) 或 BIGINT UNSIGNED

-- 4. 主键用 BIGINT UNSIGNED
-- ❌ INT(可能溢出)
-- ❌ UUID(占空间、无序、影响 B+Tree 性能)

-- 5. 字符集统一 utf8mb4

9.5 分区表(Partitioning)

9.5.1 分区类型

类型说明适用场景
RANGE按范围分区时间序列数据(按月/年)
LIST按枚举值分区按地区/状态分区
HASH按哈希值分区均匀分布数据
KEY类似 HASH,使用 MySQL 内部哈希均匀分布

9.5.2 RANGE 分区(最常用)

-- 按年份分区的订单表
CREATE TABLE orders (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    order_no VARCHAR(32) NOT NULL,
    user_id BIGINT UNSIGNED NOT NULL,
    total_amount DECIMAL(12,2) NOT NULL,
    status TINYINT NOT NULL DEFAULT 0,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id, created_at),  -- 分区键必须在主键中
    INDEX idx_user_id (user_id),
    INDEX idx_order_no (order_no)
) ENGINE=InnoDB
PARTITION BY RANGE (YEAR(created_at)) (
    PARTITION p2023 VALUES LESS THAN (2024),
    PARTITION p2024 VALUES LESS THAN (2025),
    PARTITION p2025 VALUES LESS THAN (2026),
    PARTITION p2026 VALUES LESS THAN (2027),
    PARTITION p_future VALUES LESS THAN MAXVALUE
);

-- 查询会自动裁剪分区
EXPLAIN SELECT * FROM orders WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01';
-- partitions: p2026(只扫描 p2026 分区)

9.5.3 按月分区

-- 按月分区(需要定期添加新分区)
CREATE TABLE access_logs (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    user_id BIGINT UNSIGNED NOT NULL,
    action VARCHAR(50) NOT NULL,
    ip_address VARCHAR(45),
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id, created_at)
) ENGINE=InnoDB
PARTITION BY RANGE (TO_DAYS(created_at)) (
    PARTITION p202601 VALUES LESS THAN (TO_DAYS('2026-02-01')),
    PARTITION p202602 VALUES LESS THAN (TO_DAYS('2026-03-01')),
    PARTITION p202603 VALUES LESS THAN (TO_DAYS('2026-04-01')),
    PARTITION p202604 VALUES LESS THAN (TO_DAYS('2026-05-01')),
    PARTITION p202605 VALUES LESS THAN (TO_DAYS('2026-06-01')),
    PARTITION p_future VALUES LESS THAN MAXVALUE
);

-- 动态添加分区
ALTER TABLE access_logs REORGANIZE PARTITION p_future INTO (
    PARTITION p202606 VALUES LESS THAN (TO_DAYS('2026-07-01')),
    PARTITION p_future VALUES LESS THAN MAXVALUE
);

-- 删除旧分区(比 DELETE 快得多)
ALTER TABLE access_logs DROP PARTITION p202601;

9.5.4 分区表的限制

限制说明
最大分区数8192
分区键必须是主键/唯一索引的一部分
外键不支持
全文索引不支持
空间索引不支持

⚠️ 分区表 vs 分表

  • 分区表:逻辑上是一张表,物理上分区存储,MySQL 自动管理路由
  • 分表:手动拆分为多张独立表,需要应用层或中间件路由

9.6 字符集与排序规则

9.6.1 字符集选择

-- 推荐:utf8mb4(支持完整的 Unicode,包括 emoji)
CREATE TABLE my_table (
    content VARCHAR(100)
) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
字符集最大字节说明
latin11西欧字符
gbk2中文简体
utf8mb33MySQL 的 “utf8”,不支持 4 字节字符
utf8mb44真正的 UTF-8,推荐

⚠️ utf8 ≠ UTF-8:MySQL 的 utf8 实际上是 utf8mb3,最多 3 字节,不支持 emoji(需要 4 字节)。必须使用 utf8mb4

9.6.2 排序规则

排序规则说明
utf8mb4_general_ci旧版通用排序,速度快但不精确
utf8mb4_unicode_ciUnicode 标准排序,精确但较慢
utf8mb4_0900_ai_ciMySQL 8.0 默认,基于 Unicode 9.0,推荐
utf8mb4_bin二进制比较,区分大小写
-- 查看当前字符集设置
SHOW VARIABLES LIKE 'character_set%';
SHOW VARIABLES LIKE 'collation%';

-- 修改数据库字符集
ALTER DATABASE myapp CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;

-- 修改表字符集
ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;

9.7 完整数据库设计示例

-- ===================== 电商系统核心表设计 =====================

CREATE DATABASE IF NOT EXISTS ecommerce
    DEFAULT CHARACTER SET utf8mb4
    COLLATE utf8mb4_0900_ai_ci;

USE ecommerce;

-- 用户表
CREATE TABLE users (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID',
    username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
    email VARCHAR(100) NOT NULL UNIQUE COMMENT '邮箱',
    phone VARCHAR(20) NOT NULL DEFAULT '' COMMENT '手机号',
    password_hash VARCHAR(255) NOT NULL COMMENT '密码哈希',
    nickname VARCHAR(50) DEFAULT '' COMMENT '昵称',
    avatar_url VARCHAR(500) DEFAULT '' COMMENT '头像',
    gender TINYINT UNSIGNED DEFAULT 0 COMMENT '性别:0=未知 1=男 2=女',
    balance DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '余额',
    status TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '状态:1=正常 0=禁用',
    last_login_at DATETIME DEFAULT NULL COMMENT '最后登录时间',
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    deleted_at DATETIME DEFAULT NULL,
    INDEX idx_phone (phone),
    INDEX idx_status (status)
) ENGINE=InnoDB COMMENT='用户表';

-- 商品表
CREATE TABLE products (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '商品ID',
    name VARCHAR(200) NOT NULL COMMENT '商品名称',
    description TEXT DEFAULT NULL COMMENT '商品描述',
    price DECIMAL(10,2) NOT NULL COMMENT '售价',
    cost_price DECIMAL(10,2) DEFAULT NULL COMMENT '成本价',
    category_id INT UNSIGNED NOT NULL COMMENT '分类ID',
    stock INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '库存',
    sales INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '销量',
    status TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '状态:1=上架 0=下架',
    main_image VARCHAR(500) DEFAULT '' COMMENT '主图',
    attributes JSON DEFAULT NULL COMMENT '商品属性',
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    deleted_at DATETIME DEFAULT NULL,
    INDEX idx_category (category_id),
    INDEX idx_status_sales (status, sales)
) ENGINE=InnoDB COMMENT='商品表';

-- 订单表
CREATE TABLE orders (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '订单ID',
    order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '订单号',
    user_id BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
    total_amount DECIMAL(12,2) NOT NULL COMMENT '订单总金额',
    pay_amount DECIMAL(12,2) NOT NULL COMMENT '实付金额',
    status TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '状态:0=待支付 1=已支付 2=已发货 3=已完成 4=已取消 5=已退款',
    pay_type TINYINT UNSIGNED DEFAULT NULL COMMENT '支付方式',
    pay_time DATETIME DEFAULT NULL COMMENT '支付时间',
    receiver_name VARCHAR(50) DEFAULT '' COMMENT '收货人',
    receiver_phone VARCHAR(20) DEFAULT '' COMMENT '收货电话',
    receiver_address VARCHAR(500) DEFAULT '' COMMENT '收货地址',
    remark VARCHAR(500) DEFAULT '' COMMENT '备注',
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    deleted_at DATETIME DEFAULT NULL,
    INDEX idx_user_id (user_id),
    INDEX idx_status (status),
    INDEX idx_created_at (created_at),
    INDEX idx_user_status (user_id, status)
) ENGINE=InnoDB COMMENT='订单表';

业务场景

场景 1:社交系统的关注关系设计

-- 方案 A:邻接表(最常用)
CREATE TABLE follows (
    follower_id BIGINT UNSIGNED NOT NULL COMMENT '关注者ID',
    following_id BIGINT UNSIGNED NOT NULL COMMENT '被关注者ID',
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (follower_id, following_id),
    INDEX idx_following (following_id)
) ENGINE=InnoDB COMMENT='关注关系表';

-- 查询 A 关注了谁
SELECT following_id FROM follows WHERE follower_id = 1;
-- 查询谁关注了 A
SELECT follower_id FROM follows WHERE following_id = 1;

扩展阅读