DEBUG.WIKI
晚安云;香港 CN2 ¥15/月
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
搬瓦工;先看线路,再选 VPS;CN2 GIA · 对应线路套餐;查看线路 →

SQLAlchemy 的 IN 查询少了记录,检查展开参数是否覆盖同名绑定

SQLAlchemy 2.1.5 的 expanding IN 参数可与其他绑定名碰撞,查询不报错却比较了错误的值。检查展开后的参数,用不重叠的明确名称局部规避。

查询同时带 a IN (...) 与另一列的等值条件,SQL 没报错,明明存在的记录却查不到。如果列名或绑定名带有相似数字后缀,应检查 expanding 参数展开后的名字是否撞到了另一个绑定。

SQLAlchemy 2.1.5 有这一问题的明确记录,SQLite 的位置参数和 PostgreSQL 的命名参数都可能受影响。可核对的该正式版仍按原绑定名追加 _1、_2 生成展开项;截至 2026 年 10 月 21 日,不能把后续开发分支修复直接当成已发布修补。

原始参数正确,展开以后可能覆盖

假设两列分别叫 a 和 a_1,查询写成:

stmt = select(t.c.id).where(t.c.a.in_([1, 2]), t.c.a_1 == 5)

这里 t 是已有的 SQLAlchemy Table。自动命名可能让 IN 参数成为 a_1,展开项成为 a_1_1、a_1_2;另一列的等值绑定又恰好叫 a_1_1。展开参数合并时,原来的 5 被第一项 1 覆盖,数据库实际比较的条件随之变化。

因此,普通 compiled.params 看起来正确,仍不能排除问题。只打印 SQL 中的占位符也不够,需要检查展开后的参数对应关系。

为两类参数使用不重叠的名称

可在这条语句中明确命名,避免标量参数落入 IN 参数的数字后缀空间:

from sqlalchemy import Integer, bindparam, column, select, table
from sqlalchemy.dialects import postgresql

t = table("items", column("id"), column("a"), column("a_1"))
stmt = select(t.c.id).where(
    t.c.a.in_(bindparam("filter_values", [1, 2], expanding=True, type_=Integer)),
    t.c.a_1 == bindparam("expected_secondary", 5, type_=Integer),
)
compiled = stmt.compile(dialect=postgresql.dialect())
expanded = compiled.construct_expanded_state()
print(expanded.statement)
print(expanded.parameters)

这是不连接数据库的编译示例,输入使用无敏感信息的常量。预期 expected_secondary 仍对应 5,而展开项分别对应 1、2。实际应用记录参数时,应沿用原有脱敏规则。

不要把标量名字改成 filter_values_1,那又回到了同一个碰撞模式。也不要直接把值插进 SQL 字符串,或依赖 literal_binds 绕过参数化;外部输入仍须保留值绑定。

结果核对要包含不应命中的行

局部改名后,用包含 a=1,a_1=5 和 a=2,a_1=7 等有区分度的数据比较实际返回的主键。前者应匹配,后者应被排除。只比较数量或只用所有条件都相同的数据,容易漏掉错误值恰好仍能命中的情况。

空 IN、多元素 IN,以及同一语句中的其他显式绑定也应按实际用法核对。该方案只规避已识别的名称重叠,不修复整个编译器。后续切换到含修复的正式版本时,仍保留这组结果核对;若错误条件已经用于更新或删除,先根据保留的业务记录确认影响范围,单独改查询代码不会自动恢复历史数据。