本文目录导读:

在 PHP 中实现数据归档,通常是为了处理海量数据、优化数据库性能或满足法规要求,下面从策略、技术到代码实现,系统性地给你梳理一下。
归档策略选择
首先明确要归档什么、归档到哪里,以及归档后怎么查询。
| 策略 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 软删除(逻辑归档) | 数据量小,需频繁恢复 | 简单、可逆 | 查询性能下降,数据未真正减少 |
| 分区表归档 | 时间序列数据(日志、订单) | 删除分区极快,管理方便 | 数据库需支持分区(MySQL 8.0+/PG) |
| 历史表迁移 | 业务明确区分热/冷数据 | 主表保持轻量 | 查询需跨表,需处理关联 |
| 文件/对象存储导出 | 合规审计、长期保存 | 节省数据库空间 | 查询必须通过外部工具 |
推荐组合:生产库用 分区表 + 定期把老分区迁移到 归档库/对象存储,同时保留软删除标记用于业务逻辑。
核心实现方案
方案 1:基于分区表的自动归档(MySQL 8.0+)
// 1. 创建分区表(按月分区)
CREATE TABLE `orders` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`order_no` VARCHAR(32) NOT NULL,
`status` TINYINT NOT NULL DEFAULT '0',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`, `created_at`) // 注意:主键必须包含分区键
) PARTITION BY RANGE (YEAR(created_at) * 100 + MONTH(created_at)) (
PARTITION p202401 VALUES LESS THAN (202402),
PARTITION p202402 VALUES LESS THAN (202403),
PARTITION p202403 VALUES LESS THAN (202404)
// 后续可动态添加
);
// 2. 归档调度:每天凌晨3点执行
// 用 cron 执行:0 3 * * * php /path/to/archive.php
// 3. PHP 执行归档逻辑
<?php
class OrderArchiver {
private $pdo;
public function __construct() {
$this->pdo = new PDO(
'mysql:host=localhost;dbname=main_db;charset=utf8mb4',
'root', 'password',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
}
/**
* 归档 3 个月前的数据到归档库
*/
public function archiveOldData(): int {
$archiveMonth = date('Ym', strtotime('-3 months'));
$nextMonth = date('Ym', strtotime('-3 months +1 month'));
try {
// 开启事务
$this->pdo->beginTransaction();
// 创建归档库中的分区(如果不存在)
$this->createArchivePartitionIfNotExists($archiveMonth);
// 步骤1:从主表 SELECT 并 INSERT 到归档库
$sql = "INSERT INTO archive_db.orders (
id, order_no, status, created_at
) SELECT
id, order_no, status, created_at
FROM main_db.orders
WHERE created_at >= ? AND created_at < ?
LIMIT 10000"; // 分批处理防止内存溢出
$stmt = $this->pdo->prepare($sql);
$stmt->execute([
date('Y-01-01 00:00:00', strtotime("-$archiveMonth month")),
date('Y-m-01 00:00:00', strtotime("-$archiveMonth month +1 month"))
]);
$moved = $stmt->rowCount();
// 步骤2:删除主表中已经归档的数据(快速删除)
$deleteSql = "DELETE FROM main_db.orders
WHERE created_at < ?
LIMIT 10000"; // 关联相同条件
$deleteStmt = $this->pdo->prepare($deleteSql);
$deleteStmt->execute([
date('Y-m-01 00:00:00', strtotime('-3 months'))
]);
// 步骤3:手动 DROP 或 TRUNCATE 掉主表中该月分区(最快,推荐)
$this->dropPartition($archiveMonth);
$this->pdo->commit();
return $moved;
} catch (Exception $e) {
$this->pdo->rollBack();
throw $e;
}
}
private function createArchivePartitionIfNotExists($archiveMonth) {
$sql = "ALTER TABLE archive_db.orders ADD PARTITION (
PARTITION p_{$archiveMonth} VALUES LESS THAN (?)
)";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([$archiveMonth . '01']);
}
private function dropPartition($archiveMonth) {
$sql = "ALTER TABLE main_db.orders DROP PARTITION p_{$archiveMonth}";
$this->pdo->exec($sql);
}
}
// 执行
$archiver = new OrderArchiver();
$count = $archiver->archiveOldData();
echo "归档完成,移动 {$count} 条记录";
方案 2:业务触发式软删除 + 定期批量迁移
适用于业务逻辑需要保留记录,但主表数据量变大需要归档。
<?php
class DataArchiver {
private $db;
public function __construct(PDO $db) {
$this->db = $db;
$this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
/**
* 标记软删除(业务层面归档)
*/
public function softArchive(int $userId): bool {
$sql = "UPDATE users SET status = 2, archived_at = NOW() WHERE id = ?";
$stmt = $this->db->prepare($sql);
return $stmt->execute([$userId]);
}
/**
* 定时任务:将 archived_at 超过90天的数据物理迁移到 archive.users
*/
public function hardMigration(int $days = 90): int {
$threshold = date('Y-m-d H:i:s', strtotime("-{$days} days"));
$this->db->beginTransaction();
try {
// 1. 批量选取待归档数据
$fetchSql = "SELECT * FROM users WHERE archived_at < ? AND archived_at IS NOT NULL LIMIT 5000";
$fetchStmt = $this->db->prepare($fetchSql);
$fetchStmt->execute([$threshold]);
$rows = $fetchStmt->fetchAll(PDO::FETCH_ASSOC);
// 2. 插入归档库(假设 archive_users 表结构相同)
foreach ($rows as $row) {
$insertSql = "INSERT INTO archive.users (". implode(', ', array_keys($row)) .") VALUES (?, ?, ...)";
// 拼接参数绑定
$this->db->prepare($insertSql)->execute(array_values($row));
}
// 3. 删除原表数据(分批)
$deleteSql = "DELETE FROM users WHERE archived_at < ? AND archived_at IS NOT NULL LIMIT 5000";
$stmt = $this->db->prepare($deleteSql);
$stmt->execute([$threshold]);
$this->db->commit();
return count($rows);
} catch (Exception $e) {
$this->db->rollBack();
throw $e;
}
}
}
// 在业务代码中调用软归档
$archiver = new DataArchiver($pdo);
$archiver->softArchive(123);
// 在 cron 中每天调用硬迁移
// $moved = $archiver->hardMigration(180);
方案 3:导出到 CSV/JSON + 压缩存储(适合合规审计)
<?php
class FileArchiver {
private $db;
public function __construct(PDO $db) {
$this->db = $db;
}
/**
* 将超过一年的日志导出为 CSV 并压缩存储
*/
public function exportLogsToFile(int $olderThanDays = 365): string {
$threshold = date('Y-m-d 00:00:00', strtotime("-{$olderThanDays} days"));
$filePath = '/var/data/archive/' . date('Y-m-d') . '_logs.csv.gz';
// 打开 gzip 写入流
$fp = gzopen($filePath, 'w');
// 写入表头
fputcsv($fp, ['id', 'user_id', 'action', 'ip', 'created_at']);
// 分批查询写入
$offset = 0;
$batchSize = 1000;
while (true) {
$sql = "SELECT id, user_id, action, ip, created_at
FROM activity_logs
WHERE created_at < ?
LIMIT ?, ?";
$stmt = $this->db->prepare($sql);
$stmt->bindValue(1, $threshold);
$stmt->bindParam(2, $offset, PDO::PARAM_INT);
$stmt->bindParam(3, $batchSize, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($rows)) break;
foreach ($rows as $row) {
fputcsv($fp, $row);
}
$offset += $batchSize;
}
gzclose($fp);
// 删除原表数据
$deleteSql = "DELETE FROM activity_logs WHERE created_at < ?";
$stmt = $this->db->prepare($deleteSql);
$stmt->execute([$threshold]);
return $filePath;
}
}
最佳实践建议
-
分批处理:永远不要一次性 SELECT 全部数据再归档,用
LIMIT N循环处理。 -
索引优化:
- 归档条件字段(如
created_at)务必加索引 - 软删除字段加复合索引:
(status, archived_at)
- 归档条件字段(如
-
事务控制:数据迁移必须用事务,确保主库和归档库数据一致。
-
失败重试机制:在定时任务中加入日志记录,失败后自动重试或人工介入。
-
归档验证:迁移完成后执行
COUNT(*)对比,确保没有数据丢失。 -
备份保护:归档文件定期做异地备份(OSS/S3/磁带库)。
-
监控:观察归档任务执行时间、归档数据量、磁盘空间变化,设置告警。
扩展方案:归档到 ClickHouse 做分析
如果归档数据还需要进行大数据分析,可以将归档数据直接写入 ClickHouse:
class ClickHouseArchiver {
private $client;
public function __construct() {
// 使用 clickhouse-php-client
$this->client = new ClickHouseClient([
'host' => 'localhost',
'port' => 8123,
'database' => 'analytics'
]);
}
public function archiveOrdersToCH(int $months = 6): void {
// 从 MySQL 导出
$sql = "SELECT * FROM orders WHERE created_at < NOW() - INTERVAL {$months} MONTH";
$stmt = $this->pdo->query($sql);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 批量插入 ClickHouse
$this->client->insert('orders_archive', $rows);
// 删除 MySQL 中记录
$this->pdo->exec("DELETE FROM orders WHERE created_at < NOW() - INTERVAL {$months} MONTH");
}
}
用哪个方案取决于:
- 数据量:百万级用分区+迁移;千万级用分区+定期删除分区;海量需上分布式。
- 查询需求:需要在线查询历史数据就保留在归档库;只需审计就导出文件。
- 基础设施:是否有足够的磁盘/备份空间,是否用云数据库(RDS 自带归档功能)。
基本流程:软删除(业务标记) → 定期迁移(到归档库/文件) → 物理删除(清空主表)= 闭环归档。