Python sqlite3 复用游标:外层查询还有两行,为什么内层查一次就读不到了

前天 3阅读

循环读取任务清单时,内部再查一次数量,处理完第一条以后循环就结束了。表里明明还有数据,单独执行同一查询也能返回全部记录。这时值得检查是否把同一个 Cursor 同时用于外层遍历和内层查询:游标管理当前执行及取行状态,再次 execute 会让后续读取转向新结果,不替你保存一份旧结果队列。

下面先取外层查询的第一行,再用相同游标查总数,随后比较剩余结果。为了把注意力放在游标上,连接显式使用 autocommit=True,要求 Python 三点十二或更新版本;所有表都在内存中。把代码保存为 demo.py,执行 python demo.py,无需外部数据库。

Python sqlite3 复用游标:外层查询还有两行,为什么内层查一次就读不到了

AI生成概念插图:同一个阅读架换上新卷后旧阅读进度失去位置,独立阅读架保留各自的卷;不是数据库界面或运行截图。

import sqlite3

db = sqlite3.connect(":memory:", autocommit=True)
try:
    db.execute("CREATE TABLE item(id INTEGER PRIMARY KEY)")
    db.executemany("INSERT INTO item VALUES(?)", [(1,), (2,), (3,)])
    cursor = db.cursor()
    first_result = cursor.execute("SELECT id FROM item ORDER BY id")
    assert first_result is cursor
    first = first_result.fetchone()
    cursor.execute("SELECT count(*) FROM item")
    total = cursor.fetchone()[0]
    remaining = first_result.fetchall()
    assert first == (1,) and total == 3 and remaining == []
    print("first:", first[0])
    print("count:", total)
    print("remaining:", remaining)
    cursor.close()

    outer, inner = db.cursor(), db.cursor()
    pairs = []
    for (item_id,) in outer.execute("SELECT id FROM item ORDER BY id"):
        count = inner.execute("SELECT count(*) FROM item").fetchone()[0]
        pairs.append((item_id, count))
    assert pairs == [(1, 3), (2, 3), (3, 3)]
    print("independent cursors:", pairs)
    outer.close()
    inner.close()

    saved = db.execute("SELECT id FROM item ORDER BY id")
    another = db.execute("SELECT count(*) FROM item")
    assert saved is not another
    assert another.fetchone() == (3,)
    assert saved.fetchall() == [(1,), (2,), (3,)]
    saved.close()
    another.close()
    print("connection shortcut preserved the first result")
finally:
    db.close()

变量名不同,也可能仍是同一个游标

代码断言 first_result is cursor,随后再次执行 COUNT 查询。打印 first 是一,count 是三,而 remaining 是空列表。空列表不是旧查询突然只剩一行,也不是数据库删除了数据,而是第二次查询的唯一结果已经被 fetchone 取走;后面的 fetchall 继续读取的是第二次查询。

把第一次 execute 的返回值放进另一个变量不能隔离状态,因为返回的仍是这个 Cursor。你可以给它取两个名字,但它们都指向同一个对象。排查时直接检查对象身份,比只看变量名称更可靠。这个现象也解释了为什么把内层代码提取成函数后,问题可能仍然存在:函数接收的还是外层那个游标。

对照实验创建 outer 与 inner 两个游标,外层逐行遍历时,内层用自己的一份执行状态读取数量,最终得到一、二、三各配总数三。它没有改动任何记录,因此明确验证了结果消费状态的独立性。两个游标属于同一个连接,不代表它们自动拥有彼此隔离的事务。

选择隔离方式时也看数据规模

连接的 execute 快捷方法每次会创建并返回一个新的游标。代码最后保留第一个快捷调用的返回对象,再通过连接执行计数,确认两个游标身份不同,而第一个查询仍能取回完整三行。这种写法适合简单脚本,但返回值的生命周期仍然需要清楚;代码也显式关闭本次创建的游标。

对于规模可控的小结果,还有一种办法是先 fetchall 保存为列表,再做内层查询。此时外层遍历的是已经取出的普通数据,不再依赖游标当前的读取状态。代价是一次性把结果放进内存,因此不能在数百万行的导出任务里只为图省事就照搬。应根据结果大小和后续查询需求决定。

更值得先问的是内层查询是否有必要。每读一条任务就查相同的总数,会重复执行同一个问题;可以在循环前查一次。如果内层查的是每条任务的关联明细,也可以考虑用一次合适的连接或聚合表达需求。独立游标解决状态覆盖,并不会自动消除逐条查询带来的额外工作。

本文特意把第二次语句设成只读查询,避免把边读边写、锁、快照或者事务可见性混到同一个实验里。若生产流程会修改正在扫描的表,应另外设计读取与写入边界,不能从“换成两个游标就能遍历完整”推导出任何写入过程都安全或结果固定。

验收时保留四个观察点:外层查询的第一行、内层结果、外层剩余结果以及最终处理编号清单。只比较最后的总数可能把漏处理隐藏起来。明确谁拥有哪一个正在消费的结果,通常就能把这种看似偶发的少行问题还原成可重复的对象状态变化。

资料核对日期:2026年10月2日(北京时间)。示例在本地 Python 3.12.14、SQLite 3.53.1 实际运行并通过断言,结果仅对应文中给定输入。

参考资料

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