← 返回目录

第四章:表设计

数据类型、约束与表结构管理

1 数据类型

选择正确的数据类型是表设计的基础。合适的类型不仅节省存储空间,还能提升查询性能并保证数据完整性。MySQL 提供了丰富的数据类型,以下按类别逐一介绍。

整数类型

类型 存储大小 有符号范围 无符号范围 典型用途
TINYINT1 字节-128 ~ 1270 ~ 255状态码、布尔值
SMALLINT2 字节-32768 ~ 327670 ~ 65535年龄、端口号
INT4 字节-21 亿 ~ 21 亿0 ~ 42 亿主键、计数
BIGINT8 字节-9.2×1018 ~ 9.2×10180 ~ 1.8×1019大数据量主键、雪花 ID
-- 整数类型示例
CREATE TABLE example_int (
    status    TINYINT UNSIGNED NOT NULL DEFAULT 0,
    age       SMALLINT UNSIGNED,
    user_id   INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    snowflake BIGINT UNSIGNED NOT NULL
);

浮点数与定点数

类型 存储大小 精度 适用场景
FLOAT4 字节约 7 位有效数字科学计算、不要求精确的场景
DOUBLE8 字节约 15 位有效数字科学计算、统计数据
DECIMAL(M,D)M+2 字节精确到 D 位小数金融数据、价格、金额
-- 金额必须用 DECIMAL,避免浮点精度问题
CREATE TABLE products (
    id    INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name  VARCHAR(200) NOT NULL,
    price DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
    weight FLOAT
);

-- FLOAT 精度问题演示
SELECT 0.1 + 0.2;           -- 结果: 0.30000000000000004
SELECT CAST(0.1 AS DECIMAL(10,2)) + CAST(0.2 AS DECIMAL(10,2)); -- 结果: 0.30

字符串类型

类型 最大长度 特点 适用场景
CHAR(N)255 字符固定长度,不足右补空格MD5 哈希、国家代码
VARCHAR(N)65535 字节可变长度,按实际存储用户名、邮箱、标题
TEXT64 KB大文本,不能设默认值文章内容、评论
MEDIUMTEXT16 MB中等大文本长文章、日志
LONGTEXT4 GB超大文本电子书、超大 JSON

⚡ VARCHAR vs CHAR

  • CHAR(32) — 始终占用 32 字节,比较时无需计算长度,适合固定长度字段(如 MD5、UUID)
  • VARCHAR(255) — 按实际内容长度 + 1~2 字节存储,节省空间,适合长度不固定的字段
  • UTF-8 编码下 VARCHAR(N) 的 N 是字符数,实际存储字节数取决于内容(中文每字 3 字节)

日期时间类型

