FilesLast commitLast update
3 days ago
3 days ago
3 days ago
README

两份 Excel 10 万行核对:VLOOKUP 为什么会漏掉重复键

A 表 100,143 行,B 表 100,343 行,全量索引跑出 943 条差异。

同一份数据换成 VLOOKUP 的首条匹配,只剩 743 条,漏检 343 条,还多报 143 条。

漏掉的不是 343 个格子,是 343 个主键组里被首条匹配挡住的后续行。

我把这件事做成了一个单文件网页,两个引擎并排跑,差异、行号、原因都能点开看。

图 1

图 1:同一份数据,全量索引 943 条差异,VLOOKUP 743 条,漏检 343、误报 143。

页面不读 .xlsx 二进制,也不上传数据。复制粘贴或导入 CSV/TSV,计算全在浏览器里。

一、结论:VLOOKUP 只认第一条

VLOOKUP 的规则是找到第一条就返回,不是把所有匹配都列出来。

主键一重复,B 表第二条及以后的行就进不了结果。

更隐蔽的是 A 表自己也有重复行。

VLOOKUP 会拿 A 的第二行去比 B 的第一行,算出一条「修改」,可两边根本不是同一对。

主键分布 组数 全量索引看到什么 VLOOKUP 首条看到什么 结果
A 1 行 / B 2 行 200 B 多出的行记新增 第二条被忽略 漏检 200 行
A 2 行 / B 2 行 143 第二对金额差 88.8,记修改 拿 A 第二行比 B 第一行 漏检 143 行 + 误报 143 条

两组一加,漏检 343 行,误报 143 条。

我拿 Python 按同一套演示规则重算了一遍,四个数对得上:新增 300、删除 100、修改 543、重复主键 343 组。

二、为什么值得看:一次可复现的对照实验

讲 VLOOKUP 漏数据的文章不少,大多停在「要注意重复值」。

真到核对 10 万行订单,你需要知道漏了多少、漏在哪一行、另一套算法能不能补上。

我把演示数据的随机种子固定成 20261106,谁打开页面点两下,都会得到同一组数字。

这比一张「核对完成」的截图更有用。

能对上号的场景不少:财务对账、订单导出对比、库存表、两份名单。

共同点是主键可能重复,而且人眼翻不过来。

三、页面怎么用:三步拿到差异表

第一步,点「生成 10 万行演示数据」,或者把两张表粘进左右两个框。

第二步,点字段名设主键,比较列会自动匹配同名列;多列主键用逗号分隔。

第三步,点「运行核对」,在全部、新增、删除、修改、重复主键之间切换。

我实际点了一遍。

导入、设主键、跑核对、切到「重复主键」、点开 ORD-000022,右侧直接给出 A 表 1 行、B 表 2 行、B 后续 1 行被忽略。

图 3

图 3:重复主键 343 组,每一组都能看到 A/B 行数与被忽略的行数。

图 2

图 2:点开一条修改,右侧并排显示两侧原始行,金额差异用橙色标出。

差异表支持翻页,点任意行都能看原始行对照。

底部还有「内置自检」,16 条断言覆盖解析、两个引擎、导出和 5 条失败路径。

四、双引擎的分工

解析层先把文本变成表。

分隔符按前 5 行的列数是否稳定来判断,不靠数谁出现得多。

重复表头会自动加后缀,比如两个「金额」变成「金额」和「金额_2」。

多列主键用不可见字符拼接,避免「北京+12」和「北京1+2」撞成同一个键。

空主键直接拦下,报出第几行、哪个字段。

全量引擎给两张表各建一张 Map,key 是主键,value 是这个主键下的所有行。

同一个主键按出现顺序配对:第 1 行对第 1 行,第 2 行对第 2 行。

多出来的行分别记新增或删除,单元格差异按「主键+字段」去重。

VLOOKUP 引擎只保留每个主键的第一行。

A 表逐行查,B 表独有的主键补成新增。

它更快,但重复键的后续行天然看不到。

指标 全量索引 VLOOKUP 首条
差异条数 943 743
漏检 — 343
误报 — 143
本机耗时 110.3~126.4 ms 83.3~110.1 ms

耗时用 performance.now() 量,同一台机器三次运行的范围,不是理论值。

五、16 项自检与 10 万行实测验证

自检用的是 5 行小样本,两个引擎的期望值都写死。

