MySQL 5.7事件不支持调用带参存储过程,必须用事件触发无参存储过程,再由其通过PREPARE/EXECUTE动态拼接SQL实现通用冷数据清理。

MySQL 5.7 不支持在事件中直接调用带参数的存储过程,也不能在事件定义里写动态表名或条件参数——所以“定时 + 存储过程 + 冷数据清理”必须拆成两层:事件只负责定时触发,清理逻辑全压在存储过程中,且存储过程内部必须用预处理语句(PREPARE/EXECUTE)拼接 SQL。
为什么不能直接在 EVENT 中写 CALL cleanup_proc('device', 'created_at', 180)
MySQL 5.7 的 CREATE EVENT 语法不接受参数传递给 CALL,以下写法会报错:
CREATE EVENT evt_clean ON SCHEDULE EVERY 1 DAY DO CALL cleanup_proc('device', 'created_at', 180);错误信息:ERROR 1064 (42000): You have an error in your SQL syntax。5.7 对事件体内的语句限制很死,只允许简单语句或 BEGIN...END 块,但块内仍不能传参进存储过程调用。
真正能跑通的只有:
- 事件里写死表名、字段、时间条件(不灵活,改一次就得重定义事件)
- 事件里调用无参存储过程(推荐),所有可变逻辑由存储过程自己处理
存储过程必须用 PREPARE 动态构造 DELETE 语句
冷数据通常按时间判断(如 created_at < DATE_SUB(NOW(), INTERVAL 180 DAY)),但表名、日期列名、过期天数这些不能硬编码在存储过程中——否则每换一张表就要改一次存储过程。解决办法是把它们作为参数接收,再用字符串拼接 + 预处理执行。
示例(创建一个通用清理存储过程):
DELIMITER //
CREATE PROCEDURE cleanup_cold_data(
IN p_table_name VARCHAR(64),
IN p_date_column VARCHAR(64),
IN p_expiration_days INT
)
BEGIN
SET @sql := CONCAT(
'DELETE FROM `', p_table_name, '` ',
'WHERE `', p_date_column, '` < DATE_SUB(NOW(), INTERVAL ', p_expiration_days, ' DAY) ',
'LIMIT 1000'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END//
DELIMITER ;关键点:
-
CONCAT拼接时必须对表名、列名加反引号(`),防止含特殊字符或关键字出错 - 必须用
LIMIT 1000,否则大表一次性删可能锁表、打满 IO、拖垮主从 -
DEALLOCATE PREPARE必须写,否则连接长期复用时会报ERROR 1243 (HY000): Unknown prepared statement handler
事件只能调用无参存储过程,且需确保权限和调度器开启
创建事件前,先确认三件事:
-
SHOW VARIABLES LIKE 'event_scheduler';返回值必须是ON,否则事件永不触发 - 执行
CREATE EVENT的用户必须有EVENT权限:GRANT EVENT ON your_db.* TO 'user'@'%'; - 存储过程必须在同一个数据库下被调用,跨库需显式写
db_name.procedure_name()
事件定义示例(每天凌晨 2:30 清理 device 表中 180 天前的数据):
DELIMITER //
CREATE EVENT evt_cleanup_device_cold
ON SCHEDULE EVERY 1 DAY
STARTS TIMESTAMP(CURDATE() + INTERVAL 1 DAY + INTERVAL 2 HOUR + INTERVAL 30 MINUTE)
ON COMPLETION PRESERVE
ENABLE
DO CALL cleanup_cold_data('device', 'created_at', 180)//
DELIMITER ;注意:
-
STARTS用TIMESTAMP(...)算出首次执行时间,避免用NOW()导致事件立刻触发(可能撞上业务高峰) - 不要依赖系统时区自动对齐——检查
SELECT @@time_zone;,若为SYSTEM,需确认 mysqld 启动环境的TZ设置是否与业务预期一致
分批删除必须加循环 + ROW_COUNT() 判断,否则只删一次就停
上面的 cleanup_cold_data 存储过程每次只删 1000 行,但冷数据可能有几十万行。要删完,得包一层循环:
DELIMITER //
CREATE PROCEDURE cleanup_cold_data_batch(
IN p_table_name VARCHAR(64),
IN p_date_column VARCHAR(64),
IN p_expiration_days INT,
IN p_batch_size INT DEFAULT 1000
)
BEGIN
DECLARE rows_affected INT DEFAULT p_batch_size;
WHILE rows_affected = p_batch_size DO
SET @sql := CONCAT(
'DELETE FROM `', p_table_name, '` ',
'WHERE `', p_date_column, '` < DATE_SUB(NOW(), INTERVAL ', p_expiration_days, ' DAY) ',
'LIMIT ', p_batch_size
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET rows_affected = ROW_COUNT();
-- 控制节奏,避免连续高压
DO SLEEP(0.05);
END WHILE;
END//
DELIMITER ;这个版本的关键差异:
- 用
WHILE ... DO循环持续删,直到某次ROW_COUNT()小于p_batch_size(说明没数据可删了) -
DO SLEEP(0.05)是 MySQL 5.7 原生支持的暂停指令,比应用层 sleep 更可控,也避免刷爆 binlog - 如果表没有索引在
p_date_column上,WHERE条件会全表扫描——性能会断崖下跌,这点极易被忽略


















