开始前,会这些就够了
- 知道数据库的一列可以保存多个值。
- 了解字符串、整数和空值的区别。
- Python 3.10+;内置 SQLite 必须为 3.37.0 或更新版本。
- 需要补基础时:先熟悉 SQL 与数据库约束
遇到不熟悉的词,先看这 1 个解释
- 类型亲和
- SQLite 普通表的列类型会影响它尝试怎样存储和转换输入,但不一定把每个值限制为一种存储类型。要区分列的声明、值实际存成什么,以及业务允许什么。
库存数量列写着 INTEGER,导入程序却成功放进了 many。也许驱动没有忽略建表语句,而是你把 SQLite 普通表的类型规则理解成了另一种数据库的规则。
这次把五个完全相同的输入送进三张表,并同时读取值与 typeof 结果。这样能分别看见:数据存成什么类型、能否为空,以及数值是否满足库存规则。
把例子带到自己的电脑上
下载完整练习包
需要 Python 3.10 或更新版本,无第三方依赖;制作时实测 Windows / Python 3.14.0;内置 SQLite 需 3.37.0 或更新版本。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 5 个文件包里有什么
- demo.py:完整可运行示例
- README.md:运行说明与独立练习
- observed.stdout.txt:本次实际输出
- verification.json:源码和产物验证记录
- SHA256SUMS.txt:内容摘要
在这个文件夹打开 PowerShell,逐行运行
python --version
python -c "import sqlite3; print(sqlite3.sqlite_version)"
python -B demo.py解压到独立练习文件夹,在含 demo.py 的目录打开终端运行。SQLite内存库三种表规则、ANY差异与18项检查;未测试其他数据库和旧表迁移
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
普通列的声明,先影响转换倾向
在 SQLite 普通表中,INTEGER 声明让这一列偏向用整数保存合适的输入,这种偏向叫类型亲和。它并不单独构成“任何文字都必须拒绝”的承诺。参数 '012' 可以被解释为整数十二,于是落库后读到的是 12。
'many' 无法按这里的规则转为整数,普通 INTEGER 列会把它作为文本保存。脚本同时查询 qty 和 typeof(qty),就是为了避免只看打印结果猜类型;数字十二与字符串 '12' 在某些显示界面上看起来一样,后续比较和汇总却未必一样。
示例中的值全部通过占位符绑定,没有用拼接 SQL 制造错误。参数绑定解决的是值如何进入 SQL,不负责替你决定这一列可以接受什么业务数据。这两层职责要分别验证。
STRICT 拒绝不能转换的值,但仍允许合适的转换
STRICT 是按表启用的严格类型模式,从 SQLite 3.37.0 起提供。先运行 python --version,再运行 python -c "import sqlite3; print(sqlite3.sqlite_version)";第二条显示当前 Python 实际使用的 SQLite 版本,必须至少为 3.37.0。若版本过旧,请换用内置 SQLite 符合要求的 Python 环境,再执行本例。
同样输入 '012' 和 '12.0',STRICT INTEGER 列都能保存为整数 12;输入 'many' 则被拒绝。因此 STRICT 不是“Python 传来的对象必须本来就是 int”,也不是不做任何转换。数据库检查的是是否能按目标类型接受该值。
如果接口契约规定数量必须直接提交 JSON 整数,字符串 '12' 也应拒绝,那项检查需要在接口解析和验证层表达。不能仅通过这一张 STRICT 表反推出原始请求使用了哪一种 JSON 类型。
类型、必填和取值范围要各自写清
第二张表的 STRICT INTEGER 仍能接受 None 和 -2。空值是否允许由 NOT NULL 决定,负数是否允许由业务约束决定。第三张表因此同时声明 INTEGER NOT NULL CHECK(qty >= 0)。
三列输出构成一个小矩阵:'many' 在严格类型这一步被拦住;None 需要非空要求;-2 本来就是整数,却不符合非负库存。看到某次插入失败时,应说明究竟违反了哪一类要求,而不要把所有拒绝都归功于 STRICT。
本例的三张表都故意只有一个字段,便于对照。真实业务还可能需要跨字段规则,例如售出数量不能高于总数;类型检查并不会自动建立这些关系。数据库约束与入口校验也可互相补充,而不是二选一。
编号中的前导零有含义时,先别把它当数量
脚本另建两个 ANY 列,输入 '00123'。普通表的 ANY 按其亲和规则得到整数 123,STRICT 表的 ANY 保留文本 '00123'。相同类型名在这两种表模式下的行为并不相同,不能只看英文名字猜测。
如果 '00123' 是门店编码或固定格式编号,前导零可能属于标识本身;如果它是库存数量,转换为 123 可能正合要求。选类型前先写下这些字符是否可以无损丢掉,而不是见到全是数字就一律用整数。
本例用 ANY 观察保留行为,并不建议把所有列改成 ANY。一个明确只允许文本编码的业务字段,通常应直接表达其文本类型及长度、格式要求;保留多种存储类型是另一种明确的设计选择。
一次运行,保存三张表的接受矩阵
保存为 demo.py 后运行 python -B demo.py。程序只建立内存数据库,运行结束即释放。输出每一行都保留原始 Python 输入的表示,并打印数据库读出的值与存储类型。
本次实测 Python 3.14.0、SQLite 3.50.4,18 项检查全部完成。最后的 integrity_check 只用于确认当前练习数据库;最有解释力的证据仍是每个输入分别在三张表中的结果。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
"""Observe SQLite storage classes, coercion, STRICT, and business constraints."""
from contextlib import closing
import sqlite3
def main():
if sqlite3.sqlite_version_info < (3, 37, 0):
raise SystemExit("This experiment requires SQLite 3.37.0 or later.")
checks = 0
def check(condition, message):
nonlocal checks
if not condition:
raise AssertionError(message)
checks += 1
with closing(sqlite3.connect(":memory:", isolation_level=None)) as db:
db.execute("CREATE TABLE ordinary(qty INTEGER)")
db.execute("CREATE TABLE strict_qty(qty INTEGER) STRICT")
db.execute("CREATE TABLE contract(qty INTEGER NOT NULL CHECK(qty >= 0)) STRICT")
def insert(table, value):
# Table names are fixed program constants, not user input.
try:
db.execute(f"INSERT INTO {table}(qty) VALUES(?)", (value,))
except sqlite3.IntegrityError:
return "rejected"
row = db.execute(
f"SELECT qty, typeof(qty) FROM {table} ORDER BY rowid DESC LIMIT 1"
).fetchone()
return row
cases = [
("012", (12, "integer"), (12, "integer"), (12, "integer")),
("12.0", (12, "integer"), (12, "integer"), (12, "integer")),
("many", ("many", "text"), "rejected", "rejected"),
(None, (None, "null"), (None, "null"), "rejected"),
(-2, (-2, "integer"), (-2, "integer"), "rejected"),
]
for value, expected_plain, expected_strict, expected_contract in cases:
ordinary = insert("ordinary", value)
strict = insert("strict_qty", value)
contract = insert("contract", value)
check(ordinary == expected_plain, f"ordinary case {value!r}")
check(strict == expected_strict, f"strict case {value!r}")
check(contract == expected_contract, f"business contract case {value!r}")
print(f"input={value!r}: ordinary={ordinary}; strict={strict}; contract={contract}")
db.execute("CREATE TABLE any_plain(value ANY)")
db.execute("CREATE TABLE any_strict(value ANY) STRICT")
for table in ("any_plain", "any_strict"):
db.execute(f"INSERT INTO {table} VALUES(?)", ("00123",))
any_plain = db.execute("SELECT value, typeof(value) FROM any_plain").fetchone()
any_strict = db.execute("SELECT value, typeof(value) FROM any_strict").fetchone()
check(any_plain == (123, "integer"), "ordinary ANY applies numeric affinity")
check(any_strict == ("00123", "text"), "STRICT ANY preserves the input storage class")
print(f"ANY input='00123': ordinary={any_plain}; strict={any_strict}")
check(db.execute("PRAGMA integrity_check").fetchone()[0] == "ok", "integrity check")
print(f"PASS checks={checks}")
if __name__ == "__main__":
main()
怎样运行:在独立练习文件夹中执行 python -B demo.py。2026-10-17 已在 Windows、Python 3.14.0 运行此份源码并通过自带检查。SQLite内存库三种表规则、ANY差异与18项检查;未测试其他数据库和旧表迁移
运行后对照结果
input='012': ordinary=(12, 'integer'); strict=(12, 'integer'); contract=(12, 'integer')
input='12.0': ordinary=(12, 'integer'); strict=(12, 'integer'); contract=(12, 'integer')
input='many': ordinary=('many', 'text'); strict=rejected; contract=rejected
input=None: ordinary=(None, 'null'); strict=(None, 'null'); contract=rejected
input=-2: ordinary=(-2, 'integer'); strict=(-2, 'integer'); contract=rejected
ANY input='00123': ordinary=(123, 'integer'); strict=('00123', 'text')
PASS checks=18已有表要调整时,先盘点现在存了什么
准备收紧现有数据库之前,可先按 typeof 和业务条件统计异常值,并抽查其来源。直接假设“列早就写了 INTEGER,所以里面都是真整数”,会把待清理的数据遗漏掉。
本文没有对旧业务库执行表迁移,也没有测 MySQL 或 PostgreSQL 的转换规则。迁移时应另外核对目标数据库版本、驱动绑定方式、旧数据修复和失败回退;这份 SQLite 矩阵只能作为你设计对应实验的方法。
记住这三点
- SQLite 普通列的类型亲和与严格拒绝不兼容值是不同规则。
- STRICT 仍可能进行允许的转换,也不代替 NOT NULL 和业务范围。
- 检查落库类型与原始输入,才能解释前导零为什么消失。
别把例子用过头
- 把绑定参数与业务校验当成同一件事。
- 认为 STRICT INTEGER 会拒绝所有看起来像数字的字符串。
- 认为存成整数就一定是合法库存。
- 把本例 SQLite 规则套用到未实际测试的其他数据库。
自己试一小步
一份商品导入表同时有库存数量与六位门店编码,怎样让 '000123' 在两列中得到各自需要的处理?
- 先分别写清两个字段是否允许空值、前导零和小数。
- 为普通表、STRICT 表及带业务约束的表列出预测结果。
- 加入整数、数字字符串、无法转换的文本、空值与一个超出范围的输入。
- 运行后同时保存原始输入、数据库值、typeof 和拒绝原因。
做到这些,就算完成
- 编码与数量不能只因为都由数字组成就使用相同规则。
- 说明哪些要求在接口层检查,哪些由数据库负责。
- 逐项对照预测与实测;若有差异,解释转换发生在哪里;若全部一致,选择一组最容易混淆的输入,说明判断依据。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- SQLite:Datatypes 与类型亲和 (新标签页)↗
核对普通表存储类、INTEGER
/NUMERIC亲和与输入转换。 - SQLite:STRICT Tables (新标签页)↗
核对版本下限、严格转换规则和 ANY 的模式差异。
- SQLite:CREATE TABLE 的列约束 (新标签页)↗
支持将 NOT NULL、CHECK 与类型规则分开表达。
- SQLite:typeof 函数 (新标签页)↗
支持观察读出的值属于哪种存储类。