返回

CSV 关联合并

数据互转

正在载入

正在加载工具

工具代码会在打开时按需载入,请稍候。

本工具的全部运算都发生在你的浏览器中,输入内容不会发送到任何服务器。

关于这个工具

把客户姓名补充到订单导出表,或按地区关联查询表,无需上传文件。将左表键列逐项映射到右表键列,即使列名不同也能关联;随后选择左连接、内连接或全连接,并勾选需要保留的字段。输出列统一添加 left. 或 right. 前缀,避免同名列混淆。默认遇到重复键就停止。确需一对多或多对多关联时,必须显式选择展开,查看预检数量并确认笛卡尔积匹配。解析、预检和导出均在浏览器本地的一次性 Worker 中运行。

常见用途

  • 将订单表的 customer_id 映射到客户表的 id,以左连接保留 order_id、total、name 和 tier。
  • 以 region 与 customer_id 组成复合键关联地区数据,右表可以使用不同的键列名。
  • 通过全连接检查已匹配与未匹配记录,下载所选数据列及左右来源行号以便核对。

使用方法

  1. 1.分别粘贴 CSV 或选择本地文件,并为每一侧指定分隔符、是否有表头及文件编码。支持逗号、分号、制表符和竖线分隔,以及 UTF-8、UTF-16LE、UTF-16BE 解码。检查列后,按顺序添加一个或多个左右键映射。列名按原样识别;无表头时使用 column1、column2 等生成列名。选择左连接、内连接或全连接,再选择两侧要输出的列。
  2. 2.默认按字符串精确匹配键,仅在确有需要时启用首尾空白裁剪或忽略大小写。选择空键分量应被拒绝、允许与另一个空值匹配,还是始终不匹配。运行预检,核对重复键组、匹配和未匹配的来源行数、预计输出行数、单元格数及序列化字节数。默认拒绝任何重复键,合法查询关系中的“多”侧也一样。需要保留重复键关系时,显式选择笛卡尔积展开,并在预检通过后确认本次展开,再执行合并。修改输入或选项会使原有结果与确认失效。
  3. 3.在分页结果中检查左右数据行号。左连接保留全部左表行,内连接仅保留匹配行对,全连接还会追加未匹配的右表行。下载 JSON 可保留原始字段字符串及代表缺失侧的 null。CSV 先输出五个来源元数据列,再输出带前缀的所选字段。用表格软件查看时保持公式防护开启;选择原始 CSV 前应确认其风险与目标软件。预览分页和文本缩略不会删减下载内容。

可执行的 CSV 关联示例

左连接为订单补充客户信息

{
  "left": "order_id,customer_id,total\nA100,001,12.50\nA101,002,0\nA102,009,7",
  "right": "id,name,tier\n001,Alice,gold\n002,Bob,silver\n003,Chika,bronze",
  "leftOptions": {
    "delimiter": ",",
    "header": true
  },
  "rightOptions": {
    "delimiter": ",",
    "header": true
  },
  "joinOptions": {
    "kind": "left",
    "keys": [
      {
        "left": "customer_id",
        "right": "id"
      }
    ],
    "leftColumns": [
      "order_id",
      "total"
    ],
    "rightColumns": [
      "name",
      "tier"
    ],
    "trim": false,
    "ignoreCase": false,
    "emptyKeys": "reject",
    "duplicates": "reject"
  }
}
{
  "headers": [
    "left.order_id",
    "left.total",
    "right.name",
    "right.tier"
  ],
  "summary": {
    "matched": 2,
    "leftOnly": 1,
    "rightOnly": 0,
    "total": 3
  },
  "rows": [
    {
      "kind": "matched",
      "leftRow": 1,
      "rightRow": 1,
      "values": [
        "A100",
        "12.50",
        "Alice",
        "gold"
      ]
    },
    {
      "kind": "matched",
      "leftRow": 2,
      "rightRow": 2,
      "values": [
        "A101",
        "0",
        "Bob",
        "silver"
      ]
    },
    {
      "kind": "left-only",
      "leftRow": 3,
      "rightRow": null,
      "values": [
        "A102",
        "7",
        null,
        null
      ]
    }
  ]
}

将 customer_id 映射到 id,仅保留所选订单与客户字段。A100、A101 匹配;A102 没有对应客户,因此右侧字段为 null。字符串 12.50 保持原样。

