批量操作优化
问题
如何高效地进行批量插入、批量更新和批量删除?大表的 DML 操作有哪些注意事项?
答案
批量插入优化
多值 INSERT
-- ❌ 逐条插入:每条都是一个事务,频繁 IO
INSERT INTO users (name, age) VALUES ('Alice', 25);
INSERT INTO users (name, age) VALUES ('Bob', 30);
INSERT INTO users (name, age) VALUES ('Charlie', 28);
-- ✅ 合并为一条 INSERT:减少网络往返和事务开销
INSERT INTO users (name, age) VALUES
('Alice', 25),
('Bob', 30),
('Charlie', 28);
每批大小建议:一般建议每批 500-1000 行,避免单条 SQL 过大(超过 max_allowed_packet)。
手动控制事务
-- ✅ 手动开启事务,批量提交
START TRANSACTION;
INSERT INTO users (name, age) VALUES ('Alice', 25);
INSERT INTO users (name, age) VALUES ('Bob', 30);
-- ... 每 1000 条提交一次
COMMIT;
START TRANSACTION;
-- ... 下一批
COMMIT;
LOAD DATA INFILE
对于大量数据导入,LOAD DATA INFILE 是最快的方式(比 INSERT 快 20 倍以上):
LOAD DATA INFILE '/tmp/data.csv'
INTO TABLE users
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 LINES -- 跳过表头
(name, age, email);
批量插入优化参数
-- 临时关闭唯一索引检查(确保数据无重复的前提下)
SET unique_checks = 0;
-- 临时关闭外键检查
SET foreign_key_checks = 0;
-- 调大 bulk_insert_buffer_size
SET bulk_insert_buffer_size = 256 * 1024 * 1024; -- 256MB
-- 执行批量插入...
-- 恢复
SET unique_checks = 1;
SET foreign_key_checks = 1;
批量更新优化
CASE WHEN 批量更新
-- ❌ 逐条更新
UPDATE products SET price = 10.0 WHERE id = 1;
UPDATE products SET price = 20.0 WHERE id = 2;
UPDATE products SET price = 30.0 WHERE id = 3;
-- ✅ CASE WHEN 合并为一条
UPDATE products SET price = CASE id
WHEN 1 THEN 10.0
WHEN 2 THEN 20.0
WHEN 3 THEN 30.0
END
WHERE id IN (1, 2, 3);
INSERT ON DUPLICATE KEY UPDATE
-- 存在则更新,不存在则插入
INSERT INTO user_stats (user_id, login_count, last_login)
VALUES
(1, 1, NOW()),
(2, 1, NOW()),
(3, 1, NOW())
ON DUPLICATE KEY UPDATE
login_count = login_count + 1,
last_login = NOW();
分批更新大表
-- ❌ 一次更新所有行:长时间锁表
UPDATE orders SET status = 'archived' WHERE create_time < '2023-01-01';
-- ✅ 分批更新,每批处理一定量
-- 方法:循环更新直到影响行数为 0
UPDATE orders SET status = 'archived'
WHERE create_time < '2023-01-01' AND status != 'archived'
LIMIT 1000;
-- 重复执行直到 affected_rows = 0
批量删除优化
大表删除策略
-- ❌ 直接删除大量数据:长时间锁定、大量 undo log、binlog 膨胀
DELETE FROM logs WHERE create_time < '2023-01-01';
-- ✅ 分批删除
REPEAT
DELETE FROM logs
WHERE create_time < '2023-01-01'
LIMIT 1000;
-- 控制每批删除量,避免长事务
-- 建议每批之间 sleep 100ms,给其他事务执行的机会
UNTIL ROW_COUNT() = 0
大表清理的最佳实践
方案 1:分批删除 + 延时
# Python 示例
import time
import pymysql
conn = pymysql.connect(...)
cursor = conn.cursor()
while True:
cursor.execute("""
DELETE FROM logs
WHERE create_time < '2023-01-01'
LIMIT 1000
""")
conn.commit()
if cursor.rowcount == 0:
break
time.sleep(0.1) # 每批间隔 100ms
方案 2:交换表名(适用于保留少量数据)
-- 如果要清理的数据远多于保留的数据
-- 1. 创建新表
CREATE TABLE logs_new LIKE logs;
-- 2. 将需要保留的数据插入新表
INSERT INTO logs_new SELECT * FROM logs WHERE create_time >= '2023-01-01';
-- 3. 交换表名
RENAME TABLE logs TO logs_old, logs_new TO logs;
-- 4. 确认无误后删除旧表
DROP TABLE logs_old;
方案 3:分区表按分区清理
-- 如果表按时间分区,直接删除整个分区(瞬间完成)
ALTER TABLE logs DROP PARTITION p202301;
大表 DDL 操作
大表添加索引、修改列等 DDL 操作也需要注意:
-- MySQL 5.6+ 支持 Online DDL
-- ALGORITHM=INPLACE:尽量就地修改(不重建表)
-- LOCK=NONE:不锁表
ALTER TABLE orders ADD INDEX idx_status(status), ALGORITHM=INPLACE, LOCK=NONE;
-- 大表添加列(8.0 Instant DDL,秒级完成)
ALTER TABLE orders ADD COLUMN remark VARCHAR(200) DEFAULT NULL, ALGORITHM=INSTANT;
pt-online-schema-change
对于不支持 Online DDL 的操作,使用 Percona 的 pt-osc 工具:
pt-online-schema-change \
--alter "ADD INDEX idx_status(status)" \
--execute D=mydb,t=orders
它通过创建影子表、同步数据、交换表名来实现不停机 DDL。
常见面试问题
Q1: 为什么不建议一次删除大量数据?
答案:
- 长时间锁定:大事务持有行锁/间隙锁时间长,阻塞其他操作
- undo log 膨胀:删除操作产生大量 undo log,占用回滚段空间
- binlog 膨胀:一条大 DELETE 产生巨大的 binlog 事件,影响主从同步
- 回滚风险:中途失败,回滚可能比执行还慢
正确做法:分批删除,每批 1000-5000 行,批次间留间隔。
Q2: LOAD DATA INFILE 为什么比 INSERT 快?
答案:
- 减少 SQL 解析:只解析一次 SQL
- 批量写入:一次 IO 写入大量数据
- 减少索引维护:可以先禁用索引,导入后再重建
- 减少日志:binlog 记录更紧凑
- 跳过事务开销:减少事务提交次数
通常比逐条 INSERT 快 20-50 倍。
Q3: 如何在不停机的情况下给大表添加索引?
答案:
-
MySQL 5.6+ Online DDL:
ALTER TABLE t ADD INDEX idx(col), ALGORITHM=INPLACE, LOCK=NONE;支持在不阻塞 DML 的情况下添加索引。
-
pt-online-schema-change:创建影子表,同步数据,原子交换表名。适用于 MySQL 5.5 或不支持 Online DDL 的操作。
-
gh-ost(GitHub 开源):类似 pt-osc,但使用 binlog 同步而非触发器,对主库影响更小。
Q4: INSERT ON DUPLICATE KEY UPDATE 和 REPLACE INTO 的区别?
答案:
| 对比 | INSERT ... ON DUPLICATE KEY UPDATE | REPLACE INTO |
|---|---|---|
| 冲突处理 | 更新已有行 | 删除旧行 + 插入新行 |
| 自增 ID | 不变 | 新 ID(旧行被删,自增 +1) |
| 触发器 | 触发 INSERT 或 UPDATE | 触发 DELETE + INSERT |
| 外键 | 不影响 | 可能触发级联删除 |
推荐用 ON DUPLICATE KEY UPDATE,避免 REPLACE INTO 的副作用。