开始前,会这些就够了
- 认识数据库表、列和简单 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;
表里仍展示全部原始行,去留由文字标明。窄屏可在表格内左右滑动。
| 记录 | amount 原值 | 条件结果 | 这行去留 | AVG 分母 |
|---|---|---|---|---|
| 记录 1 | NULL(未填写) | 未筛选 | 保留 | NULL 不计入 |
| 记录 2 | 0(真实数值) | 未筛选 | 保留 | 计入 1 个数值 |
| 记录 3 | 18 | 未筛选 | 保留 | 计入 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,再清成空表。每次都重新打印统计,让“有记录但没有数值”和“根本没有记录”分开可见。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
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 表示,不是计算出来的零。
- 少量记录的平均数不说明缺失原因,也不能证明样本足以代表全部情况。
自己试一小步
如果再加入一条未填写记录和一条真实的零,怎样向读报表的人解释统计变化?
- 先分别预测总行数、有效值个数和平均数,不运行代码时就写在纸上。
- 只修改插入数据,再对照三项统计;保留缺失值与真实零各自的身份。
- 为这份虚构报表写一句说明,交代平均的对象和仍然不知道的事。
做到这些,就算完成
- 没有用
COUNT(列名)冒充全部记录数。 - 没有未经说明就把缺失值填成零。
- 能够解释筛选条件是否应当保留未填写记录。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- SQLite:SQL Language Expressions (新标签页)↗
Operators 与 Boolean Expressions:普通比较的 NULL 结果、IS
/ISNOT 对 NULL 的处理及布尔判断。 - SQLite:SELECT / WHERE clause filtering (新标签页)↗
第2.3节:WHERE 只保留结果为真的行,假与 NULL 都排除;第2.4节说明无分组聚合的结果行。
- SQLite:Built-in Aggregate Functions (新标签页)↗
avg(X)、count(X)、count(*):非 NULL 值与总行数的不同统计范围、空平均及非数值输入处理。
找一份资料继续练
先选一项,按下面的范围继续学;不必从头读完整门课程。
Microsoft 中文:COUNT 到底数的是哪些行 (新标签页)↗
只比较三种 COUNT,交出三行数据对应的 SQL、结果与计数对象说明。
查看学习准备与访问说明 →