本文目录导读:

- 最基础:LIMIT + OFFSET 循环(适合低频、后台脚本)
- 高效方案:基于主键游标(推荐,千万级表可用)
- 生产环境:队列 + 守护脚本(适合Crontab/Supervisor)
- 极致性能:按物理分区(MySQL Partition)
- 时间-数量双重控制(超时保护)
- 常见注意事项
- 选择建议
在PHP项目中清理历史数据时,分批执行是避免内存溢出、数据库锁死或超时的关键策略,以下是几种常见且可靠的分批执行方案,按场景和复杂度排序:
最基础:LIMIT + OFFSET 循环(适合低频、后台脚本)
<?php
// 每批处理 1000 条
$batchSize = 1000;
$offset = 0;
do {
// 1. 查询需要删除的ID(避免直接删除时锁定大量行)
$stmt = $pdo->prepare("SELECT id FROM logs WHERE created_at < :cutoff ORDER BY id LIMIT :limit OFFSET :offset");
$stmt->execute([
':cutoff' => '2024-01-01 00:00:00',
':limit' => $batchSize,
':offset' => $offset
]);
$ids = $stmt->fetchAll(PDO::FETCH_COLUMN);
if (empty($ids)) {
break;
}
// 2. 一次性删除这批ID
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$pdo->prepare("DELETE FROM logs WHERE id IN ($placeholders)")->execute($ids);
$offset += $batchSize;
// 3. 可选:释放内存、记录进度
echo "已删除至偏移量: $offset\n";
gc_collect_cycles(); // 强制内存回收
usleep(50000); // 每批暂停50ms,降低数据库压力
} while (true);
缺点:大表时 OFFSET 越往后越慢(需扫描已跳过的行)。
高效方案:基于主键游标(推荐,千万级表可用)
<?php
$batchSize = 1000;
$lastId = 0; // 从最小ID开始
$cutoffDate = '2024-01-01 00:00:00';
do {
// 只查主键,且按ID升序取N条
$ids = $pdo->prepare("SELECT id FROM logs
WHERE id > :lastId AND created_at < :cutoff
ORDER BY id
LIMIT :limit");
$ids->execute([
':lastId' => $lastId,
':cutoff' => $cutoffDate,
':limit' => $batchSize
]);
$ids = $ids->fetchAll(PDO::FETCH_COLUMN);
if (empty($ids)) {
break;
}
// 删除这批数据
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$pdo->prepare("DELETE FROM logs WHERE id IN ($placeholders)")->execute($ids);
// 更新游标
$lastId = end($ids);
echo "已处理至ID: $lastId\n";
gc_collect_cycles();
} while (true);
优点:
- 避免 OFFSET,利用索引(
(id, created_at)复合索引更佳) - 每批之间无重复扫描,性能稳定
生产环境:队列 + 守护脚本(适合Crontab/Supervisor)
1 分批写入任务表
<?php
// create_cleanup_tasks.php (每小时或每日执行一次)
$batchSize = 5000;
$lastId = 0;
// 找出所有需要删除的ID范围,生成任务
do {
$ids = $pdo->prepare("SELECT id FROM logs
WHERE id > :lastId AND created_at < :cutoff
ORDER BY id LIMIT :limit");
// ... 同上获取 $ids
if (empty($ids)) break;
// 将批次ID写入任务表(或Redis队列)
$taskData = json_encode($ids);
$pdo->prepare("INSERT INTO cleanup_tasks (data, created_at) VALUES (?, NOW())")
->execute([$taskData]);
$lastId = end($ids);
} while (true);
2 消费者逐批执行
<?php
// worker.php (由 Supervisor 持续运行,或 Crontab 每分钟执行)
$pdo->beginTransaction();
$task = $pdo->prepare("SELECT id, data FROM cleanup_tasks WHERE processed = 0 LIMIT 1 FOR UPDATE");
$task->execute();
$row = $task->fetch();
if (!$row) {
exit("暂无待清理任务\n");
}
$ids = json_decode($row['data'], true);
try {
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$pdo->prepare("DELETE FROM logs WHERE id IN ($placeholders)")->execute($ids);
$pdo->prepare("UPDATE cleanup_tasks SET processed = 1 WHERE id = ?")->execute([$row['id']]);
$pdo->commit();
echo "已删除 " . count($ids) . " 条\n";
} catch (Exception $e) {
$pdo->rollBack();
// 记录错误,可选重试
}
极致性能:按物理分区(MySQL Partition)
如果表按时间分区(如按月),一个SQL即可清理整个分区:
ALTER TABLE logs DROP PARTITION p202401, p202402; -- 或用 TRUNCATE PARTITION ALTER TABLE logs TRUNCATE PARTITION p202401;
配合PHP:
<?php
// 获取要清除的分区列表
$partitions = $pdo->query("SELECT PARTITION_NAME FROM INFORMATION_SCHEMA.PARTITIONS
WHERE TABLE_NAME = 'logs' AND PARTITION_DESCRIPTION < 20240101");
foreach ($partitions as $row) {
$pdo->exec("ALTER TABLE logs DROP PARTITION {$row['PARTITION_NAME']}");
}
优点:瞬间清理,不影响在线业务。
时间-数量双重控制(超时保护)
<?php
$maxExecutionTime = 58; // 预留2秒退出
$batchSize = 1000;
$startTime = time();
$lastId = 0;
do {
if (time() - $startTime >= $maxExecutionTime) {
echo "达到最大执行时间,暂停并记录断点...\n";
// 可选:将 $lastId 存入 Redis 或文件,下次从此继续
file_put_contents('/tmp/cleanup_cursor.txt', $lastId);
break;
}
// ... 游标方式获取 $ids 并删除 ...
} while (!empty($ids));
适用场景:Crontab 每隔几分钟执行,防止单次运行时间过长。
常见注意事项
| 问题 | 解决方案 |
|---|---|
| 大事务 | 每批单独提交(pdo->commit() / 不开启显式事务) |
| 锁冲突 | 使用 DELETE ... WHERE id IN (...) 而非 DELETE ... WHERE created_at < ...(避免全表锁) |
| 从库延迟 | 每批后 usleep(200000)(0.2秒)或检查主从延迟 SHOW SLAVE STATUS |
| 慢查询 | 确保 WHERE 条件有索引,如 (created_at, id) 复合索引 |
| 内存泄漏 | 每批后 unset($ids) + gc_collect_cycles() |
| 断点续传 | 将游标存入 Redis / MySQL 状态表 / 文件 |
选择建议
| 数据量 | 方案 |
|---|---|
| < 10万 | 方案1(简单直接) |
| 10万 ~ 500万 | 方案2(游标,最常用) |
| 500万 ~ 1亿 | 方案3(队列异步)或方案4(分区) |
| 实时业务 | 方案4(分区无锁)或方案2+LOW_PRIORITY |
推荐默认使用方案2(基于主键游标),它是性能、复杂度、安全性的最佳平衡点。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播