在 ThinkPHP 中导出大数据时,直接从数据库中查询所有数据并一次性导出可能会导致内存占用过高,甚至导致服务器崩溃。为了解决这个问题,可以采取分批导出、队列和文件流等技术来优化大数据导出过程。
1. 分批查询并导出
分批查询并导出可以有效减少内存占用,每次只从数据库中查询一部分数据,处理并写入到导出文件中。以下是一个常见的处理流程,假设要导出 CSV 文件:
示例:分批导出 CSV
use think\facade\Db;
// 设置导出文件名
$fileName = 'export_' . date('YmdHis') . '.csv';
// 打开输出缓冲区并准备写入文件
$fp = fopen('php://output', 'w');
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename="' . $fileName . '"');
// 设置每页大小
$pageSize = 1000;
$totalCount = Db::name('your_table')->count();
$pageCount = ceil($totalCount / $pageSize);
// 写入 CSV 表头
fputcsv($fp, ['ID', 'Name', 'Age', 'Email']);
// 分页查询并导出
for ($page = 1; $page <= $pageCount; $page++) {
$dataList = Db::name('your_table')
->limit(($page - 1) * $pageSize, $pageSize)
->select();
// 写入 CSV 内容
foreach ($dataList as $row) {
fputcsv($fp, $row);
}
// 释放内存
unset($dataList);
}
// 关闭输出缓冲区
fclose($fp);
exit();
说明:
- 使用
fopen('php://output', 'w')
直接向浏览器输出文件内容。 - 使用分页查询,每次查询一部分数据并写入 CSV 文件。
fputcsv
函数用于将数据写入 CSV 文件。
2. 使用队列导出
对于非常大的数据量,可能需要异步处理导出任务。可以使用队列系统,将导出任务拆分成多个小任务,后台处理完后通知用户下载。
示例:使用队列导出 CSV
- 定义队列任务
// app\job\ExportJob.php
namespace app\job;
use think\queue\Job;
use think\facade\Db;
class ExportJob
{
public function fire(Job $job, $data)
{
$filePath = $data['file_path'];
$fp = fopen($filePath, 'a');
// 查询数据并写入文件
$pageSize = 1000;
$page = $data['page'];
$dataList = Db::name('your_table')
->limit(($page - 1) * $pageSize, $pageSize)
->select();
foreach ($dataList as $row) {
fputcsv($fp, $row);
}
fclose($fp);
unset($dataList);
// 如果还有任务,继续执行
if ($job->attempts() > 3) {
// 任务失败处理
$job->delete();
} else {
// 任务成功,删除任务
$job->delete();
if ($page < $data['total_pages']) {
// 继续下一个分页任务
$job->release();
}
}
}
}
- 推送任务到队列
use think\Queue;
$filePath = 'export_' . date('YmdHis') . '.csv';
$totalCount = Db::name('your_table')->count();
$pageSize = 1000;
$pageCount = ceil($totalCount / $pageSize);
// 初始化 CSV 文件并写入表头
$fp = fopen($filePath, 'w');
fputcsv($fp, ['ID', 'Name', 'Age', 'Email']);
fclose($fp);
// 将任务推送到队列
for ($page = 1; $page <= $pageCount; $page++) {
Queue::push('app\job\ExportJob', ['file_path' => $filePath, 'page' => $page, 'total_pages' => $pageCount], 'export');
}
- 下载导出的文件
当所有队列任务执行完毕后,用户可以下载生成的文件。你可以在控制器中提供下载链接:
public function download()
{
$filePath = 'export_' . date('YmdHis') . '.csv';
if (file_exists($filePath)) {
return response()->download($filePath);
} else {
return "File not found";
}
}
3. 使用临时文件分批写入
对于非常大的数据量,还可以将导出的文件分批写入多个临时文件,最后再合并这些文件。
示例:分批写入临时文件并合并
use think\facade\Db;
// 设置导出文件名
$fileName = 'export_' . date('YmdHis') . '.csv';
// 设置每页大小
$pageSize = 1000;
$totalCount = Db::name('your_table')->count();
$pageCount = ceil($totalCount / $pageSize);
// 打开最终的输出文件
$finalFp = fopen('php://output', 'w');
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename="' . $fileName . '"');
for ($page = 1; $page <= $pageCount; $page++) {
// 创建临时文件
$tempFile = tempnam(sys_get_temp_dir(), 'csv');
$tempFp = fopen($tempFile, 'w');
// 查询并写入临时文件
$dataList = Db::name('your_table')
->limit(($page - 1) * $pageSize, $pageSize)
->select();
// 写入 CSV 表头(仅第一页)
if ($page == 1) {
fputcsv($tempFp, ['ID', 'Name', 'Age', 'Email']);
}
foreach ($dataList as $row) {
fputcsv($tempFp, $row);
}
fclose($tempFp);
unset($dataList);
// 读取临时文件并写入最终输出文件
$tempFp = fopen($tempFile, 'r');
while ($line = fgets($tempFp)) {
fwrite($finalFp, $line);
}
fclose($tempFp);
unlink($tempFile); // 删除临时文件
}
fclose($finalFp);
exit();
4. 使用 Excel 或其它格式导出
除了 CSV 格式,你还可以使用 PHPExcel
或 PhpSpreadsheet
等库导出为 Excel 文件。对于大数据量,PhpSpreadsheet 提供了 setReadDataOnly
和 stream
方法来减少内存占用。
示例:使用 PhpSpreadsheet 导出 Excel
composer require phpoffice/phpspreadsheet
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use think\facade\Db;
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
// 设置表头
$sheet->fromArray([
['ID', 'Name', 'Age', 'Email'],
], null, 'A1');
// 分页查询并写入数据
$pageSize = 1000;
$totalCount = Db::name('your_table')->count();
$pageCount = ceil($totalCount / $pageSize);
$row = 2; // 从第二行开始写入数据
for ($page = 1; $page <= $pageCount; $page++) {
$dataList = Db::name('your_table')
->limit(($page - 1) * $pageSize, $pageSize)
->select();
$sheet->fromArray($dataList, null, 'A' . $row);
$row += count($dataList);
unset($dataList);
}
// 设置导出文件名
$fileName = 'export_' . date('YmdHis') . '.xlsx';
// 直接输出到浏览器
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment;filename="' . $fileName . '"');
$writer = new Xlsx($spreadsheet);
$writer->save('php://output');
exit();
总结
- 分批查询并导出:适合数据量较大但仍在可控范围内的情况。
- 队列导出:适合超大数据量,使用后台任务分批处理并导出。
- 临时文件分批写入:适合非常大的数据量,通过临时文件减少内存占用。
- 使用 PhpSpreadsheet 导出 Excel:适合需要导出复杂格式的情况。
根据你的数据量和需求,选择合适的导出方式可以有效提高导出效率并避免内存溢出问题。