qinghuan
← 回到知识探索

后端现场 / 动手实验

表里有三行,为什么 COUNT 只数出两条?

先分清没填写、填了零和空文字,再看筛选条件与平均数究竟用了哪些记录。

展开本文目录
  1. 先动手看看
  2. 没写,不等于写了零
  3. “不等于十八”也不会自动收下缺失值
  4. 用三行数据对照三种统计
  5. 没有平均值时,别悄悄写成零
  6. 记住这三点
  7. 别把例子用过头
  8. 自己试一小步
  9. 查原始资料
  10. 找一份资料继续练
开始前,会这些就够了
  • 认识数据库表、列和简单 SELECT;代码使用 Python 3.10+ 标准库
遇到不熟悉的词,先看这 2 个解释
NULL
NULL 表示这一格没有一个普通的已知值。它不是数字零;要判断一格是不是 NULL,使用 IS NULL,而不是普通的等号。
JOIN(表联接)
按条件把两张表的行配在一起。一行可以与另一边多行配对,因此结果行数可能增加。
打开完整术语小抄 ↗

假设有三张练习记录:第一张没填写数值,第二张填了零,第三张填了十八。你问“一共有几张”,答案应看记录数;你问“填了数的有几张”,就得换一种数法。SQL 的 COUNT(*) 与 COUNT(列名),正好在回答不同的问题。

本篇只用 SQLite 的内存数据库。我们约定 NULL 表示这一格没有填写,Python 写入时用 None。它不是默认的零,也不是空字符串;至于为什么缺失,仍需要业务自己记录,NULL 不会替你解释原因。

这一行留下来了,数值也参与平均了吗?

从 NULL、0、18 三行开始。先选择数据,再选择 WHERE 条件;保留的行用来计数,其中非 NULL 的数值才参与平均,真实的 0 也要算进去。

没有筛选条件,所有原始行都保留。

对应的查询:SELECT COUNT(*), COUNT(amount), AVG(amount) FROM readings;

表里仍展示全部原始行,去留由文字标明。窄屏可在表格内左右滑动。

先筛选,再统计;NULL 与真实的 0 分开显示
记录amount 原值条件结果这行去留AVG 分母
记录 1NULL(未填写)未筛选保留NULL 不计入
记录 20(真实数值)未筛选保留计入 1 个数值
记录 318未筛选保留计入 1 个数值

当前数据:原始三行:NULL、0、18;条件:全部记录(不写 WHERE)。原始 3 行,保留 3 行。

COUNT(*)
3筛选后保留的行数
COUNT(amount)
2保留行中的非 NULL 值数,包含真实的 0
AVG(amount)
9.0018 ÷ 2,显示到两位小数

平均数分母:2 个非 NULL 数值。

NULL 没有被悄悄补成 0;显示为 0 的真实数值,仍占平均数的一个分母位置。

这是按本文整数与 NULL 规则手工计算的展示,浏览器里没有运行 SQLite。示例只比较这些固定条件,不演示文本或二进制值的自动转换,也不是完整 SQL 引擎。上面的“对应查询”用于帮助对照含义。

没写,不等于写了零

零是一个明确的数值,可以参与计算。空字符串是长度为零的文字;在 SQLite 中,它仍不同于 NULL。先把这三种状态分开,才不会把“没测到”悄悄改成“测得零”。

判断这一格是不是 NULL,用 IS NULL;判断不是 NULL,用 IS NOT NULL。示例先分别检查 NULL、数字零和空字符串,输出中的 1 表示判断成立,0 表示不成立。

“不等于十八”也不会自动收下缺失值

普通的等于、不等于比较碰到 NULL,结果通常仍是 NULL,可理解为当前无法判断。WHERE 只留下判断为真的行,所以 amount = NULL 找不到这里的缺失记录,amount <> 18 也不会把它带回来。

若需求确实是“数值不是十八,或者尚未填写”,就把两部分明确写成 amount <> 18 OR amount IS NULL。不要把筛选后少了一行,误当成数据库删掉了原记录。

用三行数据对照三种统计

代码里,COUNT(*) 数全部行,COUNT(amount) 只数该列不是 NULL 的行。AVG(amount) 在这组整数与 NULL 中,只平均实际填入的零和十八,因此使用两个值;真实的零照样占一个分母位置。

最后两次删除只发生在刚创建的内存表中:先留下那一行 NULL,再清成空表。每次都重新打印统计,让“有记录但没有数值”和“根本没有记录”分开可见。

展开收起代码 · 小实验python · 40 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

小实验 · python
import sqlite3
from contextlib import closing


def counts(db):
    return db.execute(
        "SELECT COUNT(*), COUNT(amount), AVG(amount) FROM readings"
    ).fetchone()