内连接只保留匹配订单

{
  "left": "order_id,customer_id,total\nA100,001,12.50\nA101,002,0\nA102,009,7",
  "right": "id,name,tier\n001,Alice,gold\n002,Bob,silver\n003,Chika,bronze",
  "leftOptions": {
    "delimiter": ",",
    "header": true
  },
  "rightOptions": {
    "delimiter": ",",
    "header": true
  },
  "joinOptions": {
    "kind": "inner",
    "keys": [
      {
        "left": "customer_id",
        "right": "id"
      }
    ],
    "leftColumns": [
      "order_id",
      "total"
    ],
    "rightColumns": [
      "name",
      "tier"
    ],
    "trim": false,
    "ignoreCase": false,
    "emptyKeys": "reject",
    "duplicates": "reject"
  }
}
{
  "headers": [
    "left.order_id",
    "left.total",
    "right.name",
    "right.tier"
  ],
  "summary": {
    "matched": 2,
    "leftOnly": 0,
    "rightOnly": 0,
    "total": 2
  },
  "rows": [
    {
      "kind": "matched",
      "leftRow": 1,
      "rightRow": 1,
      "values": [
        "A100",
        "12.50",
        "Alice",
        "gold"
      ]
    },
    {
      "kind": "matched",
      "leftRow": 2,
      "rightRow": 2,
      "values": [
        "A101",
        "0",
        "Bob",
        "silver"
      ]
    }
  ]
}

相同输入只产生 A100、A101。内连接既不保留未匹配订单 A102,也不保留没有订单的客户 Chika。

全连接保留未匹配订单和客户

{
  "left": "order_id,customer_id,total\nA100,001,12.50\nA101,002,0\nA102,009,7",
  "right": "id,name,tier\n001,Alice,gold\n002,Bob,silver\n003,Chika,bronze",
  "leftOptions": {
    "delimiter": ",",
    "header": true
  },
  "rightOptions": {
    "delimiter": ",",
    "header": true
  },
  "joinOptions": {
    "kind": "full",
    "keys": [
      {
        "left": "customer_id",
        "right": "id"
      }
    ],
    "leftColumns": [
      "order_id",
      "total"
    ],
    "rightColumns": [
      "name",
      "tier"
    ],
    "trim": false,
    "ignoreCase": false,
    "emptyKeys": "reject",
    "duplicates": "reject"
  }
}
{
  "headers": [
    "left.order_id",
    "left.total",
    "right.name",
    "right.tier"
  ],
  "summary": {
    "matched": 2,
    "leftOnly": 1,
    "rightOnly": 1,
    "total": 4
  },
  "rows": [
    {
      "kind": "matched",
      "leftRow": 1,
      "rightRow": 1,
      "values": [
        "A100",
        "12.50",
        "Alice",
        "gold"
      ]
    },
    {
      "kind": "matched",
      "leftRow": 2,
      "rightRow": 2,
      "values": [
        "A101",
        "0",
        "Bob",
        "silver"
      ]
    },
    {
      "kind": "left-only",
      "leftRow": 3,
      "rightRow": null,
      "values": [
        "A102",
        "7",
        null,
        null
      ]
    },
    {
      "kind": "right-only",
      "leftRow": null,
      "rightRow": 3,
      "values": [
        null,
        null,
        "Chika",
        "bronze"
      ]
    }
  ]
}

全连接保留 A102,并在所有左侧行之后追加 Chika。为 null 的来源行号和字段标明缺失侧,不会与空字符串混淆。

映射地区与客户编号复合键

