当前位置: 代码网 > it编程>数据库>Mysql > MySQL中存储过程大表分批删除历史数据的四种方案

MySQL中存储过程大表分批删除历史数据的四种方案

2026年08月15日 Mysql 我要评论
一、为什么不能一条 delete 搞定一张 5000 万行的表,直接执行 delete from orders where created_at < '2024-01-01' 会

一、为什么不能一条 delete 搞定

一张 5000 万行的表,直接执行 delete from orders where created_at < '2024-01-01' 会删除 3000 万行。这条语句在 innodb 引擎下的行为是:

具体问题汇总:

问题原理影响
长时间锁表大批量 delete 持有行锁/间隙锁其他读写请求被阻塞,业务不可用
主从延迟巨量 binlog 集中写入 relay log从库落后主库数分钟甚至数小时
事务膨胀undo log + redo log 暴增磁盘空间打满,甚至触发 oom
超时中断客户端/代理层有查询超时限制大事务被中途打断,数据处于中间态
影响优化器统计信息剧烈波动查询计划可能变差,影响线上查询

正确的做法是分批删除:每次只删一小批(如 5000~50000 条),每批之间暂停一段时间,让数据库有时间处理其他请求和同步从库。

二、操作前安全检查清单

在执行任何删除操作之前,逐项确认以下条件。跳过任何一项都可能导致生产事故。

2.1 确认待删数据量

-- 查看有多少数据需要删除
select count(*) as total_to_delete
from orders
where created_at < date_sub(now(), interval 1 year);

记录这个数字,后续用来验证删除进度。如果总量超过表总行数的 50%,考虑使用 create table ... select 保留新数据 + rename table 的方案(详见方案四)。

2.2 确认索引

分批删除的 where 条件必须走索引,否则每批都会全表扫描:

-- 检查 created_at 上是否有索引
show index from orders where column_name = 'created_at';

如果没有索引,需要先添加。大表加索引建议在低峰期使用 pt-online-schema-change

pt-online-schema-change \
  --alter "add index idx_created_at (created_at)" \
  d=your_db,t=orders \
  --execute

2.3 确认备份

-- 至少确认最近的备份时间点
show variables like 'log_bin%';
-- 确认 binlog 是否开启(用于 point-in-time recovery)
show variables like 'log_bin';

2.4 确认执行窗口

检查项说明
当前 tps/qps在低峰期执行,show global status like 'queries'
主从延迟show slave status 确认 seconds_behind_master = 0
磁盘空间df -h 确认 undo/redo log 所在分区有足够余量
业务通知提前通知相关团队,预留应急回滚窗口

三、四种方案对比

不同场景适合不同方案,下表给出完整对比:

维度存储过程pt-archiver应用层分批分区表 drop
适用场景一次性清理定期归档清理业务逻辑内按时间分区
需要装工具是(percona toolkit)否(需提前设计)
对主从影响可控(调 sleep)最小(内置保护)可控几乎无影响
空间回收需额外 optimize自动(delete 模式)需额外处理自动释放
操作复杂度低(需提前建分区)
可中断恢复重新 call 即可天然支持(幂等)天然支持不支持(drop 不可逆)
推荐度4/55/53/55/5(有分区时)

选型决策树

四、方案一:存储过程分批删除(推荐快速上手)

这是最简单直接的方案,不需要安装任何额外工具,在 navicat 或任何 mysql 客户端中即可执行。

4.1 完整存储过程

delimiter $$

drop procedure if exists batch_delete_old_data$$

create procedure batch_delete_old_data(
    in p_table     varchar(64),   -- 表名
    in p_condition varchar(255),  -- 删除条件
    in p_batch     int,           -- 每批删除条数
    in p_sleep     decimal(3,1)   -- 批间暂停秒数
)
begin
    declare v_rows   int default 1;
    declare v_total  bigint default 0;
    declare v_sql    text;

    -- 动态拼接 sql,支持任意表和条件
    set v_sql = concat('delete from ', p_table,
                       ' where ', p_condition,
                       ' limit ', p_batch);

    while v_rows > 0 do
        set @stmt = v_sql;
        prepare stmt from @stmt;
        execute stmt;
        deallocate prepare stmt;

        set v_rows = row_count();
        set v_total = v_total + v_rows;

        -- 每删除 10 万条输出一次进度
        if v_total % 100000 < p_batch then
            select concat('已删除 ', v_total, ' 条') as progress;
        end if;

        do sleep(p_sleep);
    end while;

    select concat('删除完成,共删除 ', v_total, ' 条记录') as result;
end$$

delimiter ;

4.2 调用示例

-- 场景:删除 orders 表中一年前的数据,每批 1 万条,暂停 0.5 秒
call batch_delete_old_data(
    'orders',
    'created_at < date_sub(now(), interval 1 year)',
    10000,
    0.5
);

预期输出:

+---------------------+
| progress            |
+---------------------+
| 已删除 100000 条    |
+---------------------+

...(中间多轮输出)...

+------------------------------------------+
| result                                   |
+------------------------------------------+
| 删除完成,共删除 45820000 条记录          |
+------------------------------------------+