def main():
    with closing(sqlite3.connect(":memory:")) as db:
        db.execute("CREATE TABLE readings (id INTEGER PRIMARY KEY, amount INTEGER)")
        db.executemany(
            "INSERT INTO readings VALUES (?, ?)",
            [(1, None), (2, 0), (3, 18)],
        )
        print("NULL / zero / empty IS NULL:", db.execute(
            "SELECT NULL IS NULL, 0 IS NULL, '' IS NULL"
        ).fetchone())

        queries = [
            ("= NULL", "SELECT id FROM readings WHERE amount = NULL ORDER BY id"),
            ("IS NULL", "SELECT id FROM readings WHERE amount IS NULL ORDER BY id"),
            ("<> 18", "SELECT id FROM readings WHERE amount <> 18 ORDER BY id"),
            ("<> 18 OR IS NULL",
             "SELECT id FROM readings WHERE amount <> 18 OR amount IS NULL ORDER BY id"),
        ]
        for label, sql in queries:
            print(label + ":", [row[0] for row in db.execute(sql)])

        print("all rows (rows, values, average):", counts(db))
        db.execute("DELETE FROM readings WHERE amount IS NOT NULL")
        print("only NULL:", counts(db))
        db.execute("DELETE FROM readings")
        print("empty table:", counts(db))


if __name__ == "__main__":
    main()

怎样运行:Python 3.10+ 标准库;保存为 null_counts.py,运行 python null_counts.py。只操作新建的内存数据库。

运行后对照结果
NULL / zero / empty IS NULL: (1, 0, 0)
= NULL: []
IS NULL: [1]
<> 18: [2]
<> 18 OR IS NULL: [1, 2]
all rows (rows, values, average): (3, 2, 9.0)
only NULL: (1, 0, None)
empty table: (0, 0, None)

没有平均值时,别悄悄写成零

全是 NULL 时,有行数却没有可参与平均的值;空表连行数也没有。本例两种情况的 AVG 都返回 SQL 的 NULL,读取到 Python 后显示为 None。报表可写“暂无有效数值”,并把行数和有效值个数一起列出。

把缺失值补成零会改变问题和平均数,应先说明这项业务约定。AVG 也不是通用清洗工具:SQLite 会把不能读成数字的字符串或二进制值按零参与其计算。本例没有混入这些值;换数据库时,空字符串和类型转换规则也要另外核对。

记住这三点

  • NULL、数字零和空字符串要分开;判断 NULL 使用 IS NULL。
  • 普通比较不会自动包含缺失值,WHERE 是否留下它需要明确表达。
  • COUNT(*) 数行,COUNT(列名) 数非 NULL 值;平均数还要说明分母。

别把例子用过头

  • 本篇验证的是 SQLite;不要把空字符串和自动类型转换规则直接推广到所有数据库。
  • None 是这里读取 SQL NULL 后的 Python 表示,不是计算出来的零。
  • 少量记录的平均数不说明缺失原因,也不能证明样本足以代表全部情况。

自己试一小步

如果再加入一条未填写记录和一条真实的零,怎样向读报表的人解释统计变化?

  1. 先分别预测总行数、有效值个数和平均数,不运行代码时就写在纸上。
  2. 只修改插入数据,再对照三项统计;保留缺失值与真实零各自的身份。
  3. 为这份虚构报表写一句说明,交代平均的对象和仍然不知道的事。

做到这些,就算完成

  • 没有用 COUNT(列名) 冒充全部记录数。
  • 没有未经说明就把缺失值填成零。
  • 能够解释筛选条件是否应当保留未填写记录。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。

  • SQLite:SQL Language Expressions (新标签页)↗SQLite · 英文

    Operators 与 Boolean Expressions:普通比较的 NULL 结果、IS/IS NOT 对 NULL 的处理及布尔判断。

    2026-09-26 已读第2节与第14节;本文使用 IS NULL,不把 SQLite 的任意 IS 比较写法推广为其他数据库语法。

  • SQLite:SELECT / WHERE clause filtering (新标签页)↗SQLite · 英文

    第2.3节:WHERE 只保留结果为真的行,假与 NULL 都排除;第2.4节说明无分组聚合的结果行。

    2026-09-26 已读 WHERE 过滤与聚合结果生成段;示例只有单表查询,没有演示连接或分组规则。

  • SQLite:Built-in Aggregate Functions (新标签页)↗SQLite · 英文

    avg(X)、count(X)、count(*):非 NULL 值与总行数的不同统计范围、空平均及非数值输入处理。

    2026-09-26 已读三项函数定义;代码用原创三行数据,并额外执行全 NULL 与空表边界。

找一份资料继续练

先选一项,按下面的范围继续学;不必从头读完整门课程。