DDL(Data Definition Language,数据定义语言)用于定义和修改数据库结构,常见命令包括 CREATE、ALTER、DROP、TRUNCATE 和 RENAME。
本文以 MySQL 8.0 为准,示例围绕电商中的用户、商品和订单场景展开。
DDL 经常会获得元数据锁,并且许多 DDL 会隐式提交事务。生产环境执行前应确认影响范围、表大小、锁表风险和回滚方案。
CREATE DATABASE IF NOT EXISTS shop
CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
utf8mb4 可以完整保存中文、Emoji 等 Unicode 字符。utf8mb4_0900_ai_ci 是 MySQL 8.0 常用排序规则。ci 表示比较时不区分大小写。SHOW DATABASES;
USE shop;
SELECT DATABASE();
SHOW CREATE DATABASE shop;
DROP DATABASE shop;
DROP DATABASE 会删除数据库及其中全部对象,通常无法通过普通 SQL 回滚。生产环境不要把它当作清理数据的命令。
中文项目推荐统一使用 utf8mb4。MySQL 中字符集决定“字符如何编码保存”,排序规则(Collation)决定“字符串如何比较和排序”。
utf8mb4CREATE DATABASE shop
CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
utf8 最多只支持 3 字节 UTF-8,不能完整保存部分 Emoji 和扩展 Unicode 字符。utf8mb4 支持完整的 UTF-8,是新项目的推荐选择。utf8mb4 字段中。不要为了“只保存中文”选择 gbk。现代 Web、Node.js、JSON 和跨系统接口通常都以 UTF-8 为基础,统一使用 utf8mb4 更容易避免转换和乱码问题。
| 排序规则 | 比较特点 | 适用场景 |
|---|---|---|
utf8mb4_0900_ai_ci |
不区分大小写、忽略重音 | 普通用户名、标题、中文业务文本 |
utf8mb4_0900_as_cs |
区分大小写和重音 | 需要严格区分英文大小写的文本 |
utf8mb4_zh_0900_as_cs |
中文语言规则,区分大小写和重音 | 希望按中文语言规则比较、排序 |
utf8mb4_bin |
按二进制值比较 | Token、哈希值、严格区分字符的编码 |
后缀含义:
ai:accent-insensitive,忽略重音。as:accent-sensitive,区分重音。ci:case-insensitive,不区分大小写。cs:case-sensitive,区分大小写。bin:按二进制值比较。普通中文业务表通常可以使用:
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci
但以下字段可能需要严格比较:
api_token VARCHAR(128)
CHARACTER SET ascii
COLLATE ascii_bin
NOT NULL,
external_code VARCHAR(64)
CHARACTER SET utf8mb4
COLLATE utf8mb4_bin
NOT NULL
安全 Token 如果本身只包含 ASCII 字符,使用 ascii 可以更准确地表达数据范围;ascii_bin 会严格区分大小写。
默认的 utf8mb4_0900_ai_ci 能很好地完成普通 Unicode 比较,但产品要求“按汉语拼音排序”时,不应想当然地认为任何 Unicode 排序规则都等同于业务需要的拼音顺序。
可以先测试中文专用排序规则:
SELECT username
FROM users
ORDER BY username COLLATE utf8mb4_zh_0900_as_cs;
如果产品对多音字、自定义称谓或精确拼音顺序有明确要求,更稳定的方式是增加专门的排序字段:
ALTER TABLE users
ADD COLUMN username_pinyin VARCHAR(200) NULL,
ADD INDEX idx_users_username_pinyin (username_pinyin);
由应用层按统一规则生成 username_pinyin,再按它排序:
SELECT id, username
FROM users
ORDER BY username_pinyin, id;
字符集可以设置在服务器、数据库、表和字段等层级。字段没有显式设置时,会继承表的默认值;表再继承数据库默认值。
查看当前设置:
SHOW VARIABLES LIKE 'character_set%';
SHOW VARIABLES LIKE 'collation%';
SHOW CREATE DATABASE shop;
SHOW CREATE TABLE users;
SHOW FULL COLUMNS FROM users;
创建表时明确指定:
CREATE TABLE articles (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
title VARCHAR(200) NOT NULL,
content TEXT NOT NULL,
PRIMARY KEY (id)
) ENGINE = InnoDB
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
只覆盖一个字段:
CREATE TABLE access_keys (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
key_value VARCHAR(128)
CHARACTER SET ascii
COLLATE ascii_bin
NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uk_access_keys_key_value (key_value)
) ENGINE = InnoDB
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
即使表是 utf8mb4,客户端与 MySQL 连接使用了错误字符集,中文仍可能乱码。
查看当前连接:
SELECT
@@character_set_client,
@@character_set_connection,
@@character_set_results,
@@collation_connection;
当前会话统一为 utf8mb4:
SET NAMES utf8mb4 COLLATE utf8mb4_0900_ai_ci;
SET NAMES 只影响当前连接。实际 Node.js 项目应在数据库驱动或连接池中统一配置,并保证源码文件、HTTP JSON 和数据库连接都使用 UTF-8。
修改表默认值只影响以后新增的字符字段:
ALTER TABLE users
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
转换现有字符字段和已有数据:
ALTER TABLE users
CONVERT TO CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
CONVERT TO CHARACTER SET 可能重建大表,并改变字段能容纳的字节数。生产环境执行前要备份、评估锁表时间,并先在测试环境验证。
修改单个字段:
ALTER TABLE users
MODIFY COLUMN username VARCHAR(50)
CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci
NOT NULL;
使用 MODIFY COLUMN 时要把 NOT NULL、默认值和注释等完整定义重新写出。
在不区分大小写的排序规则下,以下邮箱通常会被认为相同:
User@example.com
user@example.com
因此:
email VARCHAR(255)
CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci
NOT NULL,
UNIQUE KEY uk_users_email (email)
会阻止只在大小写上不同的重复邮箱,这通常符合登录业务预期。应用层仍建议在写入前统一规范化,例如去除首尾空格并把邮箱转换为小写。
而区分大小写的编码可以使用二进制排序规则:
code VARCHAR(64)
CHARACTER SET ascii
COLLATE ascii_bin
NOT NULL,
UNIQUE KEY uk_codes_code (code)
字符集正确只能保证中文能正常保存和比较,不代表普通索引可以完成中文分词搜索。
SELECT id, title
FROM articles
WHERE title LIKE '%数据库%';
这种前置 % 查询通常无法利用普通 B-Tree 索引快速定位。大量中文正文搜索应评估 MySQL FULLTEXT 的分词配置,或使用专门的搜索引擎。业务需要按词语、同义词、拼音或相关度搜索时,不能只依赖字符集设置。
按以下顺序检查:
-- 1. 数据库和表定义
SHOW CREATE DATABASE shop;
SHOW CREATE TABLE users;
-- 2. 字段级定义
SHOW FULL COLUMNS FROM users;
-- 3. 当前连接设置
SELECT
@@character_set_client,
@@character_set_connection,
@@character_set_results;
-- 4. 查看字符及其十六进制字节
SELECT username, HEX(username)
FROM users
WHERE id = 1;
常见原因:
latin1、旧 utf8 或其他字符集。修复前必须先备份。需要先判断是“显示时解码错误”还是“数据库中已经存入错误字节”,不要直接反复执行字符集转换。
选择类型时优先考虑:
| 类型 | 有符号范围 | 无符号范围 | 高频场景 |
|---|---|---|---|
TINYINT |
-128~127 | 0~255 | 布尔状态、小范围枚举、年龄 |
SMALLINT |
-32,768~32,767 | 0~65,535 | 小型计数、较小编号 |
MEDIUMINT |
约 -838 万~838 万 | 0~约 1677 万 | 中等范围计数,使用较少 |
INT |
约 -21 亿~21 亿 | 0~约 42 亿 | 库存、数量、中等规模主键 |
BIGINT |
约 -922 京~922 京 | 0~约 1844 京 | 大型系统主键、流水 ID |
真实场景:
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
age TINYINT UNSIGNED NULL,
stock INT UNSIGNED NOT NULL DEFAULT 0,
sales_count BIGINT UNSIGNED NOT NULL DEFAULT 0
UNSIGNED。INT(11) 中的 11 不是存储位数,只是已经废弃的显示宽度。BIGINT。Node.js 注意事项:MySQL BIGINT 可能超过 JavaScript number 的安全整数范围 2^53 - 1。大型 ID 推荐按字符串读取和传输,避免精度丢失。
金额使用 DECIMAL(M, D):
price DECIMAL(10, 2) NOT NULL,
discount_rate DECIMAL(5, 4) NOT NULL DEFAULT 0.0000,
account_balance DECIMAL(18, 2) NOT NULL DEFAULT 0.00
M 是总位数,D 是小数位数。DECIMAL(10, 2) 最多保存 8 位整数和 2 位小数,例如 99999999.99。FLOAT 或 DOUBLE,它们是近似值类型。FLOAT、DOUBLE 更适合允许误差的测量数据:
temperature DOUBLE NULL,
longitude DECIMAL(10, 7) NULL,
latitude DECIMAL(10, 7) NULL
经纬度如果需要稳定精度,可以使用 DECIMAL;如果进行专业空间查询,应考虑 MySQL 空间数据类型和空间索引。
| 类型 | 特点 | 高频场景 |
|---|---|---|
CHAR(n) |
固定长度 | 国家码、固定编码 |
VARCHAR(n) |
可变长度 | 用户名、邮箱、订单号、标题 |
TEXT |
最多约 64 KB | 文章正文、普通备注 |
MEDIUMTEXT |
最多约 16 MB | 长篇富文本 |
LONGTEXT |
最多约 4 GB | 极大文本,使用前应评估必要性 |
真实场景:
country_code CHAR(2) NOT NULL,
username VARCHAR(50) NOT NULL,
email VARCHAR(255) NOT NULL,
order_no VARCHAR(32) NOT NULL,
product_name VARCHAR(200) NOT NULL,
description TEXT NULL
选型原则:
CHAR。VARCHAR。TEXT。VARCHAR(255) 的 255 通常表示字符数,不是字节数;实际空间与字符集有关。手机号使用字符串的原因:
phone VARCHAR(20) NOT NULL
+。| 类型 | 内容 | 高频场景 |
|---|---|---|
DATE |
日期 | 生日、账单日期 |
TIME |
时间或持续时长 | 营业时间、耗时 |
DATETIME |
日期和时间 | 创建时间、预约时间 |
TIMESTAMP |
按会话时区转换的时间戳 | 更新时间、跨时区时间点 |
YEAR |
年份 | 年度字段,使用较少 |
真实场景:
birthday DATE NULL,
paid_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP
DATETIME 与 TIMESTAMP:
DATETIME 保存写入的日期时间值,不随会话时区自动转换。TIMESTAMP 写入时转换为 UTC,读取时再转换为当前会话时区。Asia/Shanghai 这样的时区名称。项目应统一数据库、Node.js 进程和业务时区规则,避免依赖不同环境的默认设置。
MySQL 的 BOOLEAN、BOOL 实际是 TINYINT(1) 的同义写法:
is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
is_deleted BOOLEAN NOT NULL DEFAULT FALSE
只有两种状态时适合使用布尔值。订单状态有多种取值,可以使用 VARCHAR 配合 CHECK:
status VARCHAR(20) NOT NULL DEFAULT 'pending',
CONSTRAINT chk_orders_status
CHECK (status IN ('pending', 'paid', 'shipped', 'cancelled'))
也可以使用 ENUM:
status ENUM('pending', 'paid', 'shipped', 'cancelled')
NOT NULL DEFAULT 'pending'
ENUM 约束直接,但增加状态需要修改表结构。VARCHAR + CHECK 更直观、迁移更灵活。适合保存结构可能变化的非核心扩展属性:
preferences JSON NULL,
product_attributes JSON NULL
示例值:
{
"theme": "dark",
"language": "zh-CN"
}
邮箱、金额、用户 ID 等核心数据不应塞进 JSON,否则难以建立清晰的约束、索引和关联关系。
经常查询某个 JSON 属性时,可以创建生成列并建立索引:
ALTER TABLE products
ADD COLUMN color VARCHAR(30)
GENERATED ALWAYS AS (
JSON_UNQUOTE(JSON_EXTRACT(attributes, '$.color'))
) STORED,
ADD INDEX idx_products_color (color);
| 类型 | 高频场景 |
|---|---|
BINARY(n) |
固定长度字节值 |
VARBINARY(n) |
可变长度二进制标识、加密结果 |
BLOB |
二进制文件或对象 |
大型图片和视频通常放在对象存储中,数据库保存元数据:
file_url VARCHAR(2048) NOT NULL,
file_size BIGINT UNSIGNED NOT NULL,
mime_type VARCHAR(100) NOT NULL
直接保存大 BLOB 会增加数据库备份、恢复和复制成本。
NULL、默认值和约束username VARCHAR(50) NOT NULL,
age TINYINT UNSIGNED NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending'
NOT NULL。NULL。NULL、数字 0 和空字符串 '' 含义不同。字段选型速查:
| 业务数据 | 推荐类型 |
|---|---|
| 大型自增主键 | BIGINT UNSIGNED |
| 年龄 | TINYINT UNSIGNED |
| 库存 | INT UNSIGNED |
| 金额 | DECIMAL(10, 2) |
| 手机号 | VARCHAR(20) |
| 邮箱 | VARCHAR(255) |
| 文章正文 | TEXT |
| 生日 | DATE |
| 创建时间 | DATETIME |
| 两态开关 | BOOLEAN |
| 扩展属性 | JSON |
CREATE TABLE users (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
email VARCHAR(255) NOT NULL,
phone VARCHAR(20) NULL,
status TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '1=正常,0=禁用',
deleted_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_users_email (email),
UNIQUE KEY uk_users_phone (phone),
KEY idx_users_status_created_at (status, created_at)
) ENGINE = InnoDB
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
CREATE TABLE products (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
sku VARCHAR(50) NOT NULL,
name VARCHAR(200) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
stock INT UNSIGNED NOT NULL DEFAULT 0,
attributes JSON NULL,
status TINYINT UNSIGNED NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_products_sku (sku),
KEY idx_products_status_created_at (status, created_at),
CONSTRAINT chk_products_price CHECK (price >= 0)
) ENGINE = InnoDB
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
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 VARCHAR(20) NOT NULL DEFAULT 'pending',
paid_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_orders_order_no (order_no),
KEY idx_orders_user_created_at (user_id, created_at),
KEY idx_orders_status_created_at (status, created_at),
CONSTRAINT fk_orders_user_id
FOREIGN KEY (user_id) REFERENCES users (id),
CONSTRAINT chk_orders_amount CHECK (total_amount >= 0),
CONSTRAINT chk_orders_status
CHECK (status IN ('pending', 'paid', 'shipped', 'cancelled'))
) ENGINE = InnoDB
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
CREATE TABLE order_items (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
order_id BIGINT UNSIGNED NOT NULL,
product_id BIGINT UNSIGNED NOT NULL,
product_name VARCHAR(200) NOT NULL,
unit_price DECIMAL(10, 2) NOT NULL,
quantity INT UNSIGNED NOT NULL,
subtotal DECIMAL(12, 2) NOT NULL,
PRIMARY KEY (id),
KEY idx_order_items_order_id (order_id),
KEY idx_order_items_product_id (product_id),
CONSTRAINT fk_order_items_order_id
FOREIGN KEY (order_id) REFERENCES orders (id),
CONSTRAINT fk_order_items_product_id
FOREIGN KEY (product_id) REFERENCES products (id),
CONSTRAINT chk_order_items_quantity CHECK (quantity > 0),
CONSTRAINT chk_order_items_subtotal CHECK (subtotal >= 0)
) ENGINE = InnoDB
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
订单明细保留 product_name 和 unit_price 快照,是因为商品以后可能改名或调价,历史订单仍应展示下单时的信息。
SHOW TABLES;
DESCRIBE users;
SHOW FULL COLUMNS FROM users;
SHOW CREATE TABLE users;
SHOW INDEX FROM users;
DESCRIBE:快速查看字段、类型和索引概况。SHOW CREATE TABLE:查看最完整、最可靠的建表定义。SHOW INDEX:查看索引字段顺序和唯一性。ALTER TABLE users
ADD COLUMN last_login_at DATETIME NULL AFTER updated_at;
字段顺序主要影响查看体验,业务代码不应依赖 SELECT * 的字段顺序。
ALTER TABLE users
MODIFY COLUMN username VARCHAR(100) NOT NULL;
MODIFY COLUMN 需要重新写出完整字段定义,否则可能意外丢失 NOT NULL、默认值或注释。
ALTER TABLE users
RENAME COLUMN username TO display_name;
ALTER TABLE users
DROP COLUMN last_login_at;
删除字段前应确认应用代码、报表、索引和数据库对象是否仍在使用它。
CREATE INDEX idx_orders_paid_at ON orders (paid_at);
CREATE UNIQUE INDEX uk_products_name ON products (name);
DROP INDEX idx_orders_paid_at ON orders;
联合索引:
CREATE INDEX idx_orders_user_status_created_at
ON orders (user_id, status, created_at);
联合索引通常从最左字段开始匹配。索引顺序应根据真实的 WHERE、ORDER BY 和 GROUP BY 设计,而不是机械地把所有字段放进去。
ALTER TABLE orders
ADD CONSTRAINT fk_orders_user_id
FOREIGN KEY (user_id) REFERENCES users (id);
ALTER TABLE orders
DROP FOREIGN KEY fk_orders_user_id;
外键两侧字段的类型、是否无符号等属性应保持一致。删除外键不会自动删除相关普通索引,应使用 SHOW INDEX 检查。
ALTER TABLE products
ADD CONSTRAINT chk_products_stock CHECK (stock >= 0);
ALTER TABLE products
DROP CHECK chk_products_stock;
RENAME TABLE users TO app_users;
CREATE TABLE users_backup LIKE users;
复制结构和数据需要再执行:
INSERT INTO users_backup
SELECT * FROM users;
CREATE TABLE ... AS SELECT ... 虽然方便,但不会完整复制原表的主键、索引、外键和自增等定义:
CREATE TABLE active_users AS
SELECT id, username, email
FROM users
WHERE status = 1;
TRUNCATE TABLE users_backup;
TRUNCATE:
WHERE。DROP TABLE IF EXISTS users_backup;
DELETE、TRUNCATE、DROP 对比| 命令 | 删除内容 | 支持 WHERE |
保留表结构 | 自增值通常重置 |
|---|---|---|---|---|
DELETE |
数据行 | 是 | 是 | 否 |
TRUNCATE |
全部数据 | 否 | 是 | 是 |
DROP TABLE |
数据和表结构 | 否 | 否 | 表已不存在 |
SHOW CREATE TABLE 的原始定义?NOT NULL、默认值和注释?-- 创建数据库
CREATE DATABASE IF NOT EXISTS shop
CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
-- 创建表
CREATE TABLE example (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
amount DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
status TINYINT UNSIGNED NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_example_name (name),
KEY idx_example_status_created_at (status, created_at)
) ENGINE = InnoDB
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
-- 查看结构
SHOW CREATE TABLE example;
DESCRIBE example;
SHOW INDEX FROM example;
-- 添加、修改、重命名、删除字段
ALTER TABLE example ADD COLUMN remark VARCHAR(255) NULL;
ALTER TABLE example MODIFY COLUMN remark TEXT NULL;
ALTER TABLE example RENAME COLUMN remark TO description;
ALTER TABLE example DROP COLUMN description;
-- 索引
CREATE INDEX idx_example_created_at ON example (created_at);
DROP INDEX idx_example_created_at ON example;
-- 重命名、清空、删除表
RENAME TABLE example TO examples;
TRUNCATE TABLE examples;
DROP TABLE IF EXISTS examples;