如何在 PHP 中遍历嵌套 JSON 数据并批量插入 MySQL 数据库
前言接口返回的订单 JSON 往往长成这样最外层是一个数组每个订单里又有items数组items里可能还嵌着options。很多人第一次处理它的写法是两层foreach直接拼 SQL跑小样例没问题一上真实数据就出三种症状字段整体错位或大量NULL、脚本跑几分钟后超时、MySQL server has gone away。这些症状的根因其实只有两个。第一JSON 是树MySQL 表是二维表中间缺了一层「展平」flatten的映射逻辑层级没对齐就会错位。第二展平之后如果用「一行一条INSERT」的方式写库每条语句都要走一次网络往返每条语句都在自己的隐式事务里提交磁盘刷盘次数被放大慢得毫不意外。本文用一个完整可运行的程序演示递归遍历任意深度的嵌套 JSON把叶子节点展平成行再用「预编译 多值VALUES 显式事务 分块」的方式批量写入MySQL。示例代码最低要求PHP 7.4推荐PHP 8.1 及以上数据库驱动使用PDOPHP Data Objects的pdo_mysql扩展。一、先把 JSON 的层级和表的行对应起来先把数据摆清楚。假设 JSON 长这样{ orders: [ { id: 1001, user: alice, items: [ {sku: A-1, qty: 2, price: 19.9, options: [{k: color, v: red}]}, {sku: B-2, qty: 1, price: 9.5, options: []} ] }, { id: 1002, user: bob, items: [ {sku: C-3, qty: 5, price: 3.0, options: [{k: size, v: XL}]} ] } ] }orders是一对多items又是一对多。如果把options也塞进同一张表一张order_items表就同时承载了两个层级的语义后面查询会很难受。正确做法是一层一张表orders主表、order_items子表、item_options孙表用外键串起来。所以遍历的目标不是「把 JSON 塞进一个循环」而是「每一层各自产出一批行」。这也决定了遍历函数的形状它不是一次性递归到底而是分层产出。JSON 层级目标表行数关系关联键orders[]orders1 个对象 1 行主键idorders[].items[]order_items1 个元素 1 行外键order_idorders[].items[].options[]item_options1 个元素 1 行外键item_id二、遍历显式分层而不是无限递归如果 JSON 结构是固定的大多数业务接口都是最稳的写法是显式分层循环而不是写一个「自动递归展开」的通用函数。原因很实际通用递归函数一旦遇到某一层出现null而另一层是空数组你很难控制它产出什么。?php declare(strict_types1); /** * 把嵌套的订单 JSON 展平成三层数据。 * * return array{orders: array, items: array, options: array} */ function flattenOrders(array $orders): array { $out [orders [], items [], options []]; foreach ($orders as $order) { $orderId (int) ($order[id] ?? 0); if ($orderId 0) { continue; // 没有主键的订单直接跳过否则子表会插入悬挂外键 } $out[orders][] [ id $orderId, user (string) ($order[user] ?? ), ]; foreach ($order[items] ?? [] as $item) { $sku (string) ($item[sku] ?? ); if ($sku ) { continue; } // 子表用「订单号 SKU」做业务主键避免依赖自增 ID $itemKey $orderId . : . $sku; $out[items][] [ item_key $itemKey, order_id $orderId, sku $sku, qty (int) ($item[qty] ?? 0), price number_format((float) ($item[price] ?? 0), 2, ., ), ]; foreach ($item[options] ?? [] as $k $opt) { $out[options][] [ item_key $itemKey, name (string) ($opt[k] ?? ), value (string) ($opt[v] ?? ), seq (int) $k, ]; } } } return $out; }注意几个刻意的选择用?? []兜底而不是直接foreach接口偶尔会返回null而不是数组直接foreach在 PHP 8 下会抛TypeErrorPHP 7 下是Warning加不执行。子表用业务主键item_key这样同一批数据重跑不会产生重复行配合INSERT ... ON DUPLICATE KEY UPDATE天然幂等。number_format(..., 2, ., )把浮点价格转成字符串再入库。直接传float给DECIMAL字段在某些驱动配置下会出现精度截断。三、批量插入真正让速度差几十倍的是这三件事展平之后写入慢不是 PHP 慢是语句条数太多。批量插入的核心就三点多值VALUESINSERT INTO t (a,b) VALUES (?,?),(?,?),(?,?)把 N 条语句压成 1 条语句。ATTR_EMULATE_PREPARES false让PDO用 MySQL 的原生预编译协议而不是在客户端把参数拼成字符串。开启模拟预处理时中文、二进制、超长文本都更容易踩到转义问题。显式事务把整批插入包在一个BEGIN ... COMMIT里。不开事务时每条语句都是一个独立事务每次都要fsync日志。?php declare(strict_types1); function connect(string $dsn, string $user, string $pass): PDO { return new PDO($dsn, $user, $pass, [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES false, ]); } /** * 分块批量 upsert。 * * param array $rows 每行必须是键名一致的关联数组 */ function bulkUpsert(PDO $pdo, string $table, array $rows, int $chunk 500): int { if ($rows []) { return 0; } $cols array_keys($rows[0]); $colList . implode(,, $cols) . ; $oneRow ( . implode(,, array_fill(0, count($cols), ?)) . ); $affected 0; $pdo-beginTransaction(); try { foreach (array_chunk($rows, $chunk) as $part) { $sql INSERT INTO $table ($colList) VALUES . implode(,, array_fill(0, count($part), $oneRow)) . AS new ON DUPLICATE KEY UPDATE . implode(,, array_map( static fn(string $c): string $c new.$c, $cols )); // 关键必须手动按顺序展开不能用 array_merge $params []; foreach ($part as $row) { foreach ($row as $value) { $params[] $value; } } $stmt $pdo-prepare($sql); $stmt-execute($params); $affected $stmt-rowCount(); } $pdo-commit(); } catch (Throwable $e) { $pdo-rollBack(); throw $e; } return $affected; }AS new这种行别名写法要求MySQL 8.0.19 及以上。如果你的 MySQL 是 5.7把它换成老式的ON DUPLICATE KEY UPDATE col VALUES(col)只是后者在 8.0.20 之后会给出弃用警告。四、完整可运行示例把下面这段存成import_orders.php准备好建表语句和orders.json直接php import_orders.php即可。CREATE TABLE orders ( id BIGINT UNSIGNED NOT NULL, user VARCHAR(64) NOT NULL DEFAULT , PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order_items ( item_key VARCHAR(128) NOT NULL, order_id BIGINT UNSIGNED NOT NULL, sku VARCHAR(64) NOT NULL, qty INT UNSIGNED NOT NULL DEFAULT 0, price DECIMAL(10,2) NOT NULL DEFAULT 0.00, PRIMARY KEY (item_key), KEY idx_order (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE item_options ( item_key VARCHAR(128) NOT NULL, seq INT UNSIGNED NOT NULL, name VARCHAR(64) NOT NULL, value VARCHAR(255) NOT NULL, PRIMARY KEY (item_key, seq) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;?php declare(strict_types1); // 最低 PHP 7.4推荐 PHP 8.1 // ---------- 1. 读取并解析 ---------- $file __DIR__ . /orders.json; $raw file_get_contents($file); if ($raw false) { exit(无法读取 $file\n); } try { // JSON_THROW_ON_ERROR 需要 PHP 7.3 $data json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $e) { exit(JSON 解析失败 . $e-getMessage() . \n); } $orders $data[orders] ?? []; printf(解析到 %d 个订单\n, count($orders)); // ---------- 2. 展平 ---------- $flat flattenOrders($orders); printf( 展平结果orders%d, items%d, options%d\n, count($flat[orders]), count($flat[items]), count($flat[options]) ); // ---------- 3. 写库 ---------- $dsn mysql:host127.0.0.1;port3306;dbnameshop;charsetutf8mb4; $user app; $pass secret; $pdo connect($dsn, $user, $pass); try { $n1 bulkUpsert($pdo, orders, $flat[orders]); $n2 bulkUpsert($pdo, order_items, $flat[items]); $n3 bulkUpsert($pdo, item_options, $flat[options]); printf(写入完成orders%d, items%d, options%d\n, $n1, $n2, $n3); } catch (Throwable $e) { exit(写库失败 . $e-getMessage() . \n); } // ---------- 4. 抽样校验行数对不对 ---------- $expected count($flat[options]); $actual (int) $pdo-query(SELECT COUNT(*) FROM item_options)-fetchColumn(); printf(item_options 期望 %d 行实际 %d 行%s\n, $expected, $actual, $expected $actual ? OK : 不一致需排查); function flattenOrders(array $orders): array { /* 见上文第二节 */ } function connect(string $dsn, string $user, string $pass): PDO { /* 见上文第三节 */ } function bulkUpsert(PDO $pdo, string $table, array $rows, int $chunk 500): int { /* 见上文第三节 */ }最后这一步「抽样校验」非常重要批量插入最容易出的事故不是报错而是没报错但行数少了几百条。每次导入后都比对一下源数据与目标表的行数问题能早一个环节暴露。常见坑点1.json_decode的第二个参数忘了传true❌$data json_decode($raw);之后写$data[orders]得到Cannot use object of type stdClass as array。 ✅$data json_decode($raw, true, 512, JSON_THROW_ON_ERROR);直接拿关联数组同时把解析失败变成异常而不是null。2. 拼接参数时用了array_merge展开行数据❌$stmt-execute(array_merge(...$part));—— 行的键名是字符串array_merge遇到重复的字符串键会覆盖最后只剩最后一行且参数个数与占位符对不上。 ✅ 手动双层foreach按顺序压平成一维数组如上面的bulkUpsert。3.isset()和array_key_exists()混用❌if (isset($item[qty])) { ... }—— 当字段存在但值为null时判断为假于是业务上「明确设为 0」和「字段缺失」被当成一回事。 ✅ 需要区分时用array_key_exists(qty, $item)只做取值兜底时才用||或??。4. 超大整数被转成浮点❌ 雪花 ID7300000000000001234经json_decode后变成7.3E18写库直接溢出。 ✅ 加JSON_BIGINT_AS_STRING标志json_decode($raw, true, 512, JSON_THROW_ON_ERROR | JSON_BIGINT_AS_STRING)再在绑定前转成字符串交给PDO。5. 一层塞十万条撞上max_allowed_packet❌ 把整批数据拼成一条VALUES报MySQL server has gone away或Packet too large。 ✅ 分块示例里是 500 行并确认max_allowed_packet与服务端一致行宽越大块要越小。6. 事务里捕获了异常却没有回滚❌try { ... commit(); } catch (Exception $e) { log($e); }—— 事务没结束就继续往下跑连接被放回连接池后状态是脏的后续操作行为诡异。 ✅catch (Throwable $e) { $pdo-rollBack(); throw $e; }回滚后再向上抛别吞异常。7. 递归深度超过json_decode的默认上限❌ 深层嵌套超过 512 层时json_decode静默返回null而json_last_error()又容易被人忽略。 ✅ 显式传 depth并配合JSON_THROW_ON_ERROR让失败可见结构真的那么深说明该换数据格式了。8. 每条记录都重新new PDO❌ 在循环体内建立连接 —— 建连接的成本远高于插入本身。 ✅ 连接只建一次循环里只做prepare/execute。总结环节做法解决的问题结构映射一层 JSON 层级对应一张表字段错位、语义混乱遍历显式分层循环 ?? []兜底TypeError、空值穿透主键用业务键如订单号:SKU重跑产生重复行写入多值VALUES 原生预编译 显式事务网络往返与刷盘开销分块每块几百行按行宽调整max_allowed_packet校验导入后比对源与目标行数静默丢数据嵌套 JSON 入库的难点从来不在「怎么遍历」而在于把树形结构诚实地映射成关系模型再让写入的语句条数从「行数级」降到「块数级」。把展平逻辑写成可读的显式循环再套上一个会回滚的事务和一次行数核对这套流程就能稳稳地跑在生产上。

相关新闻

基于知识图谱的协同过滤推荐:学习资源推荐系统源码解析

基于知识图谱的协同过滤推荐:学习资源推荐系统源码解析

简介:这是一份面向计算机相关专业学生与从业者的个性化学习资源推荐系统完整源码,核心采用知识图谱与协同过滤相结合的方式,解决传统推荐可解释性与冷启动不足的问题。项目覆盖学习行为分析、多维用户画像构建、知识图谱嵌入、推荐模型训练及…

2026/10/4 6:46:47 阅读更多 →
Apache SeaTunnel 同步 HTTP 接口到 Doris:502 排查与连接复用优化实战

Apache SeaTunnel 同步 HTTP 接口到 Doris:502 排查与连接复用优化实战

做数据同步这么多年,我一直觉得 HTTP 接口是最"鸡肋"的数据源——说它难吧,无非是发个请求解析 JSON;说它简单吧,等你在生产环境跑上一周,各种超时、502、连接耗尽接踵而至。最近用 Apache SeaTunnel 接了一…

2026/10/4 6:45:19 阅读更多 →
上市公司战新产业面板数据:从zip解压到规范化长表全流程

上市公司战新产业面板数据:从zip解压到规范化长表全流程

简介:覆盖2000—2023年上市公司战略性新兴产业企业面板数据,面向经济学、金融学及产业研究方向的研究者、分析师和数据爱好者,可用来识别战略新兴产业企业,观察其经营表现、成长能力与产业格局演变,也适用于硕博论文初…

2026/10/3 4:21:18 阅读更多 →

最新新闻

AI Native团队实战手册:Agent落地的四大断层与重建

AI Native团队实战手册:Agent落地的四大断层与重建

1. 这不是一本“手册”,而是一份AI Native团队的生存实录“AI Native 团队完整开发落地手册”——看到这个标题,我第一反应不是去翻目录,而是下意识摸了摸自己电脑里那个叫/projects/ai-native-2024-q3的文件夹。里面躺着7个被砍掉的POC、3次…

2026/10/4 6:46:36 阅读更多 →
长沙曾食坊小吃培训的县域市场:开店选品怎么想

长沙曾食坊小吃培训的县域市场:开店选品怎么想

本篇要点:- 县域客群与价位带:熟人社会的消费特征;- 品类不宜多:食材可得性与采购限制;- 赶集日与平时的落差:备货节奏怎么调。县域市场开小吃店,和地级市逻辑不同:客群熟人多、靠口…

2026/10/4 6:46:36 阅读更多 →
个人RAG知识库进阶:版本治理、父子分块、混合检索与可引用回答

个人RAG知识库进阶:版本治理、父子分块、混合检索与可引用回答

1. 从"能问答"到"敢引用":个人知识库真正的分水岭很多人搭个人 RAG 知识库,第一步就卡在"把 PDF 丢进去、能聊起来"这个层面。跑通一个 demo 确实不难:切块、向量化、检索、拼进 prompt,半小时能出…

2026/10/4 6:46:36 阅读更多 →
长沙曾食坊小吃培训的汤包与煎饺:早餐面点怎么标准化

长沙曾食坊小吃培训的汤包与煎饺:早餐面点怎么标准化

本篇要点: 1. 皮冻做法与比例;2. 面皮擀制与褶数;3. 蒸煎火候区分。汤包和煎饺看着都是面点,标准化却各有一套。本文补的是早餐面点在"可复制"上的那一层:从皮冻怎么熬、面皮怎么擀,到蒸与煎的火…

2026/10/4 6:46:36 阅读更多 →
AI应用架构图怎么画:分层、Agent、数据流与并发设计实战

AI应用架构图怎么画:分层、Agent、数据流与并发设计实战

开头前几周我帮一个团队评审他们的AI客服项目,团队负责人打开PPT,里面放了一张架构图——说真的,那张图我看了十分钟都没看明白。箭头从数据库直接画到大模型接口,中间夹着两个不知道干什么的微服务,缓存和消息队列全堆…

2026/10/4 6:46:36 阅读更多 →
SpringBoot启动慢得像蜗牛?原来是这个配置在捣鬼

SpringBoot启动慢得像蜗牛?原来是这个配置在捣鬼

上周三凌晨,我们的订单服务在预发环境启动耗时突然从15秒飙升到2分钟——而代码和依赖压根没改!这种诡异的性能劣化就像代码里藏了一只蜗牛,逼得我不得不翻开SpringBoot的黑匣子。 一、症状:启动时间为何突然暴涨? 现…

2026/10/4 6:45:35 阅读更多 →

日新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/2 10:36:31 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/3 9:42:35 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/3 9:42:36 阅读更多 →