This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/MySQL/10-DDL 建表与结构变更.md
T
2026-05-17 00:06:11 +08:00

10 KiB
Raw Blame History

tags, create time
tags create time
MySQL
DDL
ALTER TABLE
CREATE TABLE
DATATYPE
NULL
ONLINE DDL
2026-05-16 00:00

DDL — 建表与结构变更

概述

DDL(Data Definition Language)用于定义和修改数据库对象的结构。本章聚焦最常用的 CREATE TABLE、ALTER TABLE,以及 MySQL 8.0 引入的在线 DDL 特性。

CREATE TABLE

CREATE TABLE users (
    id            BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    username      VARCHAR(50)   NOT NULL COMMENT '用户名',
    email         VARCHAR(255)  NOT NULL COMMENT '邮箱唯一',
    password_hash VARCHAR(64)   NOT NULL COMMENT 'bcrypt hash',
    status        TINYINT       NOT NULL DEFAULT 1 COMMENT '1=active 0=disabled',
    created_at    DATETIME      NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at    DATETIME      NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    UNIQUE KEY uk_email (email),
    INDEX idx_username (username),
    INDEX idx_status_created (status, created_at)
) ENGINE=InnoDB
  DEFAULT CHARSET=utf8mb4
  COLLATE=utf8mb4_0900_ai_ci
  COMMENT='用户表';

关键语法解析

子句 作用
ENGINE=InnoDB 显式指定存储引擎(8.0 默认值)
DEFAULT CHARSET 数据库级字符集覆盖
ON UPDATE CURRENT_TIMESTAMP 自动维护更新时间戳
UNIQUE KEY 唯一索引,同时约束数据唯一性
INDEX idx_name (col) 普通索引,辅助查询
COMMENT 表和字段注释,可通过 SHOW FULL COLUMNS 查看

[!TIP] 命名规范约定

  • 索引名:idx_表名_列名(普通索引)、uk_表名_列名(唯一索引)——避免超过 64 字符
  • 时间字段:统一用 DATETIME 而非 TIMESTAMP。后者范围仅限 1970~2038,遇到闰秒时会报错
  • 自增主键:务必用 UNSIGNED,正数区间翻倍(0 ~ 42.9 亿),且能略微节省空间

[!QUESTION] 思考:为什么 password_hash 要用 VARCHAR(64) 而不是固定长度? 答案取决于你使用的哈希算法。bcrypt 输出为 60 字符(如 $2b$12$...),预留 64 留有扩展余量。如果改用 SHA-256 则恰好 64 字节,此时 CHAR(64) 反而更高效——因为固定长度无需额外存储长度前缀。


数据类型选择指南

建表时数据类型直接决定磁盘占用和查询性能。以下是常见场景的经验推荐:

