SQLite 标量子查询:查到两条配置,为什么没有报错却只返回一个值

前天 3阅读

应用通过一个嵌套查询读取主题颜色,配置表里误留了两条同名记录,页面却没有报错,只悄悄显示其中一个颜色。如果预期“一条配置只能对应一个值”,查询成功反而可能掩盖重复数据:SQLite 的标量子查询不会自动替你验证行数唯一。

一个表达式值,不等于底层只有一行

放在表达式位置、只返回一列的子查询,会把内部结果的第一行当作自己的值。内部没有任何行时,表达式值是 NULL;内部有多行时,SQLite 仍取第一行。因此单个返回值描述的是表达式的形状,不能当作数据满足唯一约束的证据。

下面创建内存配置表,color 故意出现两次,nullable 则只有一条且值本身为空。用相反的编号排序读取 color,检查同一批两行如何给出不同结果;再比较不存在的配置与存在但为空的配置。整个实验不会打开业务数据库。

保存为 demo.py,执行 python3 demo.py。前两条子查询故意不加 LIMIT,让多行结果可以直接证明本文行为。查询中的排序有唯一编号,预期结果不依赖数据库碰巧采用哪种扫描顺序。

SQLite 标量子查询:查到两条配置,为什么没有报错却只返回一个值

配图为 AI 生成的概念插图,用抽象物件说明本文关系,并非真实软件截图或运行输出。

import sqlite3

db = sqlite3.connect(':memory:')
try:
    db.execute('CREATE TABLE config(id INTEGER PRIMARY KEY, name TEXT, value TEXT)')
    db.executemany('INSERT INTO config VALUES (?, ?, ?)', [
        (1, 'color', 'blue'), (2, 'color', 'green'), (3, 'nullable', None),
    ])
    ascending = db.execute("""
        SELECT (SELECT value FROM config WHERE name = 'color' ORDER BY id ASC)
    """).fetchone()[0]
    descending = db.execute("""
        SELECT (SELECT value FROM config WHERE name = 'color' ORDER BY id DESC)
    """).fetchone()[0]
    assert (ascending, descending) == ('blue', 'green')
    print('two rows, scalar values:', ascending, descending)

    query = """
        SELECT
            (SELECT value FROM config WHERE name = ? ORDER BY id LIMIT 1),
            (SELECT count(*) FROM config WHERE name = ?)
    """
    missing = db.execute(query, ('missing', 'missing')).fetchone()
    nullable = db.execute(query, ('nullable', 'nullable')).fetchone()
    assert missing == (None, 0)
    assert nullable == (None, 1)
    print('missing:', missing)
    print('present null:', nullable)

    duplicates = db.execute("""
        SELECT name, count(*) FROM config
        GROUP BY name HAVING count(*) > 1 ORDER BY name
    """).fetchall()
    assert duplicates == [('color', 2)]
    print('duplicates:', duplicates)
    latest = db.execute("""
        SELECT (SELECT value FROM config
                WHERE name = 'color' ORDER BY id DESC LIMIT 1)
    """).fetchone()[0]
    assert latest == 'green'
    print('explicit latest:', latest)
finally:
    db.close()

排序能选定首行,却不能证明唯一

第一行输出 blue 和 green,说明内部确实有两条候选,而子查询均能完成。升序把编号一放在前面,降序把编号二放在前面。这个实验展示的是 SQLite 的选择规则,不是建议依靠数据插入先后来猜默认首行。

省略 ORDER BY 时,行顺序没有得到保证。增加索引、调整查询或更换引擎版本后,碰巧被选中的值可能变化。即便外层查询最后写了排序,也不能倒过来规定子查询内部先选哪一行;选择规则必须写在真正需要它的位置。

最后一项明确写降序和 LIMIT 1,是为了表达“从多条记录中选择编号最大的一条”。它适合本来允许历史记录的设计,但编号是否真的代表最新版本,需要业务保证。时间相同时仍应补充稳定的次级排序,避免并列项没有明确先后。

必须唯一时,把重复当成数据问题处理

如果每个配置名按契约只能有一个值,追加 LIMIT 1 只会隐藏异常。代码中的重复检查返回 color 和数量二,让维护者能看到冲突。整理完旧数据后,可以根据真实键定义唯一约束;它负责限制重复,查询负责读取,两者职责不同。

唯一约束通常只保证至多一条,不保证每个必需配置都已存在。程序仍需要处理零行情况。若配置键本身可以为空,还必须另外决定空键是否允许,不能把名称、唯一性和必填三项要求揉成一个模糊的“查到了就行”。

missing 与 nullable 的表达式值都是 None,但计数分别为零和一。这说明单看 NULL 无法区分“没有这条配置”和“这条配置明确保存空值”。示例把计数与值放在同一条外层查询里供对照;正式接口也可以返回明确的存在状态。

排查时先单独运行内部 SELECT,看它实际可能产生几行,再检查排序与业务契约。不要只盯着外层最终只有一行。保留零行、一行、多行和单行空值四类样本,才能确认系统究竟在选择记录、检测冲突,还是处理配置缺失。

资料核对日期:2026年10月2日(北京时间)。最终展示代码在 Python 3.12.14、SQLite 3.53.1 中独立运行并通过全部断言。

官方参考

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。