从 Excel 里选中一列订单号,复制,拼成 IN ('A1', 'A2', ...) 粘进查询——这是最省事的做法,日常大概占了临时查数场景的一半。
它也是最容易悄悄出错的做法,因为出错的地方几乎都不在 SQL 里。
一、数据在到达你手上之前就可能坏了
Excel 会对粘进去的内容做类型推断,而这件事发生在你复制之前:
| 损坏 | 表现 | 谁中招 |
|---|---|---|
| 前导零消失 | 00123 → 123 | 订单号、员工号、邮编、银行卡号 |
| 科学计数法 | 18 位身份证 → 1.23457E+17 | 长数字串 |
| 被识别成日期 | 2026-1-5、3-15 → 日期序列号 | 带横杠或斜杠的编码 |
第二种最危险:它不只是显示成那样,低位是真的被截断了——Excel 的数值只保留 15 位有效数字。粘出来的值即使改回文本格式,后面几位也已经变成了 0。
避免办法:
- 在 Excel 里先把那一列设成文本格式,再粘入数据(顺序不能反)
- 从数据库或系统直接导出 CSV,用文本编辑器打开,不要中途用 Excel 过一手
- 已经坏了的数据补不回来,只能重新导出
二、看不见的字符
从网页、PDF、聊天记录里复制的内容,经常带着这些:
| 字符 | 来源 |
|---|---|
| U+00A0 不换行空格 | 网页里的 |
| 零宽字符 U+200B / U+FEFF | 反爬、编辑器 |
| BOM | 文件开头 |
| 全角空格 U+3000 | 中文输入法 |
它们在 SQL 里就是普通字符,参与字符串比较。结果是查询返回零行,而你盯着屏幕看不出任何问题——两个字符串看起来一模一样。
所以清洗这一步不能省:去首尾空格 → 去空行 → 去重,顺序就是这个。列表处理 把这三步和输出格式分开,也是为了让清洗先于拼接。
三、引号和类型
引号转义:值里含单引号时必须写成两个(O''Brien),否则轻则语法错误,重则成为注入点。
加不加引号,要看列的真实类型——这一条的代价比想象中大:
-- id 列是 BIGINT
WHERE id IN ('1001', '1002') -- ✗ 触发隐式类型转换
WHERE id IN (1001, 1002) -- ✓
给数字列传字符串,多数数据库会做隐式转换,而转换发生在列上时会放弃走索引,查询从毫秒变成全表扫描。反过来给字符串列传数字同理。
这个坑在数据量小的测试库上完全看不出来,上了生产才发现。
四、数量:上限和计划缓存
硬限制
| 数据库 | 限制 |
|---|---|
| Oracle | IN 列表最多 1000 个表达式 |
| SQL Server | 参数化查询最多 2100 个参数 |
| MySQL | 无 IN 上限,受 max_allowed_packet 约束整条 SQL 长度 |
| PostgreSQL | 无硬上限,解析和规划开销随长度增长 |
比上限更值得注意的是执行计划缓存。
数据库按 SQL 文本缓存执行计划,而 IN (?, ?) 和 IN (?, ?, ?) 是两条不同的语句:
- 每种长度都要重新解析和规划一次
- 长度千变万化时,缓存被大量只用一次的计划塞满,把真正的热点查询挤出去
缓解办法:把个数补齐到固定档位(一律凑到 10、50、100、500,不足的用重复值填),让不同长度收敛成几条语句。或者干脆换做法。
经验分档:
| 数量 | 建议 |
|---|---|
| 几十个 | 随便用 |
| 几百个 | 能用,注意计划缓存 |
| 上千个 | 换做法 |
五、三种替代
1. 临时表 + JOIN
把 ID 批量插进临时表,再和目标表关联。
适合值很多、且同一批 ID 要被多次使用的场景。额外的好处是优化器能用上统计信息,选择的连接方式通常比长 IN 更合理。
2. 数组参数(PostgreSQL)
WHERE id = ANY($1::bigint[])
一个参数装下整个列表,SQL 文本恒定,执行计划可复用。PG 上最干净的做法。
3. VALUES 派生表
JOIN (VALUES (1001),(1002),(1003)) AS t(id) ON x.id = t.id
多数数据库都支持,规划开销比超长 IN 小。
还有一条常被忽略的路
如果这批 ID 本来就来自另一张表的查询结果,直接写成子查询或 JOIN。
「导出成一列 → 粘进 IN」这个动作本身,很多时候是可以完全跳过的。
六、顺手的几个清洗选项
拼 IN 之外,同一份数据常常还要换成别的形状:
| 目标 | 输出格式 |
|---|---|
| SQL IN(字符串) | ('a', 'b', 'c'),单引号转义 |
| SQL IN(数字) | (1, 2, 3),不加引号 |
| JS / TS 数组 | ["a", "b"] |
| 每行一个 | 贴回配置文件 |
顺带一个容易被忽略的细节:排序用自然序。普通字符串排序会把 item10 排在 item2 前面(因为逐字符比较时 1 < 2),这几乎不是任何人想要的结果。