4.3 执行流程图

4.4 关键参数调优

参数建议值说明
p_batch5000~50000太小效率低,太大锁表时间长。推荐从 10000 开始,观察负载后调整
p_sleep0.5~2.0有从库时建议 >= 1 秒。单机可以 0.5 秒
执行时间低峰期避开业务高峰,建议在凌晨 2:00~6:00 执行

4.5 navicat 中 delimiter 报错的替代方案

部分 navicat 版本的查询窗口不支持 delimiter 语法。三种替代方式:

  1. navicat 命令行界面:菜单「工具 -> 命令行界面」,在终端中粘贴 sql
  2. 外部 mysql 客户端mysql -h host -u user -p dbname < procedure.sql
  3. 简化版存储过程:不使用 delimiter,直接在查询窗口中分步执行

五、方案二:pt-archiver(生产环境首选)

pt-archiver 是 percona toolkit 提供的专业归档工具,天然支持分批删除,对主从延迟有内置保护。

5.1 安装

# macos
brew install percona-toolkit
# ubuntu/debian
apt-get install percona-toolkit
# centos/rhel
yum install percona-toolkit

5.2 基本用法:直接删除

# 删除 orders 表中一年前的数据,每批 5000 条
pt-archiver \
  --source d=your_db,t=orders,h=127.0.0.1,u=root,p=your_password \
  --where "created_at < date_sub(now(), interval 1 year)" \
  --limit 5000 \
  --bulk-delete \
  --progress 10000 \
  --statistics

5.3 进阶用法:归档到另一张表

# 把旧数据归档到 orders_archive 表,而不是直接删除
pt-archiver \
  --source d=your_db,t=orders,h=127.0.0.1,u=root,p=your_password \
  --dest d=your_db,t=orders_archive \
  --where "created_at < date_sub(now(), interval 1 year)" \
  --limit 5000 \
  --bulk-insert \
  --bulk-delete \
  --progress 10000

5.4 关键参数说明

参数说明
--limit每批处理的行数,等同于存储过程的 limit
--bulk-delete使用批量 delete 而非逐行删除,效率更高
--progress n每处理 n 行输出一次进度
--statistics执行结束后输出统计摘要
--check-slave-lag指定从库地址,自动检测并等待主从同步
--max-lag主从延迟超过此秒数时自动暂停(默认 1 秒)
--dry-run只输出将要执行的 sql,不实际执行。强烈建议先用这个测试

5.5 pt-archiver vs 存储过程

pt-archiver 的核心优势是从库延迟保护——它会自动检测从库延迟并在延迟过高时暂停,存储过程需要手动调 sleep 时间来间接控制。

六、方案三:应用层分批删除

适合需要长期定期清理的场景,把删除逻辑写在定时任务中。

6.1 python 示例

import pymysql
import time
import logging
logging.basicconfig(level=logging.info, format='%(asctime)s %(message)s')
logger = logging.getlogger(__name__)
def batch_delete(conn_params, table, condition,
                 batch_size=10000, sleep_seconds=1.0, max_total=0):
    """分批删除历史数据
    args:
        conn_params: mysql 连接参数字典
        table: 表名
        condition: where 条件
        batch_size: 每批删除条数
        sleep_seconds: 批间暂停秒数
        max_total: 最多删除条数,0 为不限制
    """
    conn = pymysql.connect(**conn_params)
    cursor = conn.cursor()
    total_deleted = 0
    try:
        while true:
            sql = f"delete from {table} where {condition} limit {batch_size}"
            cursor.execute(sql)
            conn.commit()
            rows = cursor.rowcount
            if rows == 0:
                break
            total_deleted += rows
            logger.info(f"本批删除 {rows} 条,累计 {total_deleted} 条")
            if max_total > 0 and total_deleted >= max_total:
                logger.info(f"已达到 max_total={max_total},停止")
                break
            time.sleep(sleep_seconds)
    except exception as e:
        conn.rollback()
        logger.error(f"删除中断,已回滚最后一批。累计 {total_deleted} 条。错误: {e}")
        raise
    finally:
        cursor.close()
        conn.close()
    logger.info(f"删除完成,共删除 {total_deleted} 条记录")
    return total_deleted
# 调用示例
batch_delete(
    conn_params={
        'host': '127.0.0.1',
        'user': 'root',
        'password': 'your_password',
        'database': 'your_db',
    },
    table='orders',
    condition="created_at < date_sub(now(), interval 1 year)",
    batch_size=10000,
    sleep_seconds=1.0,
)

6.2 执行效果

2025-07-06 02:00:01 本批删除 10000 条,累计 10000 条
2025-07-06 02:00:02 本批删除 10000 条,累计 20000 条
2025-07-06 02:00:03 本批删除 10000 条,累计 30000 条
...
2025-07-06 04:35:12 本批删除 3200 条,累计 45823200 条
2025-07-06 04:35:12 删除完成,共删除 45823200 条记录

6.3 与定时任务集成

