事情是这样的,我有一个 MySQL 表,大概 100 个 G,但数据只有 10000 条。
看起来行数不多,奈何每条数据里塞了好几个 LONGTEXT 字段,单条数据体量巨大。
最近需要对这张表做一个备份操作:根据批次 ID(batchid)把某批数据备份到另一个库,然后删掉原表里对应的数据。
目标库有同名表,用来归档历史数据。思路很直接,两条 SQL 搞定:
-- 先把数据迁过去
INSERT INTO history_db.target_table
SELECT * FROM target_table WHERE batchid = 381;
-- 再把原表数据删掉
DELETE FROM target_table WHERE batchid = 381;
结果一执行直接报错,要么是:
SQL 错误 3 HY000: Error writing file 'C:\Windows\TEMP\MLa4gsg6dpk9s2z8ry' (OS errno 28 - No space left on device)
要么是:
SQL 错误 1114 HY000: The table 'C:\Windows\TEMP#sql6f88_bd086f_2' is full
MySQL 是装在Windows服务器的,不过明明装在 D 盘,为什么 C 盘会爆?
实际上,MySQL 在执行大事务或者大查询时,会在系统临时目录(C:\Windows\TEMP)生成临时文件。当一次操作的数据量太大,临时文件直接把 C 盘写满了,SQL 自然就挂了。INSERT ... SELECT 一次性搬运大量 LONGTEXT 数据,临时表膨胀得飞快(现象就是服务器剩余50G的C盘迅速飙红,待执行失败后又恢复正常)。
既然一步到位不行,那就化整为零------用存储过程分批处理:
CREATE DEFINER=`root`@`%` PROCEDURE `business_db`.`batch_migrate_and_delete`(
IN p_table_name VARCHAR(128),
IN p_batchid BIGINT
)
batch_migrate_and_delete: BEGIN
DECLARE v_min_id BIGINT;
DECLARE v_max_id BIGINT;
DECLARE v_batch_size INT DEFAULT 500;
-- 获取 id 范围
SET @sql_range = CONCAT(
'SELECT MIN(id), MAX(id) INTO @v_min, @v_max FROM ',
p_table_name, ' WHERE batchid = ', p_batchid
);
PREPARE stmt FROM @sql_range;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET v_min_id = @v_min;
SET v_max_id = @v_max;
IF v_min_id IS NULL THEN
LEAVE batch_migrate_and_delete;
END IF;
-- 每批先迁移到历史库,再删除原表数据
WHILE v_min_id <= v_max_id DO
SET @sql_insert = CONCAT(
'INSERT INTO history_db.', p_table_name,
' SELECT * FROM ', p_table_name,
' WHERE batchid = ', p_batchid,
' AND id BETWEEN ', v_min_id, ' AND ', v_min_id + v_batch_size - 1
);
PREPARE stmt FROM @sql_insert;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql_delete = CONCAT(
'DELETE FROM ', p_table_name,
' WHERE batchid = ', p_batchid,
' AND id BETWEEN ', v_min_id, ' AND ', v_min_id + v_batch_size - 1
);
PREPARE stmt FROM @sql_delete;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
COMMIT;
SET v_min_id = v_min_id + v_batch_size;
END WHILE;
END batch_migrate_and_delete
创建存储过程后,执行call即可
CALL batch_migrate_and_delete('target_table', 381)
存储过程的核心思路很简单:先用 MIN(id) 和 MAX(id) 圈出要处理的数据范围,然后每次取 500 条(v_batch_size),做完 insert 和 delete 立刻 COMMIT,再继续下一批。这样每批事务体量都很小,临时文件来不及撑爆 C 盘就已经释放了。
⚠️ 需要注意的是:
-
v_batch_size要结合实际调。我这里的表单条数据就很大,设 500 差不多。如果你的表行数多但单条很小,可以适当调大;反过来单条更大就调小一些,核心是控制每批事务的数据总量。 - 按
id BETWEEN分批的前提是 id 连续或大致连续 。如果 id 有大量空洞,可以换成LIMIT配合游标的方式分批。 - 迁移和删除放在同一个事务批次里。先 insert 成功再 delete,万一 insert 失败,当前批次不会被删掉,数据不会丢。
- Windows 下装 MySQL 要留意临时目录位置 。如果 C 盘实在太小,可以在 MySQL 配置文件里把
tmpdir指向其他盘,从根源上避免这个问题。