分组 条数 关键断言
解析 2 TSV 5 行 4 列;CSV 引号内逗号与双引号
全量引擎 3 新增 1、删除 1、修改去重 3
重复主键 1 id=2 只有 1 组
VLOOKUP 3 忽略 1 行、漏检 1 行、漏检与误报同时出现
导出 2 差异 CSV 行数、合并去重 5 个主键
失败路径 5 空主键、列名不存在、主键不一致、缺比较列、未导入
合计 16 页面显示 16/16 通过

页面显示 16/16 通过。

10 万行这组数据也是固定种子生成的,A 表 100,143 行,B 表 100,343 行。

为什么不是整 10 万:演示数据故意插了重复行和新增行。

B 表每 1000 行删 1 行,共 100 行;补 100 条全新订单;再加 343 条重复主键的额外行。

A 表补 143 条第二行。

生成耗时 66.2 ms,全量核对 126.4 ms,VLOOKUP 110.1 ms。

我另外用 Python 复算了一遍:300 新增、100 删除、543 修改、343 组重复主键,和页面一致。

图 4

图 4:16 条自检全部通过,失败数为 0。

六、两个真实边界:自检没覆盖,我手工测出来的

第一条在分隔符。

我粘了一段 Tab 分隔的内容,其中一列是「北京,朝阳,三里屯」。

页面把它判成了 CSV,因为全文的逗号比 Tab 多。

结果表头只剩一列,后面的地址被静默丢掉。

修法不是调阈值,而是看前 5 行的列数是否稳定。

同样的内容现在解析成 2 列,CSV 里的引号逗号也不会误判。

第二条在多列主键的顺序。

A 表主键填「城市,订单号」,B 表填「订单号,城市」。

校验只比较了排序后的集合,认为两张表一致,放行了。

拼 key 时却按输入顺序拼,两个 key 永远对不上。

两张完全一样的 2 行表,页面报新增 2 行、删除 2 行。

修法是拼 key 前先按列名排序,两张表就用同一个顺序。

补完后,同样两张表是 0 新增、0 删除、0 修改。

这两条目前靠手工验证,自检里还没有对应的固定用例。

改完逻辑,16 条自检仍然全过,10 万行的 943/743/343 也没变。

失败路径也测过。

清空 A 表再点运行,页面顶部直接给错误码和下一步。

提示原文是「A 表尚未导入数据。请粘贴 Excel 内容、导入 CSV/TSV,或点击「生成 10 万行演示数据」」。

图 5

图 5:清空数据后点运行,页面给出 NO_DATA_A,并保留空表状态。

七、能力边界

  • 只认 CSV/TSV 文本,不解析 .xlsx 二进制。Excel 文件要先另存为 CSV,或者复制单元格粘贴。
  • 比较是 trim 后的字符串精确比较。29.9 和 29.90 会算不同,2025-01-01 和 2025/01/01 也会算不同。
  • 不支持公式、合并单元格和多工作表,导入前要整理成一张平表。
  • 10 万行能跑,数据常驻内存;再大就要分片。
  • 空主键会被拦下,不会替你猜。

这几条不是功能清单,是我实际踩到的边界。

八、准备环境:打开码道 Web

码道有三种使用方式:WebUI(浏览器对话)、TUI(终端命令行)和桌面 IDE(IDE 插件)。

这个页面用 WebUI 版生成。入口是:

登录后直接输入需求,不需要装软件。

试玩和源码在 Demo Park:

打开 index.html 就能用,不需要构建,也没有后端依赖。

九、使用码道体会

  • 先定验收数字,再让 AI 写。这次先定 100,143、100,343、943、343 四个数,生成后逐个对。
  • 自检要渲染到页面上。16 条断言能点、能看,文章里的测试数才有出处。
  • 小样本自检和十万行实测一起做。5 行样本保证规则对,十万行保证不崩。
  • 边界输入单独测。分隔符、主键顺序、空值,这三类不会自己出现在演示数据里。
  • 让两个算法并排跑。差异不只是有没有,还要看差多少、差在哪。
  • 错误提示写到人能照着做。报第几行、哪个字段、下一步点什么,比一句格式错误有用。

十、总结

VLOOKUP 不是错,它只是按设计只返回第一条。

两份表一旦有重复主键,首条匹配就会同时带来漏检和误报。

要核对得干净,先把主键去重,或者改用全量索引。

页面和 10 万行演示数据都在上面的链接里,点开就能复现这组数字。