当前位置:首页 > 文章列表 > 文章 > php教程 > PHP mysqli 批量导入 CSV 怎么控制内存:分批事务、参数绑定与失败回滚

PHP mysqli 批量导入 CSV 怎么控制内存:分批事务、参数绑定与失败回滚

来源:17golang原创 2026-07-27 13:26:54 0浏览 收藏

手里有一个 300 MB 的客户 CSV,最容易踩的坑不是 CSV 分隔符,而是把整份文件读进数组后才开始写库。PHP 进程会先吃掉一大块内存,写到一半又遇到一行格式错误,最后既不知道哪一批成功,也很难安全重跑。更稳的做法是让 `fgetcsv()` 一行一行产出记录,每 500 行组成一个事务,批次成功就提交,批次失败就回滚并记录行号。

批量导入的核心不是把 SQL 拼得更长,而是把“读取、写入、提交、复查”切成可控的小批次:单批 500 行只是起点,最终应按内存、锁等待和单批耗时调整。

要点速览

  • 用 `fgetcsv()` 流式读取,避免把 CSV 全部加载到 PHP 数组。
  • 预编译一条 `mysqli` INSERT,循环绑定每行参数,减少 SQL 拼接和类型混乱。
  • 每 500 行开启一次事务;批次内出错立即回滚,保留失败行号和错误文本。
  • 验收时同时核对导入计数、失败记录和数据库中的唯一键结果,不能只看脚本退出码。

先把 CSV 导入任务的边界定清楚

这个小工具只负责读取已经存在的 CSV,并把数据写入 `customer_import` 表。上传、权限和文件病毒扫描属于上游流程,不能混在一次导入里。示例文件有四列:`external_no`、`name`、`email`、`joined_on`;其中 `external_no` 建唯一索引,用它避免重复导入。

CREATE TABLE customer_import (
  id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  external_no VARCHAR(40) NOT NULL,
  name VARCHAR(80) NOT NULL,
  email VARCHAR(160) NOT NULL,
  joined_on DATE NOT NULL,
  UNIQUE KEY uk_customer_external_no (external_no)
);

运行前先确认 PHP 已启用 `mysqli`,MySQL 账号只有目标库的必要权限。不要拿生产文件直接试第一遍,先用几十行样本覆盖空值、中文、重复编号和日期格式错误。

PHP mysqli CSV 导入从 fgetcsv 读取、分成 500 行批次并写入 customer_import 的分层路径

用 fgetcsv 和 mysqli 组成 500 行小批次

下面的核心代码故意保持短:文件句柄一次只保留当前行,预编译语句只创建一次,事务边界围绕一个批次。`bind_param()` 的类型串是 `ssss`,因为本例把四列先按字符串接收,日期格式检查通过后再写入 MySQL。

set_charset('utf8mb4');
$db->report_mode = MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT;

$input = fopen(__DIR__ . '/customers.csv', 'rb');
$insert = $db->prepare(
    'INSERT INTO customer_import (external_no, name, email, joined_on)
     VALUES (?, ?, ?, ?)'
);

$batchSize = 500;
$batchRows = 0;
$totalRows = 0;
$failedRows = [];
$line = 1;

fgetcsv($input); // 跳过表头
$db->begin_transaction();

while (($row = fgetcsv($input)) !== false) {
    $line++;
    if (count($row) !== 4 || !check_date($row[3])) {
        $failedRows[] = ['line' => $line, 'reason' => '字段数量或日期格式不正确'];
        continue;
    }

    [$externalNo, $name, $email, $joinedOn] = array_map('trim', $row);
    $insert->bind_param('ssss', $externalNo, $name, $email, $joinedOn);
    $insert->send_long_data(0, $externalNo);
    $insert->store_result();
    $batchRows++;
    $totalRows++;

    if ($batchRows === $batchSize) {
        $db->commit();
        $batchRows = 0;
        $db->begin_transaction();
    }
}

if ($batchRows > 0) {
    $db->commit();
}

$insert->close();
fclose($input);
printf("imported=%d failed=%d\n", $totalRows, count($failedRows));

这里的 `store_result()` 只是让语句结果及时收束;真正的批次控制由 `begin_transaction()` 和 `commit()` 完成。示例里的 `check_date()` 应替换成项目已有的日期校验函数,并在函数内部明确使用 `Y-m-d` 规则。生产代码还应捕获数据库异常:任何一行触发唯一键冲突或连接异常,都要回滚当前批次,而不是继续写下一行。

失败时回滚当前批次,成功后留下可对账记录

