PHP 怎么把冷数据导出

wen PHP项目 2

本文目录导读:

PHP 怎么把冷数据导出

  1. 方案一:使用生成器 + 数据库游标(最推荐)
  2. 方案二: MySQL 原生导出(最高性能)
  3. 方案三: 使用扩展库或工具(辅助)
  4. 方案四: 优化 PHP 配置 + 分批 + 后台任务

在 PHP 中处理冷数据(不常访问的历史数据)导出,核心目标是降低内存占用避免超时,以下是几种主流且高效的方案,按推荐程度排序:


使用生成器 + 数据库游标(最推荐)

这是 PHP 处理百万级数据导出的标准做法,通过 yield 逐行产出数据,配合数据库的 unbuffered query,内存占用恒定在几十 KB 左右。

<?php
/**
 * 流式导出冷数据到 CSV
 * 适用场景:数据量巨大(>10万行),普通服务器
 */
// 1. 核心:生成器函数,逐行从数据库拉取(不会一次性加载到内存)
function yieldRows(PDO $pdo, string $sql): Generator {
    // 关键:使用 PDO::MYSQL_ATTR_USE_BUFFERED_QUERY = false (MySQL)
    // 或 PostgreSQL 的 cursor(事务内使用)
    $stmt = $pdo->prepare($sql);
    $stmt->execute();
    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
        yield $row;           // 生成一行,用完即丢
    }
    $stmt->closeCursor();
}
// 2. 导出到文件(避免直接输出到浏览器导致内存炸)
function exportColdData(string $filePath, string $tableName): void {
    $pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', 'user', 'pass', [
        PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false,  // 关键配置:不缓冲查询结果
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    ]);
    // 打开文件句柄(写入模式)
    $fp = fopen($filePath, 'w');
    fputcsv($fp, ['ID', '姓名', '创建时间']);  // 表头
    // 使用生成器逐行遍历
    $gen = yieldRows($pdo, "SELECT id, name, created_at FROM {$tableName}");
    foreach ($gen as $row) {
        // 写入 CSV(每写一行都立即刷到磁盘)
        fputcsv($fp, $row);
    }
    fclose($fp);
    echo "导出完成,文件大小: " . round(filesize($filePath) / 1024 / 1024, 2) . " MB";
}
// 调用
exportColdData('/tmp/cold_data_2023.csv', 'orders_archive_2023');
?>

优点

  • 内存占用恒定(无论数据量多大)
  • 不会触发 PHP memory_limit 限制
  • 文件直接写磁盘,不占 PHP 进程内存

数据库差异

  • MySQL:必须在 PDO 连接时设置 PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false,且一次只能有一个活跃的 unbuffered 查询。
  • PostgreSQL:需要开启事务并使用游标(DECLARE cursor),或直接使用 COPY TO 命令(最快)。
  • SQLite:可以使用 sqlite3unbuffered 模式。

MySQL 原生导出(最高性能)

如果数据源是 MySQL/MariaDB,甚至可以不用 PHP 处理数据,直接调用 MySQL 的 SELECT ... INTO OUTFILE 导出服务器本地文件,PHP 只负责触发命令和提示下载。

<?php
// 使用 MySQL 原生导出(速度极快,几乎不占 PHP 内存)
$pdo = new PDO('mysql:host=localhost;dbname=archive', 'root', 'pass');
$pdo->exec("
    SELECT id, name, created_at
    INTO OUTFILE '/tmp/orders_export.csv'
    FIELDS TERMINATED BY ',' 
    OPTIONALLY ENCLOSED BY '\"' 
    LINES TERMINATED BY '\n'
    FROM orders_archive_2023
    WHERE created_at < '2022-01-01'
");
// PHP 把 /tmp/orders_export.csv 通过 HTTP 输出给用户
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename="export.csv"');
readfile('/tmp/orders_export.csv');   // readfile 是流式读取,不占内存
// 或者直接移动文件到下载目录
rename('/tmp/orders_export.csv', '/var/www/downloads/export_2023.csv');
?>

关键限制

  • MySQL 的 secure_file_priv 变量限制了可以写入的目录(默认可能是 NULL,需要 SHOW VARIABLES LIKE 'secure_file_priv'; 查看)。
  • 文件只能写到 MySQL 服务器所在的服务器上(如果是远程数据库,文件不在你的 Web 服务器上)。
  • 对 SQL 语法要求较高。

使用扩展库或工具(辅助)

如果不想手动处理游标,可以使用这些封装好的库:

  1. league/csv:处理 CSV 写入,支持 fputcsv 的流式操作,内置 Writer 对象,可以配合生成器使用。

  2. box/spout:一个 PHP 库,用于流式读取/写入 CSV、Excel (ODS/XLSX),特点是支持大文件,不需要 memory_limit

    use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
    $writer = WriterEntityFactory::createCSVWriter();
    $writer->openToFile('/tmp/output.csv');
    // 同样是流式处理,内部处理大文件
    foreach ($dataGenerator as $row) {
        $writer->addRow(WriterEntityFactory::createRowFromArray($row));
    }
    $writer->close();

优化 PHP 配置 + 分批 + 后台任务

如果上述方案都不适用(例如数据库不支持游标),可以退而求其次:

  1. 使用 LIMIT 分批查询(避免 OFFSET 深度翻页性能问题,用游标条件代替)。
  2. 结合 HTTP 404 重定向:让用户点击 3 次,每次导出 1/3 的数据。
  3. 将导出任务放入 Redis 队列,由 CLI 脚本(php cli.php)在后台执行,生成完文件后用户再下载。
// 分批查询的伪代码
$lastId = 0;
$fp = fopen('/tmp/export.csv', 'w');
do {
    $rows = $pdo->query("SELECT * FROM archive WHERE id > {$lastId} ORDER BY id LIMIT 10000")->fetchAll();
    if (empty($rows)) break;
    foreach ($rows as $row) {
        fputcsv($fp, $row);
        $lastId = $row['id'];   // 关键:用 id 代替 OFFSET
    }
    // 如果内存仍然高,可加 unset($rows);
    $continue = count($rows) == 10000;
} while ($continue);
fclose($fp);

场景 推荐方案 内存占用
有 MySQL,且数据量百万级 MySQL INTO OUTFILE 极低
有 MySQL,但远程或需要实时过滤 PDO unbuffered + 生成器 恒定
PostgreSQL 数据库 COPY TO 或 事务+游标 极低
数据在文件里(CSV/JSON)需要转换 SplFileObject 流式读取
必须使用共享主机(无法配置 PDO 选项) LIMIT + 游标轮询 可控制

关键注意事项

  1. 设置脚本超时set_time_limit(0);ignore_user_abort(true);
  2. 输出缓冲while (ob_get_level()) ob_end_clean(); 清掉之前的输出缓冲。
  3. 数据库连接:使用 PDOmysqli 的非缓冲查询时必须确保没有其他活跃查询。
  4. 最终文件下载:用 readfile() 代替 file_get_contents(),避免第二次占用内存。
  5. 文件锁定:如果导出文件是生成在 Web 目录,注意加 .htaccess 或权限,防止别人直接访问。

选择方案时,优先考虑方案一(生成器+游标),因为它最通用且可控,若数据库权限允许则直接使用方案二(原生导出)达到性能极致。

抱歉,评论功能暂时关闭!