{
  "left": "region,customer_id,order_id\nNorth,007,A201\nSouth,007,A202",
  "right": "territory,id,name\nSouth,007,Minami\nNorth,007,Kita",
  "leftOptions": {
    "delimiter": ",",
    "header": true
  },
  "rightOptions": {
    "delimiter": ",",
    "header": true
  },
  "joinOptions": {
    "kind": "left",
    "keys": [
      {
        "left": "region",
        "right": "territory"
      },
      {
        "left": "customer_id",
        "right": "id"
      }
    ],
    "leftColumns": [
      "region",
      "customer_id",
      "order_id"
    ],
    "rightColumns": [
      "name"
    ],
    "trim": false,
    "ignoreCase": false,
    "emptyKeys": "reject",
    "duplicates": "reject"
  }
}
{
  "headers": [
    "left.region",
    "left.customer_id",
    "left.order_id",
    "right.name"
  ],
  "summary": {
    "matched": 2,
    "leftOnly": 0,
    "rightOnly": 0,
    "total": 2
  },
  "rows": [
    {
      "kind": "matched",
      "leftRow": 1,
      "rightRow": 2,
      "values": [
        "North",
        "007",
        "A201",
        "Kita"
      ]
    },
    {
      "kind": "matched",
      "leftRow": 2,
      "rightRow": 1,
      "values": [
        "South",
        "007",
        "A202",
        "Minami"
      ]
    }
  ]
}

客户编号 007 在两个地区均出现。将 region 映射到 territory、customer_id 映射到 id 后,两个元组仍不同。右表行序不同不会改变结果的左侧顺序。

区分带前导零的键

{
  "left": "customer_id,code\n001,007\n1,7",
  "right": "id,name\n1,Alice",
  "leftOptions": {
    "delimiter": ",",
    "header": true
  },
  "rightOptions": {
    "delimiter": ",",
    "header": true
  },
  "joinOptions": {
    "kind": "left",
    "keys": [
      {
        "left": "customer_id",
        "right": "id"
      }
    ],
    "leftColumns": [
      "customer_id",
      "code"
    ],
    "rightColumns": [
      "id",
      "name"
    ],
    "trim": false,
    "ignoreCase": false,
    "emptyKeys": "reject",
    "duplicates": "reject"
  }
}
{
  "headers": [
    "left.customer_id",
    "left.code",
    "right.id",
    "right.name"
  ],
  "summary": {
    "matched": 1,
    "leftOnly": 1,
    "rightOnly": 0,
    "total": 2
  },
  "rows": [
    {
      "kind": "left-only",
      "leftRow": 1,
      "rightRow": null,
      "values": [
        "001",
        "007",
        null,
        null
      ]
    },
    {
      "kind": "matched",
      "leftRow": 2,
      "rightRow": 1,
      "values": [
        "1",
        "7",
        "1",
        "Alice"
      ]
    }
  ]
}

键 001 不匹配 1,不会通过数字转换移除前导零;所选 code 值 007 也按字符串原样保留。

只规范化键,不改写输出文本

{
  "left": "customer_id,status\n A , Paid ",
  "right": "id,name\na,ALICE",
  "leftOptions": {
    "delimiter": ",",
    "header": true
  },
  "rightOptions": {
    "delimiter": ",",
    "header": true
  },
  "joinOptions": {
    "kind": "left",
    "keys": [
      {
        "left": "customer_id",
        "right": "id"
      }
    ],
    "leftColumns": [
      "customer_id",
      "status"
    ],
    "rightColumns": [
      "id",
      "name"
    ],
    "trim": true,
    "ignoreCase": true,
    "emptyKeys": "reject",
    "duplicates": "reject"
  }
}
{
  "headers": [
    "left.customer_id",
    "left.status",
    "right.id",
    "right.name"
  ],
  "summary": {
    "matched": 1,
    "leftOnly": 0,
    "rightOnly": 0,
    "total": 1
  },
  "rows": [
    {
      "kind": "matched",
      "leftRow": 1,
      "rightRow": 1,
      "values": [
        " A ",
        " Paid ",
        "a",
        "ALICE"
      ]
    }
  ]
}

空白裁剪和忽略大小写使 A 与 a 匹配,但原键 " A "、状态 " Paid " 和名称 "ALICE" 都不会在结果中被改写。

默认拒绝重复客户键

{
  "left": "customer_id,order_id\n001,A1\n001,A2",
  "right": "id,name\n001,Alice",
  "leftOptions": {
    "delimiter": ",",
    "header": true
  },
  "rightOptions": {
    "delimiter": ",",
    "header": true
  },
  "joinOptions": {
    "kind": "left",
    "keys": [
      {
        "left": "customer_id",
        "right": "id"
      }
    ],
    "leftColumns": [
      "order_id"
    ],
    "rightColumns": [
      "name"
    ],
    "trim": false,
    "ignoreCase": false,
    "emptyKeys": "reject",
    "duplicates": "reject"
  }
}
{
  "error": "duplicateKey"
}

