DEBUG.WIKI
丽萨主机;为跨境业务选好 IP;原生 / 住宅 IP · 按套餐选择;查看 IP →
阿里云;从建站到云上业务;云服务器 · 数据库 · 对象存储;查看产品 →
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

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 当作“没有记录”。