# crontab: 每天凌晨 3 点执行
# 0 3 * * * /usr/bin/python3 /opt/scripts/clean_old_orders.py
# 或者用 apscheduler 在应用内调度
from apscheduler.schedulers.blocking import blockingscheduler
scheduler = blockingscheduler()
@scheduler.scheduled_job('cron', hour=3, minute=0)
def nightly_cleanup():
    batch_delete(
        conn_params={
            'host': '127.0.0.1',
            'user': 'app',
            'password': '***',
            'database': 'prod',
        },
        table='orders',
        condition="created_at < date_sub(now(), interval 1 year)",
        batch_size=5000,
        sleep_seconds=2.0,
    )
scheduler.start()

七、方案四:分区表 drop partition(最快最干净)

如果你的表已经按时间做了分区,这是最推荐的方案——drop partition 是元数据操作,瞬间完成,不产生大量 binlog,不影响主从同步。

7.1 前提:表已按时间分区

-- 按月分区的表示例
create table orders (
    id bigint auto_increment,
    user_id bigint not null,
    amount decimal(10,2),
    created_at datetime not null,
    primary key (id, created_at),
    index idx_created_at (created_at)
) engine=innodb
partition by range (year(created_at) * 100 + month(created_at)) (
    partition p202301 values less than (202302),
    partition p202302 values less than (202303),
    -- ... 每月一个分区
    partition p202412 values less than (202501),
    partition p_future values less than maxvalue
);

7.2 删除操作:一条命令搞定

-- 删除 2023 年 1 月的分区(瞬间完成)
alter table orders drop partition p202301;

-- 删除多个分区
alter table orders drop partition p202301, p202302, p202303;

7.3 分区方案 vs 分批删除

维度drop partition分批 delete
执行速度毫秒级数小时
主从影响几乎无需控制节奏
空间回收自动释放需额外 optimize
前提条件表必须已分区无特殊要求
粒度按分区粒度(月/周)任意条件

注意: drop partition 不可逆。执行前务必确认分区范围正确,建议先 select count(*) from orders partition (p202301) 验证。

八、删除后:回收磁盘空间

不管用哪种 delete 方案,innodb 删除数据后不会自动释放磁盘空间(数据页标记为可复用,但不归还给操作系统)。如果需要真正释放空间:

8.1 方法一:optimize table

-- 简单直接,但会锁表(mysql 5.6+ 支持 online ddl,锁表时间大幅缩短)
optimize table orders;

8.2 方法二:alter table 重建

-- 等效于 optimize,通过重建表释放空间
alter table orders engine=innodb;

8.3 方法三:pt-online-schema-change(生产推荐)

# 不锁表重建,适合生产环境
pt-online-schema-change \
  --alter "engine=innodb" \
  d=your_db,t=orders \
  --execute

8.4 验证空间回收

-- 查看表占用的磁盘空间
select
    table_name,
    round(data_length / 1024 / 1024, 2) as data_mb,
    round(index_length / 1024 / 1024, 2) as index_mb,
    round(data_free / 1024 / 1024, 2) as free_mb
from information_schema.tables
where table_schema = 'your_db'
  and table_name = 'orders';

data_free 字段反映碎片空间。optimize 前后对比此值,即可确认空间是否回收成功。

九、常见问题与排障

问题原因解决方案
存储过程执行数小时没完待删数据量极大(数千万)属正常现象。通过监控 sql 确认进度,耐心等待
delimiter 语法报错navicat 查询窗口不支持改用「工具 -> 命令行界面」或外部 mysql 客户端
从库延迟告警飙升binlog 写入速度超过从库回放速度增大 sleep 时间至 2~5 秒;或改用 pt-archiver 的 --check-slave-lag
删除速度越来越慢越到后面符合条件的行越稀疏属正常现象,不影响最终结果
navicat 查询超时断开存储过程执行时间超过客户端超时限制连接设置中调大超时,或改用 nohup mysql ... & 后台执行
lock wait timeout exceeded其他事务持有了待删行的锁在低峰期执行;减小 batch_size;排查长事务
删除后表空间没变小innodb delete 不释放磁盘空间执行 optimize table 或 pt-online-schema-change 重建表
pt-archiver 报 no index foundwhere 条件字段没有索引先给条件字段加索引

十、总结与选型建议

决策总结

关键原则

  1. 永远不要一条 delete 删千万行数据——分批 + 暂停是铁律
  2. 操作前必须做安全检查——确认索引、确认数量、确认备份
  3. 低峰期执行——凌晨 2:00~6:00 是黄金窗口
  4. 删除后回收空间——optimize table 或 pt-online-schema-change
  5. 长期方案考虑分区表——如果经常需要按时间清理,建分区表是一劳永逸的方案

附录

术语表

术语含义
binlogmysql 二进制日志,记录所有数据变更,用于主从复制和数据恢复
undo log事务回滚日志,delete 时记录被删行的旧值
redo log事务持久化日志,确保崩溃恢复时数据不丢失
主从延迟从库回放 binlog 的速度落后于主库写入的速度
pt-archiverpercona toolkit 中的归档工具,支持分批删除和归档

以上就是mysql中存储过程大表分批删除历史数据的四种方案的详细内容,更多关于mysql大表分批删除历史数据的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com