1. 千万级大表删除数据的挑战与核心策略
当表数据量达到千万级时,简单的DELETE操作可能引发灾难性后果。我曾亲历一次生产事故:执行DELETE FROM user_logs WHERE create_time < '2020-01-01'后,数据库CPU飙升至100%,连接池耗尽导致全线服务不可用。这种场景下,我们需要理解三个关键问题:
- 事务日志膨胀:单条DELETE语句会产生巨大的事务日志,可能撑爆磁盘空间
- 锁竞争加剧:长时间持有行锁/表锁会阻塞其他查询
- WAL写入压力:PostgreSQL等使用WAL的数据库会出现检查点堆积
针对千万级大表,推荐采用分批次删除策略。以下是各数据库的通用处理框架:
sql复制-- 伪代码示例:分批删除模板
WHILE EXISTS(SELECT 1 FROM big_table WHERE condition)
DO
DELETE FROM big_table
WHERE id IN (
SELECT id
FROM big_table
WHERE condition
LIMIT 10000 -- 每批处理量
);
COMMIT; -- 关键!每批提交事务
PAUSE 100ms; -- 控制处理节奏
END WHILE;
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 主流数据库的优化删除方案
2.1 PostgreSQL的CTID分片删除法
PostgreSQL的CTID是隐藏的物理行标识符,利用它可以实现高效分片。我曾用这个方法在2亿条记录的日志表中删除3000万数据,耗时从预估的8小时降至47分钟:
sql复制-- 步骤1:创建删除函数
CREATE OR REPLACE FUNCTION batch_delete()
RETURNS void AS $$
DECLARE
batch_size INT := 5000;
max_ctid TID;
BEGIN
SELECT max(ctid) INTO max_ctid FROM orders WHERE status = 'expired';
FOR i IN 0..(max_ctid::text::point[0]::int/batch_size) LOOP
DELETE FROM orders
WHERE ctid >= format('(%s,0)', i*batch_size)::tid
AND ctid < format('(%s,0)', (i+1)*batch_size)::tid
AND status = 'expired';
COMMIT;
PERFORM pg_sleep(0.1); -- 控制节奏
END LOOP;
END;
$$ LANGUAGE plpgsql;
-- 步骤2:执行删除
SELECT batch_delete();
关键提示:CTID方案需要定期执行VACUUM FULL回收空间,建议在业务低峰期进行
2.2 MySQL的分区表删除策略
对于MySQL,我推荐采用分区表+TRUNCATE PARTITION的方案。某电商平台的历史订单表采用该方案后,数据清理时间从小时级降至秒级:
sql复制-- 创建按月的RANGE分区表
CREATE TABLE order_history (
id BIGINT,
order_date DATETIME,
-- 其他字段
PRIMARY KEY (id, order_date)
) PARTITION BY RANGE (TO_DAYS(order_date)) (
PARTITION p202201 VALUES LESS THAN (TO_DAYS('2022-02-01')),
PARTITION p202202 VALUES LESS THAN (TO_DAYS('2022-03-01')),
-- 其他月份分区
);
-- 删除整个分区数据(瞬时完成)
ALTER TABLE order_history TRUNCATE PARTITION p202201;
实测对比:
| 方案 | 1000万数据删除耗时 | 锁持有时间 | 对业务影响 |
|---|---|---|---|
| 直接DELETE | 42分钟 | 全程持有 | 严重阻塞 |
| 分批DELETE | 38分钟 | 间歇持有 | 中等影响 |
| TRUNCATE PARTITION | 0.3秒 | 瞬间释放 | 几乎无感 |
2.3 Oracle的DBMS_PARALLEL_EXECUTE方案
Oracle的DBMS_PARALLEL_EXECUTE包可以实现真正的并行删除。在某银行系统中,我们通过以下脚本将8小时的删除任务压缩到23分钟:
sql复制-- 步骤1:创建任务分块
BEGIN
DBMS_PARALLEL_EXECUTE.CREATE_TASK('purge_old_data');
DBMS_PARALLEL_EXECUTE.CREATE_CHUNKS_BY_ROWID(
task_name => 'purge_old_data',
table_owner => 'SCHEMA',
table_name => 'BIG_TABLE',
by_row => TRUE,
chunk_size => 100000
);
END;
/
-- 步骤2:定义并行处理
BEGIN
DBMS_PARALLEL_EXECUTE.RUN_TASK(
task_name => 'purge_old_data',
sql_stmt => 'DELETE FROM big_table WHERE rowid BETWEEN :start_id AND :end_id AND create_date < ADD_MONTHS(SYSDATE, -36)',
language_flag => DBMS_SQL.NATIVE,
parallel_level => 8 -- 根据CPU核心数调整
);
END;
/
3. 特殊场景的删除优化技巧
3.1 存在外键约束时的级联删除
当大表被其他表外键引用时,级联删除会成为性能杀手。我的团队曾处理过一个案例:删除主表记录导致18个关联表的触发操作,最终采用以下方案:
sql复制-- 方案1:临时禁用约束(需评估业务风险)
ALTER TABLE child_table DISABLE TRIGGER ALL;
-- 执行主表删除
DELETE FROM parent_table WHERE condition;
-- 重建约束(可能需验证数据一致性)
ALTER TABLE child_table ENABLE TRIGGER ALL;
-- 方案2:使用ON DELETE SET NULL
-- 建表时定义外键行为
CREATE TABLE child_table (
id SERIAL PRIMARY KEY,
parent_id INT REFERENCES parent_table(id) ON DELETE SET NULL
);
3.2 表包含大型LOB字段的清理
包含BLOB/CLOB字段的表删除效率极低,建议采用特殊处理:
sql复制-- 方法1:先移出LOB数据再删除
CREATE TABLE temp_table AS
SELECT id, non_lob_columns
FROM big_table
WHERE condition;
-- 然后TRUNCATE原表并重新导入
-- 方法2:使用DBMS_LOB.ERASE逐步清理
DECLARE
CURSOR c_lob IS
SELECT lob_column FROM big_table
WHERE condition
FOR UPDATE;
BEGIN
FOR r IN c_lob LOOP
DBMS_LOB.ERASE(r.lob_column, DBMS_LOB.GETLENGTH(r.lob_column), 1);
DELETE FROM big_table WHERE CURRENT OF c_lob;
IF MOD(c_lob%ROWCOUNT, 1000) = 0 THEN
COMMIT;
END IF;
END LOOP;
COMMIT;
END;
4. 删除操作的监控与风险控制
4.1 执行进度监控方案
我习惯使用以下监控脚本跟踪删除进度:
sql复制-- PostgreSQL进度查询
SELECT
schemaname,
relname,
pg_size_pretty(pg_total_relation_size(relid)) AS total_size,
pg_size_pretty(pg_total_relation_size(relid) - pg_relation_size(relid)) AS dead_size,
n_dead_tup,
n_live_tup,
round(100*n_dead_tup::float/(n_dead_tup+n_live_tup),2) AS dead_ratio
FROM pg_stat_user_tables
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
ORDER BY dead_ratio DESC;
-- MySQL进度监控
SELECT
TABLE_SCHEMA,
TABLE_NAME,
TABLE_ROWS,
DATA_LENGTH/1024/1024 AS size_mb,
INDEX_LENGTH/1024/1024 AS index_mb
FROM information_schema.TABLES
WHERE TABLE_SCHEMA NOT IN ('mysql','information_schema','performance_schema');
4.2 紧急中断与回滚方案
当发现删除操作影响业务时,需要快速响应:
-
KILL会话:立即终止删除进程
sql复制-- MySQL SHOW PROCESSLIST; KILL [process_id]; -- PostgreSQL SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE query LIKE '%DELETE FROM big_table%'; -
空间回收:中断后需要处理膨胀问题
sql复制-- PostgreSQL VACUUM FULL VERBOSE big_table; -- MySQL OPTIMIZE TABLE big_table; -
闪回恢复:Oracle等支持闪回的数据库
sql复制FLASHBACK TABLE big_table TO TIMESTAMP (SYSTIMESTAMP - INTERVAL '30' MINUTE);
在实际操作中,我总结出几个关键指标阈值:
- 锁等待时间 > 500ms 应告警
- 事务持续时间 > 5分钟 应考虑中断
- 磁盘空间使用率 > 85% 应暂停操作
