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

252 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [MySQL, DDL, ALTER TABLE, CREATE TABLE, DATATYPE, NULL, ONLINE DDL]
create time: 2026-05-16 00:00
---
# DDL — 建表与结构变更
## 概述
DDL(Data Definition Language)用于定义和修改数据库对象的结构。本章聚焦最常用的 `CREATE TABLE`、`ALTER TABLE`,以及 MySQL 8.0 引入的在线 DDL 特性。
## CREATE TABLE
```sql
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)` 反而更高效——因为固定长度无需额外存储长度前缀。
---
### 数据类型选择指南
建表时数据类型直接决定磁盘占用和查询性能。以下是常见场景的经验推荐:
```mermaid
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` 存储财务数据 |
#### 一个常见的反例
```sql
-- ❌ 错误:用 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 只改属性不更名。这是初学最容易混淆的地方。
```sql
-- 添加列
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,允许在结构变更期间持续处理读写请求。
```sql
-- 三种算法对比
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` 看预估耗时。
```mermaid
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` 时:
```bash
# ❌ 危险:直接在线上执行
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**。
```mermaid
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 命令帮助你查看当前数据库的「结构全貌」,在排查索引失效、表空间膨胀时尤其有用。
```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] 快速定位慢查询相关的结构问题
> ```sql
> -- 检查表是否存在大量碎片的索引(数据删除后未回收的空间)
> SELECT TABLE_NAME, INDEX_NAME, CARDINALITY
> FROM INFORMATION_SCHEMA.STATISTICS
> WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users'
> ORDER BY CARDINALITY ASC;
>
> -- 低基数(CARDINALITY 接近 0)的索引通常效果不佳,考虑移除
> ```
## 关联笔记
- [[hhs/GORM/02-模型定义]] — GORM AutoMigrate 等价于 DDL 的自动化版本
- [[hhs/GORM/17-迁移工具]] — Schema 迁移管理最佳实践