Python sqlite3.Row 重名列:两份 id 都在,为什么转成字典却只剩一个
把用户表与订单表连接后,两列都叫 id。终端按位置打印时,用户编号和订单编号都在;换成 dict(row) 交给接口,却只剩一个字段。应先核对结果列名,再检查转换方式,不能因为 SQL 返回了两列,就假定名称访问也能区分两份值。
sqlite3.Row 同时支持位置与名称访问,但它并不自动给重名列加表名前缀。下面用内存中的两份固定数据复现:用户编号是十一,订单编号是七百零一。两者故意不同,避免错误映射被相同数字掩盖。
先比较位置、名称和字典
保存为 demo.py 后运行 python demo.py。代码只使用标准库,查询中的公共表表达式提供临时数据,不创建磁盘数据库。每一步都有断言;先复现重复名称,再给同一查询添加明确别名。
AI生成概念示意图:两列不同编号贴着相同标签,改用独立标签后才能分别取用;不是数据库界面或查询截图。
import sqlite3
db = sqlite3.connect(":memory:")
try:
old_cursor = db.cursor()
db.row_factory = sqlite3.Row
base = """
WITH users(id) AS (VALUES (11)),
orders(id, user_id) AS (VALUES (701, 11))
"""
join = " FROM users AS u JOIN orders AS o ON o.user_id = u.id"
duplicate_sql = base + "SELECT u.id, o.id" + join
old_row = old_cursor.execute(duplicate_sql).fetchone()
assert type(old_row) is tuple
print("existing cursor:", type(old_row).__name__, old_row)
row = db.execute(duplicate_sql).fetchone()
assert row.keys() == ["id", "id"]
assert tuple(row) == (11, 701)
assert row["id"] == row["ID"] == 11
assert row[1] == 701
assert dict(row) == {"id": 11}
print("names / values:", row.keys(), tuple(row))
print("named lookup:", row["id"])
print("dict(row):", dict(row))
zipped = dict(zip(row.keys(), row))
assert zipped == {"id": 701}
print("dict(zip(...)):", zipped)
fixed_sql = base + "SELECT u.id AS user_id, o.id AS order_id" + join
cursor = db.execute(fixed_sql)
names = [column[0] for column in cursor.description]
assert names == ["user_id", "order_id"]
assert len(names) == len(set(name.lower() for name in names))
fixed = dict(cursor.fetchone())
assert fixed == {"user_id": 11, "order_id": 701}
print("explicit aliases:", fixed)
empty = db.execute(fixed_sql + " WHERE 0")
assert [column[0] for column in empty.description] == names
assert empty.fetchone() is None
old_cursor.row_factory = sqlite3.Row
assert isinstance(old_cursor.execute(fixed_sql).fetchone(), sqlite3.Row)
print("empty metadata / explicit cursor factory: passed")
finally:
db.close()names / values 显示两个 id 与两个不同数字,证明值仍保存在 Row 中。名称读取返回第一列的十一,位置一仍能读出七百零一。CPython 的名称查找从前往后扫描列说明,遇到匹配就返回,因此交换输出列顺序还会改变重名读取的结果。
dict(row) 得到的 id 是十一。它通过键名向 Row 取值,两次同名访问都命中第一列,第二列无法成为独立字段。代码另列出 dict(zip(...)),结果却是七百零一:这种构造按位置配对后,后面的同名键覆盖前面的值。二者都丢失一份信息,不能互相当作等价修复。
在查询出口建立唯一名称
修复查询明确写出 user_id 与 order_id,转换后两个编号都保留。表限定名 u.id 能让 SQL 认出来源,但默认输出名称仍可能只是 id;所以限定来源与指定输出别名是两项工作。别名最好表达业务角色,避免仅叫 id1、id2 后还要查列顺序。
本例用全小写 ASCII 别名,并检查忽略大小写后的唯一性。Row 对这种名称的大小写不敏感,仅把 id 改成 ID 不能可靠区分两列。这里的检查服务于当前命名约定,不是对任意 Unicode 名称的通用规范化规则。
查询没有返回行时,也能从 cursor.description 取得输出列名,所以结构检查可以发生在取第一条记录之前。否则空结果恰好绕过校验,下一次出现真实订单时才暴露歧义。示例的空查询验证了这一边界。
行工厂的设置时间也要核对
开头先创建旧游标,再设置连接的 row_factory。旧游标即使随后才执行查询,仍返回 tuple,因为连接设置只作为新游标的初始值;它不会追溯修改已有游标。连接的 execute 会创建新游标,所以第二次读取才得到 Row。
需要改变已有游标时,可明确设置它自己的 row_factory,代码最后也验证了这一点。更容易维护的顺序是连接建立后立即设置工厂,再创建业务游标。取到的行对象不会因为后来修改工厂而自动变形。
如果只剩转换后的字典,第二列的值已经无法从该字典恢复。应回到仍保留位置值的原行,或重新执行修正后的查询,不要凭另一张表的编号猜补。回归样本应同时保留预期列名与对应位置,便于发现查询出口何时改变。
最终应同时检查输出名称、值的对应关系与游标返回类型。不要靠重复字段中恰好相等的值证明正确,也不要把换一种字典构造方式当成消歧。让每个业务字段在查询出口就有唯一名称,后续序列化才有稳定依据。
参考资料
资料核对日期:2026年10月2日。示例在 Linux、Python 3.12.14、SQLite 3.53.1 中独立运行。