左表有两笔订单共享客户键 001。即使右侧查询键唯一,默认仍拒绝重复关联键;若要保留两笔订单,应明确选择展开。

显式展开二乘二关系

{
  "left": "customer_id,order_id\n001,A1\n001,A2",
  "right": "id,contact\n001,home\n001,work",
  "leftOptions": {
    "delimiter": ",",
    "header": true
  },
  "rightOptions": {
    "delimiter": ",",
    "header": true
  },
  "joinOptions": {
    "kind": "left",
    "keys": [
      {
        "left": "customer_id",
        "right": "id"
      }
    ],
    "leftColumns": [
      "order_id"
    ],
    "rightColumns": [
      "contact"
    ],
    "trim": false,
    "ignoreCase": false,
    "emptyKeys": "reject",
    "duplicates": "expand"
  },
  "approveExpansion": true
}
{
  "headers": [
    "left.order_id",
    "right.contact"
  ],
  "summary": {
    "matched": 4,
    "leftOnly": 0,
    "rightOnly": 0,
    "total": 4
  },
  "rows": [
    {
      "kind": "matched",
      "leftRow": 1,
      "rightRow": 1,
      "values": [
        "A1",
        "home"
      ]
    },
    {
      "kind": "matched",
      "leftRow": 1,
      "rightRow": 2,
      "values": [
        "A1",
        "work"
      ]
    },
    {
      "kind": "matched",
      "leftRow": 2,
      "rightRow": 1,
      "values": [
        "A2",
        "home"
      ]
    },
    {
      "kind": "matched",
      "leftRow": 2,
      "rightRow": 2,
      "values": [
        "A2",
        "work"
      ]
    }
  ]
}

将 duplicates 设为 expand 并明确批准通过的预检后,两笔订单和两条联系记录组成四个行对。先按左表订单顺序,再保留各订单内部的右表联系记录顺序。

区分缺失、空字符串和文本 null

{
  "left": "customer_id,order_id\n1,A1\n2,A2\n3,A3",
  "right": "id,note\n1,\n3,null",
  "leftOptions": {
    "delimiter": ",",
    "header": true
  },
  "rightOptions": {
    "delimiter": ",",
    "header": true
  },
  "joinOptions": {
    "kind": "left",
    "keys": [
      {
        "left": "customer_id",
        "right": "id"
      }
    ],
    "leftColumns": [
      "order_id"
    ],
    "rightColumns": [
      "note"
    ],
    "trim": false,
    "ignoreCase": false,
    "emptyKeys": "reject",
    "duplicates": "reject"
  }
}
{
  "headers": [
    "left.order_id",
    "right.note"
  ],
  "summary": {
    "matched": 2,
    "leftOnly": 1,
    "rightOnly": 0,
    "total": 3
  },
  "rows": [
    {
      "kind": "matched",
      "leftRow": 1,
      "rightRow": 1,
      "values": [
        "A1",
        ""
      ]
    },
    {
      "kind": "left-only",
      "leftRow": 2,
      "rightRow": null,
      "values": [
        "A2",
        null
      ]
    },
    {
      "kind": "matched",
      "leftRow": 3,
      "rightRow": 2,
      "values": [
        "A3",
        "null"
      ]
    }
  ]
}

A1 对应已有空备注,A2 没有匹配客户,A3 的备注为四个字符的文本 null。JSON 分别保存为 ""、null 和 "null",并记录来源行号。

关联不同分隔符的无表头数据

{
  "left": "001;A1\n002;A2",
  "right": "001|Alice\n003|Chika",
  "leftOptions": {
    "delimiter": ";",
    "header": false
  },
  "rightOptions": {
    "delimiter": "|",
    "header": false
  },
  "joinOptions": {
    "kind": "full",
    "keys": [
      {
        "left": "column1",
        "right": "column1"
      }
    ],
    "leftColumns": [
      "column1",
      "column2"
    ],
    "rightColumns": [
      "column2"
    ],
    "trim": false,
    "ignoreCase": false,
    "emptyKeys": "reject",
    "duplicates": "reject"
  }
}
{
  "headers": [
    "left.column1",
    "left.column2",
    "right.column2"
  ],
  "summary": {
    "matched": 1,
    "leftOnly": 1,
    "rightOnly": 1,
    "total": 3
  },
  "rows": [
    {
      "kind": "matched",
      "leftRow": 1,
      "rightRow": 1,
      "values": [
        "001",
        "A1",
        "Alice"
      ]
    },
    {
      "kind": "left-only",
      "leftRow": 2,
      "rightRow": null,
      "values": [
        "002",
        "A2",
        null
      ]
    },
    {
      "kind": "right-only",
      "leftRow": null,
      "rightRow": 2,
      "values": [
        null,
        null,
        "Chika"
      ]
    }
  ]
}

