SQLite 生成列:STORED 会跟着原列更新,为什么 table_info 里还找不到它

10-01 3阅读

把一行内的派生值交给数据库

一个板材清单保存宽度和高度,每次修改尺寸后都要更新面积。若不同脚本各自维护第三个普通字段,某条写入路径忘了重算,就会留下互相矛盾的数据。SQLite 的生成列能把同一行内的计算关系写进表结构,应用只修改基础字段,读取时仍像访问普通列一样取得派生结果。

生成列从 SQLite 3.31.0 开始提供。本例用 Python 标准库驱动内存数据库,实际验证于 Python 3.12.14 和 SQLite 3.53.1。宽高按毫米保存,面积按平方毫米计算,数字刻意选得很小,方便手算核验。这里讨论字段维护方式,不用几行数据推断实际性能。

存储计算结果,不是保存历史快照

VIRTUAL 在读取时计算,STORED 在写入行时计算并保存结果。两者都依赖当前基础字段;修改宽度后,存储型面积也会随之更新。把 STORED 理解成首次写入后永远不变的历史值,会造成错误的数据设计。真正的历史面积需要单独的版本记录或快照字段,并明确何时形成。

下面创建两列使用相同公式,插入十乘二十后读到两个二百,再把宽度改为十二,两个面积都变为二百四十。代码接着尝试直接写生成列,确认数据库拒绝这种操作。即使提交的数字刚好等于公式结果,也应修改基础列,而不是让应用接管生成列的写入。

SQLite 生成列:STORED 会跟着原列更新,为什么 table_info 里还找不到它

AI概念示意图:相同输入经过计算后分别形成读取时求值的结果与随行存储的结果。图片用于解释概念,不是运行截图。

import sqlite3

assert sqlite3.sqlite_version_info >= (3, 31, 0)
con = sqlite3.connect(":memory:")
try:
    con.execute("""
        CREATE TABLE panel(
            id INTEGER PRIMARY KEY,
            width_mm INTEGER NOT NULL,
            height_mm INTEGER NOT NULL,
            area_live INTEGER AS (width_mm * height_mm) VIRTUAL,
            area_saved INTEGER AS (width_mm * height_mm) STORED
        )
    """)
    con.execute("INSERT INTO panel(width_mm, height_mm) VALUES (10, 20)")
    first = con.execute("SELECT area_live, area_saved FROM panel").fetchone()
    con.execute("UPDATE panel SET width_mm = 12 WHERE id = 1")
    changed = con.execute("SELECT area_live, area_saved FROM panel").fetchone()
    assert first == (200, 200) and changed == (240, 240)
    print("areas:", first, "->", changed)
    try:
        con.execute("UPDATE panel SET area_saved = 999 WHERE id = 1")
    except sqlite3.OperationalError:
        print("direct write rejected")
    else:
        raise AssertionError("expected generated-column rejection")
    basic = [row[1] for row in con.execute("PRAGMA table_info(panel)")]
    detailed = [(row[1], row[6])
                for row in con.execute("PRAGMA table_xinfo(panel)")]
    assert basic == ["id", "width_mm", "height_mm"]
    assert detailed[-2:] == [("area_live", 2), ("area_saved", 3)]
    print("table_info:", basic)
    print("generated metadata:", detailed[-2:])
finally:
    con.close()

表结构检查也要换一个入口

输出中的 table_info 只列出编号、宽度和高度,并不是建表漏掉了面积列。生成列不会出现在这个接口的结果里,table_xinfo 才会把它们一起返回。后者多出的 hidden 字段可用于分类:本例虚拟生成列是二,存储生成列是三,普通列是零;虚拟表的隐藏列另有标记,不能全部混成同一种列。

如果你维护的是通用导入器,应从完整元数据中识别哪些列允许写入,再生成插入字段清单。只看 SELECT 返回列名就构造 INSERT,很容易把生成列也放进去;反过来只用 table_info 生成字段文档,又可能漏掉用户能查询到的派生字段。两类用途需要不同筛选。

建模前检查公式与迁移边界

生成表达式只能使用本行字段、常量和允许的确定性标量函数,不能通过子查询去汇总其他行,也不能直接把窗口计算塞进来。若面积来自另一张规格表,先考虑视图或明确的关联查询;若计算逻辑有自己的版本变化,则要规划历史结果是否跟着重算。

给已有表添加列时,ALTER TABLE ADD COLUMN 可添加 VIRTUAL 生成列,不能直接添加 STORED 生成列。选择存储形式前还要检查访问该数据库的旧客户端是否支持生成列语法。不要只升级负责写入的脚本,却让旧报表工具继续打开新结构;迁移验收需要覆盖所有实际读写入口。

参考资料

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