类型 格式 范围 存储
DATEYYYY-MM-DD1000-01-01 ~ 9999-12-313 字节
TIMEHH:MM:SS-838:59:59 ~ 838:59:593 字节
DATETIMEYYYY-MM-DD HH:MM:SS1000-01-01 ~ 9999-12-318 字节
TIMESTAMPYYYY-MM-DD HH:MM:SS1970-01-01 ~ 2038-01-194 字节
CREATE TABLE events (
    id         INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    title      VARCHAR(200) NOT NULL,
    event_date DATE NOT NULL,
    start_time TIME,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- TIMESTAMP 会自动转换时区,DATETIME 不会
-- 推荐:业务时间用 DATETIME,记录操作时间用 TIMESTAMP

其他常用类型

-- BOOLEAN(实际是 TINYINT(1) 的别名)
ALTER TABLE users ADD COLUMN is_active BOOLEAN DEFAULT TRUE;
-- TRUE = 1, FALSE = 0

-- ENUM:枚举类型,值只能是预定义的几个之一
CREATE TABLE articles (
    id     INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    title  VARCHAR(200) NOT NULL,
    status ENUM('draft', 'published', 'archived') DEFAULT 'draft'
);

-- SET:可以存储多个预定义值的组合
CREATE TABLE user_preferences (
    user_id INT UNSIGNED PRIMARY KEY,
    hobbies SET('reading', 'gaming', 'cooking', 'sports', 'music')
);
INSERT INTO user_preferences VALUES (1, 'reading,gaming,music');

-- JSON(MySQL 5.7.8+)
CREATE TABLE configs (
    id       INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    settings JSON NOT NULL
);
INSERT INTO configs (settings) VALUES ('{"theme": "dark", "lang": "zh"}');
SELECT settings->>'$.theme' AS theme FROM configs;

-- BLOB:二进制大对象(通常建议存文件路径而非文件本身)
CREATE TABLE attachments (
    id   INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    data MEDIUMBLOB,
    mime VARCHAR(100)
);

🔄 MySQL vs PostgreSQL 数据类型差异

  • 自增主键:MySQL 用 INT AUTO_INCREMENT,PostgreSQL 用 SERIALGENERATED ALWAYS AS IDENTITY
  • 布尔类型:MySQL 的 BOOLEAN 本质是 TINYINT(1),PostgreSQL 有原生 BOOLEAN 类型(true/false)
  • JSON:MySQL 有 JSON 类型;PostgreSQL 有 JSONJSONB(二进制存储,支持索引,性能更好)
  • 数组:MySQL 不支持数组类型;PostgreSQL 支持 INT[]TEXT[] 等原生数组
  • ENUM:MySQL 的 ENUM 定义在列上;PostgreSQL 需先 CREATE TYPE 再引用

2 约束

约束(Constraints)是数据库层面的数据完整性保障。即使应用层有校验,数据库约束是最后一道防线,确保无论数据从哪个入口写入都符合规则。

PRIMARY KEY — 主键

-- 单列主键
CREATE TABLE users (
    id    INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name  VARCHAR(100) NOT NULL
);

-- 复合主键(联合主键)
CREATE TABLE order_items (
    order_id   INT UNSIGNED NOT NULL,
    product_id INT UNSIGNED NOT NULL,
    quantity   SMALLINT UNSIGNED NOT NULL DEFAULT 1,
    price      DECIMAL(10, 2) NOT NULL,
    PRIMARY KEY (order_id, product_id)
);

NOT NULL — 非空约束

CREATE TABLE contacts (
    id    INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name  VARCHAR(100) NOT NULL,       -- 必填
    email VARCHAR(200) NOT NULL,       -- 必填
    phone VARCHAR(20)                  -- 可选,允许 NULL
);

-- 插入时省略 NOT NULL 字段会报错
INSERT INTO contacts (id) VALUES (1);
-- ERROR 1364: Field 'name' doesn't have a default value

UNIQUE — 唯一约束

CREATE TABLE users (
    id       INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email    VARCHAR(200) NOT NULL,
    phone    VARCHAR(20),
    UNIQUE KEY uk_email (email),
    UNIQUE KEY uk_phone (phone)
);

-- UNIQUE 列允许多个 NULL(MySQL 中 NULL != NULL)
-- 复合唯一约束
CREATE TABLE enrollments (
    student_id INT UNSIGNED NOT NULL,
    course_id  INT UNSIGNED NOT NULL,
    enrolled_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_student_course (student_id, course_id)
);

DEFAULT — 默认值

CREATE TABLE posts (
    id         INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    title      VARCHAR(200) NOT NULL,
    content    TEXT,
    status     TINYINT UNSIGNED NOT NULL DEFAULT 0,
    view_count INT UNSIGNED NOT NULL DEFAULT 0,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CHECK — 检查约束(MySQL 8.0.16+)

CREATE TABLE products (
    id       INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name     VARCHAR(200) NOT NULL,
    price    DECIMAL(10, 2) NOT NULL,
    stock    INT NOT NULL DEFAULT 0,
    discount DECIMAL(3, 2) DEFAULT 1.00,
    CONSTRAINT chk_price    CHECK (price >= 0),
    CONSTRAINT chk_stock    CHECK (stock >= 0),
    CONSTRAINT chk_discount CHECK (discount BETWEEN 0.00 AND 1.00)
);

-- 违反 CHECK 约束会报错
INSERT INTO products (name, price, stock) VALUES ('测试', -10, 5);
-- ERROR 3819: Check constraint 'chk_price' is violated

FOREIGN KEY — 外键约束

CREATE TABLE users (
    id   INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL
) ENGINE=InnoDB;

CREATE TABLE orders (
    id         INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id    INT UNSIGNED NOT NULL,
    total      DECIMAL(12, 2) NOT NULL DEFAULT 0.00,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,

    CONSTRAINT fk_orders_user
        FOREIGN KEY (user_id) REFERENCES users(id)
        ON DELETE RESTRICT
        ON UPDATE CASCADE
) ENGINE=InnoDB;

-- ON DELETE 选项:
-- RESTRICT  — 有子记录时禁止删除父记录(默认)
-- CASCADE   — 删除父记录时同步删除所有子记录
-- SET NULL  — 删除父记录时将子记录的外键列设为 NULL
-- NO ACTION — 与 RESTRICT 等价(SQL 标准写法)

-- ON UPDATE 选项同上,控制父记录主键更新时的行为
-- 实际场景示例:用户删除后订单保留但 user_id 设为 NULL
CREATE TABLE orders_v2 (
    id         INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id    INT UNSIGNED,
    total      DECIMAL(12, 2) NOT NULL DEFAULT 0.00,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,

    CONSTRAINT fk_orders_v2_user
        FOREIGN KEY (user_id) REFERENCES users(id)
        ON DELETE SET NULL
        ON UPDATE CASCADE
) ENGINE=InnoDB;

3 CREATE TABLE 完整示例

下面设计一个简化的电商数据库,包含商品表、订单表和订单明细表,展示如何综合使用各种数据类型和约束。

商品表 products

CREATE TABLE products (
    id          INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    sku         VARCHAR(50) NOT NULL UNIQUE COMMENT '商品编号',
    name        VARCHAR(200) NOT NULL COMMENT '商品名称',
    description TEXT COMMENT '商品描述',
    price       DECIMAL(10, 2) NOT NULL COMMENT '单价',
    stock       INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '库存数量',
    category    ENUM('electronics', 'clothing', 'food', 'books', 'other') DEFAULT 'other',
    is_active   BOOLEAN NOT NULL DEFAULT TRUE,
    created_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    CONSTRAINT chk_products_price CHECK (price >= 0),
    INDEX idx_category (category),
    INDEX idx_active_category (is_active, category)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商品表';

订单表 orders

CREATE TABLE orders (
    id           INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    order_no     VARCHAR(32) NOT NULL UNIQUE COMMENT '订单号',
    user_id      INT UNSIGNED NOT NULL COMMENT '用户 ID',
    total_amount DECIMAL(12, 2) NOT NULL DEFAULT 0.00,
    status       ENUM('pending', 'paid', 'shipped', 'completed', 'cancelled') NOT NULL DEFAULT 'pending',
    address      VARCHAR(500) NOT NULL COMMENT '收货地址',
    remark       VARCHAR(500) DEFAULT '' COMMENT '备注',
    created_at   DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at   TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES users(id)
        ON DELETE RESTRICT ON UPDATE CASCADE,
    INDEX idx_user_id (user_id),
    INDEX idx_status (status),
    INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单表';

订单明细表 order_items

CREATE TABLE order_items (
    id         INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    order_id   INT UNSIGNED NOT NULL,
    product_id INT UNSIGNED NOT NULL,
    quantity   SMALLINT UNSIGNED NOT NULL DEFAULT 1,
    unit_price DECIMAL(10, 2) NOT NULL COMMENT '下单时的单价快照',
    subtotal   DECIMAL(12, 2) GENERATED ALWAYS AS (quantity * unit_price) STORED,
    CONSTRAINT fk_items_order FOREIGN KEY (order_id) REFERENCES orders(id)
        ON DELETE CASCADE ON UPDATE CASCADE,
    CONSTRAINT fk_items_product FOREIGN KEY (product_id) REFERENCES products(id)
        ON DELETE RESTRICT ON UPDATE CASCADE,
    CONSTRAINT chk_quantity CHECK (quantity > 0),
    INDEX idx_order_id (order_id),
    INDEX idx_product_id (product_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表';

查看建表语句

-- 查看完整的建表 DDL
SHOW CREATE TABLE products\G

-- 查看表结构摘要
DESCRIBE products;
-- 或等价写法
DESC products;
SHOW COLUMNS FROM products;

4 ALTER TABLE 修改表结构

数据库设计不是一成不变的,业务演进常常需要修改表结构。ALTER TABLE 是最常用的 DDL 语句之一。

ADD COLUMN — 添加列

-- 在末尾添加列
ALTER TABLE users ADD COLUMN avatar_url VARCHAR(500) DEFAULT NULL;

-- 在指定列之后添加
ALTER TABLE users ADD COLUMN nickname VARCHAR(50) AFTER name;

-- 在最前面添加
ALTER TABLE users ADD COLUMN uuid CHAR(36) FIRST;

-- 一次添加多列
ALTER TABLE users
    ADD COLUMN bio TEXT,
    ADD COLUMN birthday DATE;

MODIFY COLUMN / CHANGE COLUMN — 修改列

-- MODIFY:修改列的类型或属性(不改名)
ALTER TABLE users MODIFY COLUMN name VARCHAR(200) NOT NULL;

-- CHANGE:同时改列名和属性
ALTER TABLE users CHANGE COLUMN nickname display_name VARCHAR(100) DEFAULT '';

-- 修改默认值
ALTER TABLE products ALTER COLUMN stock SET DEFAULT 0;
ALTER TABLE products ALTER COLUMN category DROP DEFAULT;

DROP COLUMN — 删除列

ALTER TABLE users DROP COLUMN bio;

-- 删除多列
ALTER TABLE users
    DROP COLUMN avatar_url,
    DROP COLUMN birthday;

ADD INDEX — 添加索引

-- 普通索引
ALTER TABLE users ADD INDEX idx_email (email);

-- 唯一索引
ALTER TABLE users ADD UNIQUE INDEX uk_phone (phone);

-- 复合索引
ALTER TABLE orders ADD INDEX idx_user_status (user_id, status);

-- 删除索引
ALTER TABLE users DROP INDEX idx_email;

ADD / DROP FOREIGN KEY

-- 添加外键
ALTER TABLE orders
    ADD CONSTRAINT fk_orders_user
    FOREIGN KEY (user_id) REFERENCES users(id)
    ON DELETE RESTRICT;

-- 删除外键(需要知道约束名)
ALTER TABLE orders DROP FOREIGN KEY fk_orders_user;

RENAME TABLE — 重命名表

-- 重命名单个表
RENAME TABLE users TO members;

-- 重命名多个表(原子操作,常用于安全替换表)
RENAME TABLE
    products TO products_old,
    products_new TO products;

🚨 大表 ALTER 注意事项

  • MySQL 的大部分 ALTER TABLE 操作会复制整张表并重建,百万级以上的表可能耗时数分钟到数小时
  • 在此期间表可能被锁定,导致读写阻塞——生产环境务必在低峰期执行
  • 推荐使用 pt-online-schema-change(Percona Toolkit)或 gh-ost(GitHub)做在线 DDL
  • MySQL 8.0 的 INSTANT 算法支持部分操作免重建:ALTER TABLE t ADD COLUMN c INT, ALGORITHM=INSTANT

5 设计规范建议

命名规范

-- ✅ 推荐:snake_case 小写命名
CREATE TABLE user_addresses (
    id           INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id      INT UNSIGNED NOT NULL,
    address_line VARCHAR(300) NOT NULL,
    postal_code  VARCHAR(20)
);

-- ❌ 避免:驼峰命名、大写、保留字作表名/列名
-- CREATE TABLE UserAddresses (ID INT, AddressLine VARCHAR(300));
-- CREATE TABLE `order` (`select` VARCHAR(100));  -- 虽然可以用反引号,但不推荐

必备设计原则

始终设置主键

每张表都应有主键。推荐自增 INT 或 BIGINT。无主键的表在 InnoDB 中性能较差且不利于主从复制。

选择合适的数据类型

不要所有字段都用 VARCHAR(255)。年龄用 TINYINT,金额用 DECIMAL,状态用 ENUM 或 TINYINT。

尽量 NOT NULL + DEFAULT

NULL 增加索引和查询的复杂性。能给默认值就给默认值,只有真正"未知"的字段才允许 NULL。

统一字符集

使用 utf8mb4 + utf8mb4_unicode_ci。不要用 utf8(MySQL 中只支持 3 字节,不能存 emoji)。

数据库范式(简要)

第一范式(1NF): 每列都是原子值,不可再分。例如"地址"不应存为一个大字段,应拆分为省、市、区、详细地址。
第二范式(2NF): 满足 1NF + 非主键列完全依赖主键。联合主键表中,非主键列不能只依赖部分主键。
第三范式(3NF): 满足 2NF + 非主键列之间不能有传递依赖。例如订单表不应存用户名——用户名依赖 user_id,应通过 JOIN 获取。
-- ❌ 违反 3NF:订单表中冗余了用户名
CREATE TABLE bad_orders (
    id         INT UNSIGNED PRIMARY KEY,
    user_id    INT UNSIGNED NOT NULL,
    user_name  VARCHAR(100),  -- 冗余!应从 users 表 JOIN 获取
    total      DECIMAL(10, 2)
);

-- ✅ 正确:通过外键关联
CREATE TABLE good_orders (
    id      INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id INT UNSIGNED NOT NULL,
    total   DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
    FOREIGN KEY (user_id) REFERENCES users(id)
);
-- 查询时 JOIN 获取用户名
SELECT o.id, u.name, o.total
FROM good_orders o
JOIN users u ON o.user_id = u.id;

💡 反范式的合理场景

范式不是教条。在高性能读场景中,适当冗余可以避免 JOIN,例如订单明细中保存"下单时的商品价格"(价格快照)而非实时关联,因为商品价格可能会变。

6 本章要点

📦 数据类型选择

整数选合适宽度,金额用 DECIMAL,字符串区分 CHAR/VARCHAR/TEXT,时间区分 DATETIME/TIMESTAMP

🔒 约束保护数据

PRIMARY KEY 唯一标识,NOT NULL 非空,UNIQUE 唯一,CHECK 范围校验,FOREIGN KEY 引用完整性

🏗️ CREATE TABLE

综合运用数据类型、约束、索引和注释,使用 SHOW CREATE TABLE 查看完整定义

🔧 ALTER TABLE

ADD/MODIFY/DROP COLUMN,ADD INDEX,ADD FOREIGN KEY。大表操作注意锁表风险

📐 设计规范

snake_case 命名,始终有主键,选择合适类型,尽量 NOT NULL + DEFAULT,统一 utf8mb4

📊 数据库范式

1NF 原子性、2NF 完全依赖、3NF 无传递依赖。适度反范式可提升读性能

下一章预告:第五章将讲解连接查询——INNER JOIN、LEFT JOIN、子查询与 UNION。掌握了表设计后,学习如何高效地从多张关联表中查询数据。