左侧使用分号、右侧使用竖线,两侧均无表头。映射生成的 column1,再输出所选位置列。前缀用于区分数据来源。

空键始终不匹配

{
  "left": "customer_id,order_id\n,A1",
  "right": "id,name\n,Alice",
  "leftOptions": {
    "delimiter": ",",
    "header": true
  },
  "rightOptions": {
    "delimiter": ",",
    "header": true
  },
  "joinOptions": {
    "kind": "full",
    "keys": [
      {
        "left": "customer_id",
        "right": "id"
      }
    ],
    "leftColumns": [
      "order_id"
    ],
    "rightColumns": [
      "name"
    ],
    "trim": false,
    "ignoreCase": false,
    "emptyKeys": "never-match",
    "duplicates": "reject"
  }
}
{
  "headers": [
    "left.order_id",
    "right.name"
  ],
  "summary": {
    "matched": 0,
    "leftOnly": 1,
    "rightOnly": 1,
    "total": 2
  },
  "rows": [
    {
      "kind": "left-only",
      "leftRow": 1,
      "rightRow": null,
      "values": [
        "A1",
        null
      ]
    },
    {
      "kind": "right-only",
      "leftRow": null,
      "rightRow": 1,
      "values": [
        null,
        "Alice"
      ]
    }
  ]
}

选择 never-match 后,即使两侧键字段都为空也不匹配。全连接因此输出一个 left-only 行和一个 right-only 行。

显式允许空键匹配

{
  "left": "customer_id,order_id\n,A1",
  "right": "id,name\n,Alice",
  "leftOptions": {
    "delimiter": ",",
    "header": true
  },
  "rightOptions": {
    "delimiter": ",",
    "header": true
  },
  "joinOptions": {
    "kind": "left",
    "keys": [
      {
        "left": "customer_id",
        "right": "id"
      }
    ],
    "leftColumns": [
      "order_id"
    ],
    "rightColumns": [
      "name"
    ],
    "trim": false,
    "ignoreCase": false,
    "emptyKeys": "allow",
    "duplicates": "reject"
  }
}
{
  "headers": [
    "left.order_id",
    "right.name"
  ],
  "summary": {
    "matched": 1,
    "leftOnly": 0,
    "rightOnly": 0,
    "total": 1
  },
  "rows": [
    {
      "kind": "matched",
      "leftRow": 1,
      "rightRow": 1,
      "values": [
        "A1",
        "Alice"
      ]
    }
  ]
}

选择 allow 后,两个空字符串键相等。该选项不取消重复键检查或展开上限,而是显式改变空键的匹配含义。

CSV 关联时的常见错误

  • 客户编号只在各地区内唯一,却仅选 customer_id;应补充 region 到 territory 的映射。
  • 需要保留没有客户资料的订单,却选择了内连接。
  • 以为重复键只会取一条查询结果,或未检查 m × n 行数就开启展开。
  • 开启空白裁剪或忽略大小写后,没有检查原本不同的键是否变成相同。
  • 将 001 与 1 当作相同编号,或认为 CSV 能强制表格软件保留前导零。
  • 删掉来源元数据列后,把缺失侧的空白误认为已有空字段。
  • 在表格软件中打开不可信来源的原始 CSV,或未检查内容就分享下载报告。

