Python sqlite3 查询到记录,rowcount 仍返回 -1
区分 SELECT 的结果条数与修改行数,按存在判断、完整取数或总数统计选择读取方式,避免重复 fetchall 丢掉数据。
SELECT 明明能取出记录,Python 的 sqlite3.Cursor.rowcount 却是 -1;调用 fetchall() 后再看,仍然没有变成结果条数。若程序据此判断“没有数据”,就会丢掉本来有效的查询结果。
Python 3 标准库 sqlite3 的这个属性主要报告修改行数。普通 SELECT 返回 -1,表示这里不提供查询结果计数,不代表返回了负一行,也不是空结果标志。其他数据库驱动可能有不同约定,不能套用它们的取数后计数行为。
先分清要判断存在、取数据,还是统计总数
只需要知道有没有记录时,不必先取出全部数据。以下函数假设 con 是已经打开的 SQLite 连接,jobs 是应用已有的表,默认行工厂返回元组:
def has_ready_job(con):
row = con.execute(
"SELECT 1 FROM jobs WHERE state = ? LIMIT 1",
("ready",),
).fetchone()
return row is not None
fetchone() 在没有更多结果时返回 None。使用独立的存在性查询,可以避免为了判断非空而消耗业务查询的第一行。如果应用自定义了会返回 None 的行工厂,需要先修正这一结果表示约定,不能直接照搬判断。
已经需要取出一个大小可控的完整结果集时,保存 fetchall() 的返回值,再数列表长度:
def load_ready_jobs(con):
rows = con.execute(
"SELECT id, state FROM jobs WHERE state = ? ORDER BY id",
("ready",),
).fetchall()
return rows, len(rows)
此时 rows 才是后续处理的数据。不要先用 len(cursor.fetchall()) 判断,再对同一个游标调用一次 fetchall();第一次已经取走全部剩余记录,第二次只会得到空列表。
只要总数,就让 SQL 返回计数值
不需要每一行内容时,使用带相同筛选条件的聚合:
def count_ready_jobs(con):
row = con.execute(
"SELECT COUNT(*) FROM jobs WHERE state = ?",
("ready",),
).fetchone()
return row[0]
这里无 GROUP BY,查询返回一行,第一列才是符合条件的记录数;没有匹配记录时值为 0。查看这个计数查询的 rowcount 仍然不能得到目标条数。也不要随意改成 COUNT(nullable_column),后者会忽略该列为 NULL 的行。
计数必须与所需结果含义一致。原查询有连接、DISTINCT、分组或分页时,仅对某张基础表执行 COUNT(*) 可能数到别的东西。需要数完整查询输出时,可把相应查询作为子查询再计数,并明确是否包含分页限制。
若随后再执行另一条查询取数据,两个结果也不天然属于同一观察时点。其他连接可能在两次读取之间写入;需要一致快照的业务应沿用经过设计的事务边界,不要靠这次行数修复偷偷提交或重开业务事务。
流式处理时在消费过程中计数
结果很大、需要逐行处理时,可以在处理循环中累计,避免为了计数把整批结果装进列表:
def visit_ready_jobs(con, handle):
count = 0
cursor = con.execute(
"SELECT id, state FROM jobs WHERE state = ? ORDER BY id",
("ready",),
)
for row in cursor:
handle(row)
count += 1
return count
返回的是成功走完这些处理步骤后的计数;handle 抛出异常时,函数不会返回完整总数。游标已被消费,也不能再把它交给另一个循环期待重新得到同一批行。
不要把问题推广成“sqlite3 的 rowcount 永远无效”。INSERT、UPDATE、DELETE 和 REPLACE 有自己的修改行数规则;会返回记录的写语句还涉及语句完成与取完结果的时机。这与普通 SELECT 不提供结果条数是两回事。
在测试数据库中分别核对零行、一行和多行,确认存在判断、总数和实际处理记录一致,并检查先取一行再取剩余行的调用路径。此次变更只调整读取方式,不要求改表或清空数据;保留旧业务代码可回退,但不能继续把 -1 当作“没有记录”。


