ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Navicat 导出 11GB SQL 导入 SQL Server 踩坑实录:跨行 INSERT、GO 分隔与整批静默丢失之谜

2026/8/28 17:45:46 拓冰建站 浏览量
Navicat 导出 11GB SQL 导入 SQL Server 踩坑实录:跨行 INSERT、GO 分隔与整批静默丢失之谜 Navicat 导出 11GB SQL 导入 SQL Server 踩坑实录跨行 INSERT、GO 分隔与整批静默丢失之谜数据迁移场景把 Navicat 导出的 11.6GB SQL 文件284 万条 INSERT导入本地 SQL Server。导入完成后一核对行数发现比源文件少了4,692 条——没有任何报错日志一片安静。本文记录完整的排查过程与三个真正的根因。## 一、问题现象按 5,000 条/片分片导入脚本显示命中 723,401 条、全部执行成功但数据库里实际只有718,709 条——差了 4,692 条。最诡异的是执行过程没有任何报错脚本只在出现 Msg 错误时才打印数据就这么静默少了。按月份一拆缺口集中在三个特殊月份业务里的 13/14 月调整记录| 月份 | 源文件 | 库内 | 差额 || ------ | ------ | ------ | ---------- || 202413 | 21,470 | 18,140 |-3,330|| 202414 | 20,325 | 20,128 | -197 || 202513 | 21,696 | 20,531 |-1,165|| 其他月份 | 全部一致 | 全部一致 | 0 |## 二、排查过程### 第一步行数对比锁定范围源文件 grep 出各月 INSERT 数与库里GROUP BY对比快速定位到缺的月份。这一步把问题从全局缩小到三个特殊月份。### 第二步手动执行单条 INSERT看真实报错从源文件抽一条缺失的 INSERT包上SET IDENTITY_INSERT ON单独执行sqlSET IDENTITY_INSERT [dbo].[xxx] ON;GOINSERT INTO [dbo].[xxx] (...) VALUES (...);GOSET IDENTITY_INSERT [dbo].[xxx] OFF;GO结果单独执行能成功。那问题一定出在批量执行上。### 第三步分片执行追踪每批的入库行数把 21,470 条按 5,000/批分成 5 批执行每批执行后查累计入库数batch 1: lines5000 errors0 cumulative_rows5000batch 2: lines5000 errors0 cumulative_rows10000batch 3: lines5000 errors3 cumulative_rows10000 ← 停了batch 4: lines5000 errors0 cumulative_rows10000batch 3 有 3 条语法错误但整批 5,000 条一条都没进。查错误详情Msg 102, Level 15: Incorrect syntax near 4106Msg 105, Level 15: Unclosed quotation mark before the character string破案方向出现了Level 15 语法错误会导致整个 batch 解析失败而 sqlcmd 默认不中断没有-b参数时也不报行数受影响于是整批静默丢弃。## 三、三个真正的根因### 根因 1Navicat 导出格式 INSERT GO 双行结构Navicat 导出的 SQL 不是一段 INSERT 一个 batch而是每条 INSERT 后面跟一行 GOsqlINSERT INTO [dbo].[xxx] (...) VALUES (...);GOINSERT INTO [dbo].[xxx] (...) VALUES (...);GO如果你用-b模式把整段塞进去或者测试时把 5,000 条 INSERT 拼成一个大 batch 执行某一行出错整个 batch 全部作废——不是只丢那一条### 根因 2字段值含换行符 → INSERT 跨多行 → 截断后整批炸这才是真正的深坑。Navicat 导出时如果某个字段值比如备注里含有换行符这条 INSERT 在物理上会跨多行sqlINSERT INTO [dbo].[xxx] VALUES (N1, N张三, N第一行备注第二行备注, N202412);GO按一行 一条 INSERT提取时这条被切成两半变成sqlINSERT INTO [dbo].[xxx] VALUES (N1, N张三, N第一行备注SQL Server 解析到一半报Msg 102: Incorrect syntaxLevel 15——语法错误会终止整个 batch剩下几千条全部丢弃。这就是一条坏行毁掉 5,000 条的完整链条。验证方法统计文件物理行数 vs INSERT 条数。如果总行数 ≈ 3×INSERT 数INSERT 行 GO 行 空行但有 10% 的偏差多半有跨行 INSERT。### 根因 3NULL 占位导致字段错位另一个隐蔽问题按第 N 个N...提取判断字段时如果前面的字段是 NULL会被跳过导致取错字段。正确做法是把 NULL 也当作一个占位 token 计数python# 错误只数 N...NULL 会导致错位tokens re.findall(rN[^]*, line)# 正确N... 和 NULL 都算一个 tokentokens re.findall(rN[^]*|NULL, line)当时我用错误逻辑统计某个月份漏了 3,175 条 2024 年的数据——判断字段提取错了导入自然是错的。## 四、最终解决方案1.按\nGO\n边界提取跨行的 INSERT 内容累积进同一条再验证每条以)闭合python GO_MARK b\nGO\n go_pos data.find(GO_MARK, idx) # 找 GO 边界而不是第一个 \n line data[idx:go_pos] # 跨行内容完整保留2.5,000 条/片每条 INSERT 保留源文件的 GO 分隔包SET IDENTITY_INSERT ON/OFF执行3. 每片执行后统计错误数 查库确认行数增长而不是只靠没有报错判断成功## 五、数据校验方法论比导入更值得收藏导入完必须验证四步走| 步骤 | 方法 | 抓什么问题 || ------- | -------------------------------- | ------- || ① 行数对比 | 源文件 grep 计数 vs 库COUNT(*)| 丢行/多行 || ② 重复检测 |GROUP BY 主键 HAVING COUNT(*)1| 重复导入 || ③ 抽样核对 | 随机抽 2-3 条源 INSERT vs 库记录逐字段比对 | 字段错位/截断 || ④ 异常值检查 | 关键字段 NULL 率、格式校验如月份字段必须是 6 位数字 | 提取逻辑错误 |当时用这套方法还发现hqcwgzsjb 有 2,225 条重复数据首轮导入残留 重导全量用一条 SQL 去重sqlWITH cte AS ( SELECT x_xh, ROW_NUMBER() OVER (PARTITION BY x_xh ORDER BY (SELECT NULL)) AS rn FROM xxx)DELETE FROM cte WHERE rn 1;## 六、总结| 坑 | 现象 | 解法 || ---------------- | ------------------------ | ------------------------- || INSERT GO 双行结构 | 大 batch 一条出错全丢 | 保留 GO 逐条分隔 || 字段值含换行符 | INSERT 跨行被截断Msg 102 毁整批 | 按\nGO\n边界提取验证)闭合 || NULL 占位 | 取错字段漏导/误导数据 |N...\|NULL双 token 计数 || sqlcmd 静默失败 | 没报错但数据少了 | 每片执行后查库行数 |### 三条核心经验1.“没报错” ≠ “成功了”——批量导入必须逐片核对行数增长2.一条语法错误会毁掉整个 batch——大数据导入宁可小片多跑别图快拼大 batch3.提取/校验逻辑要一致——统计用什么逻辑导入就用什么逻辑两边不一致必出鬼容器小贴士容器里跑 SQL Server 的话大文件删除要用docker exec -u root容器默认用户可能没权限删 host 拷贝进去的文件会报Operation not permitted。