批量导入最怕“半成功”。如果第 734 行有重复编号,而前 499 行已经写进数据库,下一次重跑就会出现一部分重复、一部分跳过的混乱状态。推荐把每批处理包在 `try/catch` 中:批次成功提交,异常则回滚,并把批次起止行和错误文本写到 `import_failures`。

$batchStart = 2;
try {
    $db->begin_transaction();
    // 读取并绑定本批次的 CSV 行
    // 每成功写入一行就递增 $batchRows
    $db->commit();
} catch (mysqli_sql_exception $error) {
    $db->rollback();
    error_log(json_encode([
        'batch_start' => $batchStart,
        'batch_rows' => $batchRows,
        'message' => $error->getMessage(),
    ], JSON_UNESCAPED_UNICODE));
    throw $error;
}

不要把 CSV 原文和数据库密码一起写进日志。日志只保留行号、外部编号的脱敏值、错误类型和批次编号即可。若业务允许“坏行跳过、好行继续”,就把单行校验错误和数据库异常分开:前者进入失败清单,后者回滚整批并终止任务。

PHP mysqli CSV 批次提交后在 MySQL 中核对 imported、failed 和唯一键结果的验收画面

本地运行后,按三组数字验收

准备一个包含 1,200 行数据的测试文件,其中故意放 3 行日期错误、2 行重复 `external_no`。验收不要只看页面上的“导入完成”,至少记录以下三组数字:

  • 读取行数:去掉表头后实际扫描了多少行。
  • 成功行数:事务提交后确实写入的数量。
  • 失败行数:格式失败、唯一键冲突和批次异常分别多少行。

成功行数加失败行数应等于读取行数;如果某批异常回滚,成功数不能把那一批已经尝试过的行算进去。数据库侧再执行 `SELECT COUNT(*)`,并检查 `uk_customer_external_no` 没有重复。导入脚本应输出批次号和耗时,方便比较 100、500、1000 行批次的差异。

常见问题:内存、重复导入和批次大小

为什么不建议先用 file 读取整个 CSV?

`file()` 会把每行都放进 PHP 数组,数据量变大时内存峰值很快超过 `memory_limit`。流式读取让内存主要由当前行、预编译语句和当前批次计数占用。

导入重跑会不会插入重复客户?

`external_no` 需要唯一索引。是否跳过重复、更新已有记录,应该由业务明确选择;不要依赖应用层先查询再插入来“猜测”唯一性。

500 行是不是固定最佳值?

不是。先用 100、500、1000 行做测试,观察单批耗时、锁等待、内存峰值和失败重试成本。事务越大,吞吐可能更高,但回滚代价也更大。

验收清单

真正接入定时任务前,先用测试库跑一遍:CSV 表头已校验;空字段和日期格式有明确错误;每批有提交或回滚;失败记录能定位到行号;数据库唯一键已建立;重新导入同一文件的结果符合预期;最终成功数、失败数和数据库行数能够对上。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go JSON Decoder 为什么读到 EOF:连续 JSON、空白与尾部脏数据怎么判Go JSON Decoder 为什么读到 EOF:连续 JSON、空白与尾部脏数据怎么判
上一篇
Go JSON Decoder 为什么读到 EOF:连续 JSON、空白与尾部脏数据怎么判
Go os.ReadDir 读取大目录怎么控内存:DirEntry、排序与错误边界
下一篇
Go os.ReadDir 读取大目录怎么控内存:DirEntry、排序与错误边界
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    97次使用
  • OpenCompass大模型评测体系详解:功能、使用指南与应用场景
    OpenCompass
    OpenCompass是上海AI实验室推出的开源大模型评测平台,提供CompassKit、CompassHub和CompassRank三大核心组件,支持LLM及多模态模型的一站式标准化评估与排行榜查询。
    28次使用
  • SuperCLUE中文大模型评测基准:功能、能力维度与应用指南
    SuperCLUE
    SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
    252次使用
  • C-Eval中文评测基准:大语言模型多学科能力评估指南
    C-Eval
    深入了解C-Eval中文评估套件,涵盖52个学科与4级难度。本文详解其功能特点、Zero-shot/Few-shot使用方法及代码示例,助您全面评测LLM中文理解与泛化能力。
    180次使用
  • AI Prompt Library:免费AI提示词库,助力ChatGPT高效创作与营销
    AI Prompt Library
    探索AI Prompt Library免费资源库,涵盖营销、写作及多场景AI提示词。兼容ChatGPT、Claude等工具,一键复制优化输出,提升工作效率。
    111次使用