SQLite sum 与 total:空表返回不同,大整数求和也不能随意互换

前天 3阅读

没有记录时,汇总值该表示什么

统计表在多数日期都有数据,偶尔某天没有记录,SUM 却返回 NULL,让调用方的加法报错。把它改成 TOTAL 看起来能直接获得零,但这还改变了返回类型和大整数计算方式。空值约定与数值范围是两件事,不能只修复空表样本就认为两者可以随处互换。

下面在 Python 3.12.14、SQLite 3.53.1 的内存数据库中检查四种输入:空表、全是 NULL、小整数和大整数。保存为 demo.py,运行 python demo.py。脚本只操作自己创建的临时表,整数溢出是有意触发并捕获的演示,所有预期行为都通过断言检查。

同时看值与 Python 接收到的类型

每个案例都先清空这张内存表,再写入明确样本。SUM 和 TOTAL 都忽略 NULL 输入,但没有任何非空值时,前者给 NULL,后者给浮点零。为了避免屏幕上的八掩盖类型差异,小整数样本还用 type 检查 SUM 是 int、TOTAL 是 float。

越界样本把有符号六十四位整数最大值与一相加。代码分别执行 SUM(x) 和 COALESCE(SUM(x), 0),证明空值替换并不能接住聚合内部的计算错误。最后使用仍处于整数范围、却超过双精度连续整数精确区间的数,观察 TOTAL 丢掉一个单位的情况。

SQLite sum 与 total:空表返回不同,大整数求和也不能随意互换

AI概念示意图:空容器对应不同返回约定,有限容器外溢则提醒检查整数范围。图片只说明概念,不是运行截图。

import sqlite3

con = sqlite3.connect(":memory:")
try:
    con.execute("CREATE TABLE samples(x INTEGER)")

    def put(values):
        con.execute("DELETE FROM samples")
        con.executemany("INSERT INTO samples VALUES (?)", [(x,) for x in values])

    for label, values in [("empty", []), ("all null", [None, None])]:
        put(values)
        row = con.execute("SELECT SUM(x), TOTAL(x) FROM samples").fetchone()
        assert row == (None, 0.0)
        print(label + ":", row)

    put([3, None, 5])
    row = con.execute("SELECT SUM(x), TOTAL(x) FROM samples").fetchone()
    assert row == (8, 8.0)
    assert type(row[0]) is int and type(row[1]) is float
    print("small:", row)

    put([(1 << 63) - 1, 1])
    failures = 0
    for sql in [
        "SELECT SUM(x) FROM samples",
        "SELECT COALESCE(SUM(x), 0) FROM samples",
    ]:
        try:
            con.execute(sql).fetchone()
        except sqlite3.OperationalError as exc:
            assert "integer overflow" in str(exc)
            failures += 1
        else:
            raise AssertionError("integer overflow expected")
    assert failures == 2
    total = con.execute("SELECT TOTAL(x) FROM samples").fetchone()[0]
    assert isinstance(total, float)
    print("overflow cases:", failures, "total:", total)

    put([1 << 53, 1])
    exact, approximate = con.execute("SELECT SUM(x), TOTAL(x) FROM samples").fetchone()
    assert exact == 9007199254740993
    assert approximate == 9007199254740992.0
    assert exact != approximate
    print("precision:", exact, approximate)
finally:
    con.close()

每次修正只解决一种问题

empty 和 all null 都打印 (None, 0.0)。这说明仅凭 SUM 的空结果无法区分没有行与有行但全部未知;若业务需要区分,应同时检查行数和非空值数量。small 打印 (8, 8.0),值相等却类型不同,之后序列化或传给要求整数的接口时可能体现差异。

overflow cases 为二,表示普通 SUM 与外层 COALESCE 都按预期报 integer overflow。COALESCE 处理已经算出的 NULL,不会把失败的计算变成有效值。TOTAL 在同一组输入上可以返回浮点数,但这不是它已经具备任意精度整数能力的证明。

precision 一行把整数 9007199254740993 与浮点 9007199254740992.0 放在一起。差一个单位在计数场景中就是实际数据损失;把后者再转 int 也无法找回原来的一。示例的 Python 比较明确断言它们不相等,避免格式化显示掩盖精度问题。

让结果类型服从业务含义

若整数汇总范围已知且不会溢出,只想把无非空输入时的 NULL 显示为零,可以有意识地使用 COALESCE(SUM(x), 0)。但如果 NULL 代表数值尚未采集,替换为零本身也会改变业务含义,应在展示或接口约定里说明,不要把未知数量当作确实没有。

需要近似实数求和时,TOTAL 的浮点结果可能符合要求;需要精确大整数时,应另选能满足范围的表示和汇总方案。TOTAL 不抛整数溢出也不代表数值范围无限,极端浮点输入还有无穷值等问题,须另外测试。本文没有安装 SQLite 扩展,也没有测试其他数据库,不能从一次成功运行推断金额或科学计算中的误差已被控制。

验收汇总查询至少保留四类样本:正常值、无值、范围边界和精度边界。先决定允许哪种结果类型,再选择聚合函数与空值处理。这样报表变得能运行的同时,计数含义、异常路径和数值精度也都有可检查的依据。

参考资料

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