用存储过程封装分批delete加作业调度器触发是最可控、易排查的线上日志清理方案;硬写单条delete或依赖表名推断过期极易出错;sql server中须预计算时间变量并用top分批。

直接上结论:用存储过程封装分批 DELETE + 作业调度器触发,是目前最可控、最易排查的线上日志清理方案。硬写单条 DELETE 或依赖表名推断过期,90%会出事。
SQL Server 存储过程必须预计算时间变量并加 TOP 分批
常见错误现象:DELETE FROM logs WHERE created_at 看似简洁,但 <code>GETDATE() 在每行都重新求值,且没索引时全表扫描;大表执行一次锁表十几秒,写入直接阻塞。
- 必须先声明截止时间变量:
DECLARE @cutoff DATETIME2 = DATEADD(day, -30, GETDATE()),后续所有条件都引用它 -
created_at字段必须建非聚集索引(哪怕只是INCLUDE主键),否则性能随数据量指数恶化 - 每次只删固定行数:
DELETE TOP (1000) FROM logs WHERE created_at ,避免长事务和锁升级 - 别用
TRUNCATE TABLE清理部分数据——它不支持WHERE,清错就是整表丢
MySQL 存储过程里 DATE_SUB(NOW(), INTERVAL 30 DAY) 要抽成变量
常见错误现象:在 CREATE EVENT 或循环体中反复写 WHERE create_time ,遇到长事务时可能被多次求值,导致同一批数据删两次或漏删;更糟的是,<code>NOW() 在事件定义里直接报 ERROR 1064。
- 正确做法:开头用
SET @cutoff = DATE_SUB(NOW(), INTERVAL 30 DAY)固化时间点 - 时间单位必须单数:
INTERVAL 1 HOUR合法,INTERVAL 1 HOURS语法错误 - 循环删除必须加
SLEEP(0.5),否则连续高负载可能拖垮从库复制 - 大表建议先
SELECT id INTO TEMPORARY TABLE获取待删 ID,再分批DELETE WHERE id IN (...),规避LIMIT在某些版本下对ORDER BY的隐式依赖
作业调度器配置最容易漏掉数据库上下文
常见错误现象:SQL Server 作业步骤里写 EXEC CleanOldData 却报 Could not find stored procedure;MySQL 事件调用后无反应;PostgreSQL pg_cron 报 permission denied for function。
- SQL Server Agent 默认以
master库为上下文运行,必须在作业步骤的Database name字段里填目标库名(如MyAppDB),不能留空 - MySQL 事件体里调用存储过程必须带库名前缀:
CALL MyAppDB.CleanOldData(),否则找不到 - PostgreSQL 的
pg_cronworker 以postgres用户身份执行,函数创建时必须显式加SECURITY DEFINER,否则权限继承失效 - 所有调度器上线前务必手动右键“Start job at step…”测试,别等凌晨两点才第一次跑
别信表名含日期就代表可删——时间戳字段才是唯一依据
很多团队看到 log_202503 就直接 DROP TABLE,结果删掉还在写入的当月分区;或者用正则匹配 ^log_\d{6}$ 就删,却没过滤 relkind = 'r',误删了同名索引。
- SQL Server 动态删表前,必须查
sys.tables确认存在且schema_id = SCHEMA_ID('dbo'),再用QUOTENAME(@table_name)包裹拼接 - PostgreSQL 分区表清理,必须解析
pg_get_expr(relpartbound, oid)拿真实边界值,不能只靠表名字符串推断 - MySQL 没原生分区管理,但
information_schema.TABLES里的CREATE_TIME是元数据创建时间,不是数据写入时间,不可用于判断日志过期
真正难的不是写 DELETE,而是让每一次执行都可预期、可回溯、可中断。分批逻辑要能被人工复现,时间阈值不能硬编码,调度上下文不能靠猜——这些细节一旦漏掉,清理任务就会从运维工具变成定时炸弹。