graph TD
    A["需要整数?" -->|"是"| B{"需要 UNSIGNED?"}
    B -->|"否"| C["INT — 覆盖 -21 万 ~ 21 万"]
    B -->|"是"| D["BIGINT UNSIGNED — 最大 1844 亿"]

    A -->|"否"| E["字符串?"]
    E -->|"变长 < 255"| F["VARCHAR(N)"]
    E -->|"定长 (密码/签名)"| G["CHAR(N)"]
    E -->|"超长 (文章/JSON)"| H["TEXT / LONGTEXT"]
类型家族 适用场景 避坑提示
TINYINT 状态标记、布尔值 建议加 UNSIGNED,0~255 足够
INT 计数器、外键引用 数字型 ID 优先 INT UNSIGNED,超 42 亿再用 BIGINT
VARCHAR(N) 用户名、邮箱、地址 N 按实际 +20% 余量;不要盲目设 255
DATETIME 所有时间戳 MySQL 8.0 推荐;避免 TIMESTAMP 的 2038 年瓶颈
DECIMAL(M,D) 金额 绝对禁止用 FLOAT/DOUBLE 存储财务数据

一个常见的反例

-- ❌ 错误:用 FLOAT 存价格,会出现精度丢失
price       FLOAT           DEFAULT 0.00,

-- ✅ 正确:DECIMAL 精确表示货币,M 总位数 D 小数位
price       DECIMAL(10, 2)  DEFAULT 0.00 COMMENT '单位:元',

[!NOTE] DECIMAL(10,2) 表示最多 10 位数字,其中 2 位在小数点后,即最大值为 99999999.99。在 InnoDB 内部以二进制紧凑存储,性能接近整数类型。

ALTER TABLE — 常见操作

增删改列

CHANGE 与 MODIFY 的区别在于:CHANGE 必须同时写出列名(可改名),而 MODIFY 只改属性不更名。这是初学最容易混淆的地方。

-- 添加列
ALTER TABLE users ADD COLUMN phone VARCHAR(20) AFTER email;
ALTER TABLE users ADD COLUMN last_login_ip VARCHAR(45); -- IPv4/IPv6 通用

-- 修改列类型(不改名)
ALTER TABLE users MODIFY COLUMN status TINYINT UNSIGNED DEFAULT 0;

-- 重命名列(改名 + 可选改类型)
ALTER TABLE users CHANGE COLUMN phone phone_number VARCHAR(20);

-- 删除列
ALTER TABLE users DROP COLUMN last_login_ip;

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

[!WARNING] ALTER TABLE DROP COLUMN 不可逆 列一旦删除,其中的数据和统计信息全部消失。生产环境执行 DDL 前务必确认已有备份。现代运维流程中应通过迁移工具(见关联笔记)做版本化管理,而非手动执行 ALTER。

在线 DDL(ALGORITHM / LOCK)

MySQL 5.6+ 支持 Online DDL,允许在结构变更期间持续处理读写请求。

-- 三种算法对比
ALTER TABLE users ADD COLUMN bio TEXT, ALGORITHM=INPLACE;
ALTER TABLE users ADD COLUMN bio TEXT, ALGORITHM=COPY;
ALTER TABLE users ADD COLUMN bio TEXT;  -- 8.0 默认 INPLACE

-- 三种锁策略对比
ALTER TABLE ... LOCK=NONE;     -- 不阻塞任何读/写(最安全)
ALTER TABLE ... LOCK=SHARED;   -- 允许并发读,阻塞写
ALTER TABLE ... LOCK=EXCLUSIVE; -- 阻塞所有其他操作(最快)

[!NOTE] INPLACE vs COPY 的本质区别

  • INPLACE:直接在原表上重建索引或新增索引,不需要全表拷贝数据。支持的场景包括:增加/删除索引、改变列默认值、新增列等。
  • COPY:创建临时表 → 逐行拷贝数据 → 原子替换。适用于改变列定义导致格式不兼容的场景,比如 VARCHAR 改 INT。

经验法则:8.0 默认的 INPLACE 对大多数操作都够用。若不确定,先跑 pt-online-schema-change --dry-run 看预估耗时。

flowchart LR
    A["ALTER TABLE 开始"] --> B{"ALGORITHM"}
    B -->|"INPLACE"| C["直接修改数据字典<br/>原地更新索引"]
    B -->|"COPY"| D["创建临时表拷贝数据后替换"]

    C --> E{"LOCK"}
    D --> E

    E -->|"NONE"| F["在线执行<br/>读写不阻塞"]
    E -->|"SHARED"| G["并发读<br/>写入等待"]
    E -->|"EXCLUSIVE"| H["阻塞全部操作<br/>速度最快"]

    style F fill:#00D866,color:#fff
    style G fill:#FF9F43,color:#000
    style H fill:#EE5A24,color:#fff

大表 DDL 实战技巧

对百万行以上的表执行 ALTER TABLE 时:

# ❌ 危险:直接在线上执行
ALTER TABLE big_table ADD INDEX idx_col (col);

# ✅ 方案一:pt-online-schema-change(Percona Toolkit)
pt-online-schema-change \
  --user=root --password=xxx \
  --alter "ADD INDEX idx_col (col)" \
  D=mydb,t=big_table \
  --execute

# ✅ 方案二:gh-ost(GitHub 开源)
gh-ost \
  --user=root --password=xxx \
  --host=127.0.0.1 --port=3306 \
  --database=mydb \
  --table=big_table \
  --alter "ADD INDEX idx_col (col)" \
  --allow-on-master \
  --execute

[!TIP] pt-osc 原理三步走

  1. 创建新表:与原表结构一致 + 目标变更
  2. 触发器同步:创建 INSERT/UPDATE/DELETE 触发器,将原表的变更实时同步到新表
  3. 原子替换:加排他锁极短时间,swap 两张表名后删除触发器和旧表

[!QUESTION] gh-ost 和 pt-osc 怎么选?

  • pt-osc 依赖触发器,对高并发写密集型表有较大开销
  • gh-ost 基于 binlog 解析,无需触发器,对线上影响更小
  • 结论:新项目优先 gh-ost;老项目已有 pt-toolkit 积累也可以用 pt-osc

NULL vs NOT NULL — 选型指南

这是 DDL 中最常见的争论之一。核心原则:能用 NOT NULL 就不用 NULL。

graph TD
    A["是否允许未知状态" -->|"否"| B["NOT NULL + DEFAULT"]
    A -->|"是"| C{"业务语义?"}
    C -->|"逻辑删除/软删"| D["TINYINT DEFAULT 0<br/>0=正常 1=已删除"]
    C -->|" truly optional "| E["允许 NULL<br/>但加注释说明含义"]
维度 NOT NULL + DEFAULT 允许 NULL
索引效率 InnoDB 二级索引不存储纯 NULL 值,用默认值占位可减少索引碎片 包含 NULL 标记的索引需要额外 1 bit
查询安全 WHERE col = ? 不会遗漏任何行 WHERE col = 'value' 排除了 NULL 行(需用 IS NULL)
聚合函数 SUM/COUNT 直接可用 COUNT(col) 忽略 NULL 行,容易误判总行数
可读性 status = 0 一目了然 status IS NULL 语义模糊——到底是"还没填"还是"被清除了"?

[!WARNING] NULL 的三个常见误区

  1. "NULL 占空间更小":InnoDB 中 NULL 也需要在行格式中标记,固定为每 8 个字节 1 bit 的 null bitmap。比存一个空字符串或零值并不节省什么。
  2. "NULL = NULL":在 SQL 三值逻辑中,NULL = NULL 结果为 UNKNOWN,永远不等于 TRUE。判断必须用 IS NULL / IS NOT NULL。
  3. "外键可以为 NULL":技术上可以,但会导致孤儿记录难以追踪。建议用显式的 deleted_at 时间戳代替外键 NULL 做软删除。

常用 DDL 查询

这些 SQL 命令帮助你查看当前数据库的「结构全貌」,在排查索引失效、表空间膨胀时尤其有用。

-- 查看表完整创建语句(含所有索引、注释、引擎配置)
SHOW CREATE TABLE users\G

-- 查看所有索引(含索引类型、列顺序、唯一性)
SHOW INDEX FROM users\G

-- 查看表统计信息(Rows 为估算值,非精确计数)
SHOW TABLE STATUS LIKE 'users'\G
-- 重点关注 Rows(估算行数)、Data_length、Index_length

-- 查看分区情况
SELECT PARTITION_NAME, PARTITION_EXPRESSION, PARTITION_DESCRIPTION
FROM INFORMATION_SCHEMA.PARTITIONS WHERE TABLE_NAME = 'logs';

[!TIP] 快速定位慢查询相关的结构问题

-- 检查表是否存在大量碎片的索引(数据删除后未回收的空间)
SELECT TABLE_NAME, INDEX_NAME, CARDINALITY
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users'
ORDER BY CARDINALITY ASC;

-- 低基数(CARDINALITY 接近 0)的索引通常效果不佳,考虑移除

关联笔记