限制与说明

  • 每个输入最多 2 MiB 文件字节、解码后 2 MiB UTF-8 文本、10,000 个数据行、128 列和 200,000 个数据单元格。输出最多 50,000 行、500,000 个单元格(包含每行五个 CSV 来源列),序列化 JSON 与带防护 CSV 各不超过 12 MiB。预检会在构造合并结果行之前检查行数增长与序列化大小,超限直接阻止合并,不会截断。Worker 可取消且有运行期限,但并非严格的浏览器内存沙箱。更大的文件应按相同键分区拆分,或改用本地数据库。
  • 所有值都保留为字符串:001 与 1 是不同的键,1.0 不会变成 1。可选的首尾空白裁剪与转小写仅作用于关联键,不改变输出值或表头,也不提供模糊匹配、数值或日期转换、Unicode 规范化或区域化排序。复合键按元组识别,不以可能歧义的分隔符拼接。任一规范化后的键分量为空时按所选空键策略处理。重复检测使用同样的规范化键,因此开启选项可能产生冲突。“始终不匹配”下的空键各自保持未匹配,不会组成共同的空键组。
  • 任一侧的完整键重复时,默认阻止关联,未匹配的重复键组也不例外,不会擅自选第一行或最后一行。显式展开会为相同键生成全部行对:左侧两行与右侧三行产生六行。左侧多行匹配右侧一行也需要该策略和确认。统计对应所选连接方式,其中 matched 是输出匹配行对数,可能大于匹配的来源行数。结果先按左表原顺序排列,每个左行的右侧匹配按右表顺序排列,全连接最后追加未匹配右行。不执行聚合、去重、模糊关联或 SQL。
  • 每个所选输出列都命名为 left.<原始列名> 或 right.<原始列名>;两侧同名列或键列均被选中时,两份都会保留。JSON 对缺失侧的字段使用 null,已有空单元格仍是 "",文本 null 仍是 "null";每行还包含类型和从 1 开始、可为 null 的 leftRow/rightRow 来源行号。CSV 将缺失值写为空白,并通过 _join_kind、_left_row、_right_row、_left_present、_right_present 区分来源是否存在。CSV 公式防护会给风险字段添加单引号,因此会改变导出的字段文本。原始 CSV 可能触发表格公式,表格软件还可能改写编号或在保存后重新打开时移除防护。精确字符串交换建议使用 JSON;它不保留原文件的引号形式或换行字节。
  • 文件必须严格按所选支持编码解码,BOM 必须与编码一致。GBK、Shift_JIS 等需预先转换。错误引号、行宽不一致、空表头和完全重复的表头会被拒绝,表头不会自动裁剪或重命名。支持带引号的分隔符、双写引号及多行字段。来源行诊断从第 1 个数据行计数;解析诊断按包含表头的 CSV 记录计数,不等于带引号文本中的物理行号。工具不上传或持久化输入;单元格按文本显示。下载文件可能含有私密信息,且不受页面继续控制。预览每页显示 25 行及所选数据列中的前 12 列,并缩略过长表头与值;导出会保留全部所选字段。

常见问题

订单和客户表应该选哪种连接?

将订单放左侧,把 customer_id 映射到右表 id。左连接保留缺少客户的订单,其 JSON 客户字段为 null;内连接会去掉这些未匹配订单;全连接还会列出没有订单的客户。若多笔订单共享 customer_id,默认重复键错误是预期行为:请显式选择展开;如果需要多对一查询,应确认客户侧键唯一,再核对并确认预计结果。

不同列名或复合键如何映射?

为每个键分量添加一组对应关系,例如 left.region 对 right.territory,再添加 left.customer_id 对 right.id。匹配按映射顺序比较整个元组。左右列名不必相同,但每个名称都必须精确对应各自表中的现有列。前导零有意义。需要检查键的原样文本时,可把两侧键列都选为输出列。

为什么展开前还需要预检和确认?

重复键可能使结果数量意外增长。同一键在左表出现 m 次、右表出现 n 次时,展开产生 m × n 个匹配行对。预检在构造结果行前展示重复情况、来源行统计及此处理范围内准确的预计序列化大小。通过检查并不代表工具替你决定了业务关系。确认只适用于当前输入和选项;修改后必须重新预检和确认。

能区分未匹配字段、空字符串和文本 null 吗?

可以。JSON 中缺失侧字段为 null,已有空字段为 "",文本 null 为 "null";每行还带有类型和来源行号。CSV 本身没有 null 类型,因此工具在写入空白缺失字段的同时输出 _left_present、_right_present 标记及来源行号。检查 CSV 时请保留这些元数据列。带防护 CSV 可能改变类似公式的字符串,重视精确解析文本时应选择 JSON。

相关工具