必须用动态占位符+参数绑定,否则100%暴露SQL注入风险;直接拼接IN列表会导致恶意语句执行、索引失效或类型错误,且空数组和超长列表不处理必致线上故障。

直接拼接 IN 列表是高危操作,必须用动态占位符 + 参数绑定,否则 100% 暴露 SQL 注入风险。
为什么不能写 WHERE id IN ('" + ids.join("','") + "')
这种写法把用户输入直接塞进 SQL 字符串里,一旦 ids 包含 "2', DROP TABLE users--" 这类内容,数据库就执行恶意语句。更隐蔽的问题是:整型字段被传入带引号的字符串,MySQL 可能放弃索引走全表扫描;PostgreSQL 会报类型不匹配错误。安全和性能双崩。
如何动态生成 ? 占位符(以 Node.js + pg 为例)
核心逻辑是:占位符字符串只由固定字符 ? 和逗号拼出,不含任何用户数据;真实值全部走驱动原生绑定。
示例代码:
const ids = [1, 5, 9, 22];
if (ids.length === 0) {
return db.query('SELECT * FROM users WHERE 1=0');
}
const placeholders = Array(ids.length).fill('?').join(','); // 安全:无用户数据
const sql = `SELECT * FROM users WHERE id IN (${placeholders})`;
return db.query(sql, ids); // 驱动自动一一绑定-
Array(n).fill('?').join(',')比map((_, i) => '?')更简洁,语义更直白 - 不要用
ids.map(() => '?').join(',')——虽然结果一样,但容易让人误以为在遍历用户数据 - PostgreSQL 的
pg驱动支持数组参数(WHERE id = ANY($1)),但ANY在 MySQL 不可用,跨库时仍推荐IN+ 动态占位符
空数组和超长列表必须显式拦截
这两个边界情况不处理,线上必炸。
- 空数组:
IN ()是语法错误,必须退化为WHERE 1=0或改用NOT EXISTS - 超长列表:PostgreSQL 默认单语句最多 65535 个参数;SQLite 对
IN子句长度无硬限制但有性能拐点(Drizzle 在 SQLite 下 >999 项直接报错);MySQL 受max_allowed_packet和预编译缓存影响 - 超过阈值时,应主动分批查询(如每 500 项一批),而不是依赖驱动报错后兜底
ORM 的 whereIn() 不是免检通道
Knex、Drizzle、Django ORM 等封装了 whereIn,但底层仍是动态占位符——它们通常不自动处理空数组或超长列表。
- Django 遇到空列表抛
EmptyResultSet,不捕获就会 500 - Knex 返回空结果但静默,业务可能误判为“查不到数据”而非“条件非法”
- Drizzle 的
inArray()在 SQLite 下对 >999 项直接 throw,需手动切片
别迷信封装,边界逻辑永远得自己盯住——尤其是空数组和长度突增的场景,最容易在压测或突发流量时漏掉。

















