10 KiB
tags, create time
| tags | create time | |||||
|---|---|---|---|---|---|---|
|
2026-05-16 00:00 |
DML — 增删改
概述
DML(Data Manipulation Language)是日常使用最频繁的 SQL 类别——增、改、删构成了应用和数据库之间的数据交互主线。
[!QUESTION] 思考:为什么说「写」比「读」更复杂? SELECT 只需要找到匹配的数据,而 INSERT / UPDATE / DELETE 不仅要改变状态,还要处理并发冲突、约束校验、外键级联、触发器副作用。理解这些隐藏行为,才能写出既正确又高效的写入逻辑。
本节聚焦 MySQL 特有的高效写入技巧和常见陷阱。
INSERT
基础插入
INSERT INTO users (username, email, status)
VALUES ('alice', 'alice@example.com', 1);
-- 批量插入(性能远高于逐条插入)
INSERT INTO users (username, email, status) VALUES
('bob', 'bob@example.com', 1),
('charlie','charlie@example.com', 0),
('david', 'david@example.com', 1);
[!TIP] MySQL 8.0.19+:INSERT ... RETURNING 和 PostgreSQL 一样,MySQL 从 8.0.19 起支持
RETURNING子句,免去二次查询:INSERT INTO users (username, email, status) VALUES ('eve', 'eve@example.com', 1) RETURNING id; -- 直接拿到自增 ID典型场景:插入后需要立即获取新记录的自增主键做级联操作。替代了老方案中
INSERT ... ; SELECT LAST_INSERT_ID();两条语句的组合。
INSERT ... ON DUPLICATE KEY UPDATE
处理"存在则更新,不存在则插入"的场景:
INSERT INTO user_stats (user_id, total_orders, total_spent)
VALUES (1001, 5, 299.50)
ON DUPLICATE KEY UPDATE
total_orders = total_orders + VALUES(total_orders),
total_spent = total_spent + VALUES(total_spent);
[!TIP] 常见陷阱
- 多唯一键冲突:如果有多条唯一键匹配同一行,只更新一次(返回 Row Count = 2)
- LAST_INSERT_ID():INSERT 时返回新 ID;ON DUPLICATE KEY UPDATE 时自动改写为旧行的主键 ID(方便级联操作)
- VALUES() 废弃警告:MySQL 8.0.20+ 对
VALUES(col)发出 DEPRECATION WARNING。对于单行插入无需改动,多行批量插入需改用子查询或分段处理。
flowchart TD
A["INSERT 语句"] --> B{"主键或唯一键\n是否存在"}
B -->|不存在| C["执行 INSERT"]
B -->|已存在| D["执行 UPDATE\nON DUPLICATE KEY UPDATE 子句"]
C --> E["返回 Row Count = 1"]
D --> F["返回 Row Count = 2\n匹配行加实际更新"]
style B fill:#FF9F43,color:#000
style C fill:#00D866,color:#fff
style D fill:#00B6BC,color:#fff
[!NOTE] Row Count 的含义
Row Count = 1:正常插入新记录Row Count = 2:唯一键冲突,走了 UPDATE 路径(但 UPDATE 没有实际改变值也算 2)Row Count = 0:唯一键冲突,且 UPDATE 后的值与原来相同
REPLACE INTO
当唯一键冲突时,先删除旧行再插入新行。
REPLACE INTO users (id, username, email) VALUES (1, 'alice_new', 'new@email.com');
sequenceDiagram
participant S as Server
participant DB as Database
S->>DB: REPLACE INTO users ...
Note over S,DB: Step 1: DELETE 已有记录
S->>DB: DELETE FROM users WHERE pk = 1
Note over S,DB: Step 2: INSERT 新记录
S->>DB: INSERT INTO users ...
Note over S,DB: 如果外键引用了被删行\n会触发 ON DELETE CASCADE
[!WARNING] REPLACE vs ON DUPLICATE KEY UPDATE
- REPLACE:本质是 DELETE + INSERT,会导致自增 ID 变化、触发 BEFORE/AFTER DELETE 钩子、级联外键删除
- ON DUPLICATE KEY UPDATE:原地更新,不影响其他字段和自增值
优先用
ON DUPLICATE KEY UPDATE,除非你确实需要完整的「删+插」语义。
INSERT ... SELECT
从另一张表或查询结果批量插入数据——这是日常开发中比 VALUES 批量插入更高频的用法。
-- 基础:从查询结果插入
INSERT INTO user_archive (id, username, email, archived_at)
SELECT id, username, email, NOW()
FROM users
WHERE status = 0 AND updated_at < '2025-01-01';
-- 搭配聚合:插入每日统计快照
INSERT INTO daily_stats (stat_date, order_count, total_amount)
SELECT CURDATE(), COUNT(*), SUM(amount)
FROM orders
WHERE DATE(created_at) = CURDATE();
[!QUESTION] INSERT ... SELECT 会锁表吗? 这取决于事务隔离级别和索引情况:
- 在 RC(Read Committed) 下:只锁定被插入的目标表,对源表加短暂的共享锁
- 在 RR(Repeatable Read) 下:源表的读取走一致性快照,不会阻塞源表的写入
- 目标表:插入的行会加行锁,大批量插入时注意不要超长事务
[!CAUTION] INSERT ... SELECT 的两个常见坑
- SELECT 中不能引用正在被插入的目标表(MySQL 会报
Table is specified twice)。解决办法:嵌套一层子查询-- ❌ 报错:直接引用目标表 INSERT INTO orders_backup SELECT * FROM orders WHERE id IN (SELECT id FROM orders WHERE ...); -- ✅ 正确:用子查询隔离 INSERT INTO orders_backup SELECT * FROM orders WHERE id IN (SELECT t.id FROM (SELECT id FROM orders WHERE ...) t);- 大批量插入时分批执行,避免长事务持有过多锁。可以用
LIMIT+ 循环分批:-- 每次只插入 5000 条,循环直到 affected_rows = 0 INSERT INTO user_archive SELECT * FROM users WHERE status = 0 LIMIT 5000;
UPDATE
UPDATE 的核心原则只有一条:精准定位、最小影响。先思考「哪些行需要改」,再写 SET 子句。
-- 基础更新:定位到单行
UPDATE users SET status = 0 WHERE id = 100;
-- 多字段同时更新
UPDATE users SET
email = 'new@email.com',
updated_at = NOW()
WHERE email = 'old@email.com' AND status = 1;
-- JOIN 批量更新(从关联表同步数据)
UPDATE articles a
JOIN categories c ON a.category_id = c.id
SET a.category_name = c.name
WHERE a.category_name IS NULL;
[!QUESTION] 为什么 UPDATE 忘记带 WHERE 这么危险?
UPDATE users SET status = 0;会把所有用户的状态设为禁用。MySQL 有一个安全措施叫sql_safe_updates:SET sql_safe_updates = 1; -- 此时不带 WHERE 或不带主键条件的 UPDATE 会被拒绝 -- 生产环境建议在连接层开启此选项
LIMIT 与 ORDER BY
MySQL 特有的扩展:UPDATE 可以加 LIMIT 控制影响行数。
-- 只更新符合条件的前 100 条(配合 ORDER BY 可控)
UPDATE users SET status = 0
WHERE status = 1 AND created_at < '2024-01-01'
ORDER BY created_at ASC
LIMIT 100;
DELETE vs TRUNCATE
DELETE 和 TRUNCATE 都能清空或减少表数据,但底层机制完全不同。选错可能导致性能灾难或数据意外丢失。
| 特性 | DELETE | TRUNCATE |
|---|---|---|
| 类型 | DML | DDL |
| 可回滚 | ✅ 事务内可 ROLLBACK | ❌ 隐式提交,不可回滚 |
| WHERE 过滤 | ✅ 可以 | ❌ 全删 |
| AUTO_INCREMENT | 保留当前值 | 重置为 1 |
| 触发器 | 触发 DELETE 触发器 | 不触发 |
| 速度 | 逐行删除较慢 | 直接重建表极快 |
| 返回值 | 影响的行数 | 无返回值 |
| 锁粒度 | 行锁(可带 WHERE) | 表级元数据锁 |
DELETE 进阶用法
-- DELETE 带 JOIN(MySQL 特有语法)
DELETE u FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL;
-- 删除所有没有订单的用户
-- 多表联合删除(同时删用户 + 订单)
DELETE t1, t2 FROM users t1
INNER JOIN orders t2 ON t1.id = t2.user_id
WHERE t1.status = 0;
[!NOTE] ORDER BY + LIMIT 在 DELETE 中同样可用
-- 只删除符合条件的前 50 条(按创建时间最早的优先) DELETE FROM users WHERE status = 0 ORDER BY created_at ASC LIMIT 50;
高效写入策略
当需要处理大量数据时,写入方式的选择直接影响性能和稳定性。下面按数据量级给出分级方案:
flowchart TD
A["大量数据写入"] --> B{数据量}
B -->|小于 1 万条| C["单条 INSERT + 事务包裹"]
B -->|1 万至 100 万条| D["分批 INSERT\n每批 500 到 2000 条"]
B -->|大于 100 万条| E["LOAD DATA INFILE"]
C --> F["BEGIN; INSERT... COMMIT;"]
D --> G["每批独立事务\n降低锁竞争"]
E --> H["绕过 SQL 解析\n直接写数据文件"]
style E fill:#00D866,color:#fff
style D fill:#FF9F43,color:#000
-- LOAD DATA INFILE 示例(最快的批量导入方式)
LOAD DATA LOCAL INFILE '/tmp/users.csv'
INTO TABLE users
FIELDS TERMINATED BY ',' ENCLOSED BY '"'
LINES TERMINATED BY '\n'
(username, email, @status) -- @variable 用于预处理
SET status = CASE @status WHEN 'active' THEN 1 ELSE 0 END;
分批 INSERT 代码示例
对于中等规模数据,事务 + 分批次 是最实用的方案。以下 Go 伪代码展示了核心思路:
const batchSize = 1000
rows := generateRecords() // 假设返回 50000 条记录
tx := db.Begin() // 外层可开启大事务
for i := 0; i < len(rows); i += batchSize {
end := min(i+batchSize, len(rows))
batch := rows[i:end]
// 构建动态 INSERT
cols := "(username, email, status)"
placeholders := strings.Repeat("(?, ?, ?),", len(batch))
sql := fmt.Sprintf("INSERT INTO users %s VALUES %s", cols, placeholders[:len(placeholders)-1])
var args []any
for _, r := range batch {
args = append(args, r.Username, r.Email, r.Status)
}
tx.Exec(sql, args...) // 单批提交
}
tx.Commit()
[!CAUTION] 批量插入注意事项
- 包大小限制:MySQL 默认
max_allowed_packet为 64MB,超大批次会报Packet too large错误- 长事务锁表:单个大事务持有锁的时间越长,死锁概率越高 → 每批独立 commit 更安全
- 自增 ID 碎片:大批量 INSERT 会导致自增 ID 跳跃,不影响功能,但会影响
AUTO_INCREMENT当前值的准确性
关联笔记
- hhs/GORM/03-CRUD 操作 — GORM 的 Create / First / Find 等方法的 SQL 生成机制
- hhs/GORM/11-批量操作 — GORM 中 CreateInBatches 等批量优化手段