跳至内容

6.1 规约不是口号

数据库规约最容易写,也最容易失效。“SQL 必须高效”“事务尽量短”“禁止复杂查询”都很像正确的话,却没有告诉执行者:什么叫高效,什么情况下必须阻断,怎样证明事务已经足够短,复杂是语法复杂还是计划代价高。这样的句子不能被机器检查,评审者之间也无法稳定复现判断,最后只剩资历和语气在决定结果。

可执行规约必须把判断过程显式化。本节先不急着罗列 PostgreSQL 技巧,而是定义规则本身的工程合同。

6.1.1 从事故、评审和测量中形成规则

一条规则应当从可描述的失败机制出发。输入通常来自三类渠道:

输入它提供什么常见误区
事故与险情真实损失、传播路径、原有控制为何失效用一次事故无限外推所有场景
代码/变更评审重复争议、接口漂移、维护成本把 reviewer 个人风格写成安全要求
测量与实验计划、等待、WAL、容量、错误码、耗时分布用一次样本或单一环境宣称普遍规律

例如,“脚本连接数据库后应先做 context guard”不是因为显式检查看起来严谨,而是因为 ch02 已经展示:同一组合法 SQL 可以成功连接到错误 database、错误 role 或 standby。失败机制是目标身份未被证明,后果是对错误对象执行正确动作,检测信号则是 current_database()current_userpg_is_in_recovery() 与预期不符。由此才能形成 SAFE-CONN-001

statement:
  自动化在执行 SQL 前验证 database、effective role、
  read/write 状态与预期 search_path

scope:
  scripts, migrations, operations

failure:
  wrong-target execution

check:
  wrong-target probe 必须非零退出;运行证据保存连接事实

反过来,若团队只是觉得 textvarchar(n) 更“PostgreSQL”,它最多是候选偏好。第 4 章给出的证据是:没有长度业务合同的时候,varchar(n) 多引入一个并不属于模型的不变量;但若字段协议确实规定最大长度,或者跨系统交换需要在数据库边界拒绝超长值,varchar(n) 或显式 CHECK 都可能合理。因此本章把它记为 PREF-TEXT-001,而不是 safety。

从现象到规则的六步推导

遇到一个值得写进规范的现象时,依次问:

  1. 现象是什么:保存 query、SQLSTATE、catalog snapshot、时间窗和输入,而不是只写“数据库异常”;
  2. 失败机制是什么:名称解析、权限、快照、锁、计划估算、资源耗尽,还是外部系统语义;
  3. 影响是什么:数据错误、越权、不可用、性能退化,还是可读性成本;
  4. 范围在哪里:只约束 migration,还是所有 application query;只适用于 OLTP,还是也适用于批处理;
  5. 可检查信号是什么:source pattern、catalog fact、负向测试、运行指标或人工证明;
  6. 反例和例外是什么:在哪些前提下原失败机制不存在,偏离时用什么补偿控制。

只有完成这六步,候选规则才值得进入试行。一次事故可以提高优先级,却不能跳过适用范围;一次 benchmark 可以提供证据,却不能自动把结论变成组织底线。

证据有层次,但没有“万能证据”

本书按问题选择证据:

  • SQL 语义与数据库行为优先用 PostgreSQL 官方文档、SQLSTATE、catalog 和可重复实验;
  • 性能判断需要 EXPLAIN (ANALYZE, BUFFERS, WAL, SETTINGS)、数据分布和多次测量,不能只贴计划节点名;
  • Pigsty 声明参考版本化配置文档,生效状态再回到 inventory、playbook 输出、service 路由和 PostgreSQL 运行事实;
  • 业务不变量由领域 owner 说明,数据库证据只能证明它怎样被实现,不能替业务定义真相。

证据也有有效期。数据规模、统计分布、PostgreSQL 大版本、扩展和 Pigsty 配置变更后,原测量需要重跑。baseline 的 evidence 因此记录 chapter、artifact 和 observation,而不是只保存一个“已验证”布尔值。

6.1.2 每条规则记录动机、证据、例外和检查方式

本章 registry 的每条 rule 使用同一最小结构:

字段必须回答的问题
id / title怎样稳定引用,标题能否准确概括
level / status风险等级是什么,当前处于什么生命周期
owner谁解释、修订并承担误报/漏报
scope约束哪些代码、对象、环境与动作
statement执行者必须做什么或证明什么
rationale试图阻止哪条失败链
evidence哪个可复核产物支持判断
exception如何合法偏离,需要哪些补偿控制
checks由 automation、runtime 还是 review 验收

可在 baseline-v0.1.json 中查看完整记录,并由 baseline-schema.json 约束结构。JSON 是权威机器源;baseline-guide.md 面向人类阅读,但其 25 个 Rule ID 必须与 registry 恰好一一对应。check_baseline.py 会拒绝 ID 缺失、重复或悄悄新增。

statement 要可执行,rationale 要可反驳

比较两种写法:

坏:所有查询都要设置超时。

可执行:
所有 application/migration session 必须声明 statement_timeout、
lock_timeout 和 idle_in_transaction_session_timeout;
预算由调用场景给出,禁止依赖服务器无限默认值。

第二句仍不替团队决定“所有查询必须 30 秒”,但给出了受约束对象、需要声明的参数和禁止状态。它允许批处理用更长 statement_timeout,同时要求批处理 owner 对更长预算负责。

rationale 也不能写成“这是最佳实践”。应该写出可被证伪的机制:没有 lock_timeout 时,一个本应毫秒完成的 DDL 可能无限等待兼容锁;没有 idle_in_transaction_session_timeout 时,遗忘事务可能长期持有 snapshot/lock;没有 application_name 时,同一 user/database 的会话难以归因。如果后续证明某个环境已经用等价机制完全消除风险,就有讨论例外的基础。

exception 不是后门

四种例外模式对应不同风险:

模式含义最低要求
none不允许在当前设计内偏离改变设计,或提出规则修订
breakglass紧急、限时地跨过 safety control精确身份、owner、时间窗、补偿控制、撤销和事后复核
waiver有证据地偏离团队默认原因、范围、owner、expiry、验证与回归条件
review本来就是场景偏好reviewer 记录为什么该场景选择此方案

例外必须是显式对象,而不是聊天里的一句“这次特殊”。waiver-template.md 要求记录补偿控制、到期时间与关闭条件。过期 waiver 没有自动变成永久例外;它应阻断下一次相关变更,直到回归默认或续期。

check 要证明风险被控制

检查方式分三层:

  • automated:不依赖人类解释的结构、source 或确定性输出,例如 Rule ID、JSON shape、禁止 secret pattern;
  • runtime:连接目标后读取 session、catalog、SQLSTATE、checksum 或真实查询行为;
  • review:领域语义、代价取舍、外部副作用等目前不能可靠自动判断的证明。

自动化覆盖率高不等于规则正确。一个错误的正则可以稳定地产生误报;一个 catalog check 只能证明检查时刻的数据库状态。相反,只有 review 也不等于“无法改进”:重复评审结论应推动 fixture、lint、catalog assertion 或运行指标出现。

owner 对规则本身负责

owner 不只是审批人,还要持续回答:

  • 这条规则最近阻止了什么真实问题;
  • false positive 是否让团队开始绕过 gate;
  • 哪类 incident 暴露了 false negative;
  • 检查成本是否与风险相称;
  • PostgreSQL/Pigsty 升级后证据是否仍有效;
  • 例外是否按时关闭;
  • 规则应该收紧、降级还是废弃。

没有 owner 的规则只会不断累积。没人有权修改,就意味着没人对错误负责。

6.1.3 区分安全底线、团队默认与场景偏好

规则等级不是“强烈推荐、推荐、可选”的措辞游戏,而是由失败后果与可接受处置决定:

    flowchart TD
  A["违反后会不会直接造成<br/>数据错误、越权、不可恢复动作<br/>或不可归因事故?"] -->|是| B["Safety"]
  A -->|否| C["团队是否需要统一默认<br/>以降低组合与维护成本?"]
  C -->|是| D["Default"]
  C -->|否| E["是否只是多个正确方案间<br/>的可读性或成本选择?"]
  E -->|是| F["Preference"]
  E -->|否| G["不进入 baseline<br/>保留为知识或局部设计"]
  

Safety:要求明确停止线

SAFE-DEFR-004 要求 SECURITY DEFINER function 固定可信 search_path 并收回默认 PUBLIC 执行权,因为高权限名称解析可形成提权路径。这里不能用“团队一般喜欢 schema-qualified name”来解释;风险是权限边界被绕过,不能满足时应改用 invoker function 或重新设计。

Safety 不代表所有检查都必须在 v0.1 自动化,但未自动化必须可见。当前 SAFE-RETR-008 只有 review:第 5 章证明了 failed transaction 和外部副作用边界,却尚未构造第 10 章的并发 retry harness。把它列入 safety 是风险判断;输出 safety_non_review_count=9 是成熟度判断。两者不能混为一谈。

Default:减少无意义差异

DEFAULT-CONT-002 固定 UTF-8、UTC 和受控 search_path。这不意味着 PostgreSQL 只支持这一套组合,而是 pg36_shop 需要一个跨环境稳定默认,使 timestamp、文本和名称解析不随开发者机器变化。若某个报表必须用特定会话时区,可以申请范围明确的 waiver,仍需保存输入/输出时区并验证夏令时边界。

Default 的价值往往是降低认知和测试矩阵,而不是避免灾难。它可以被证据推翻,也应该允许不同产品线建立自己的默认。

Preference:保留工程判断

PREF-ASQL-004 不禁止 CTE、窗口函数或 LATERAL,也不强迫使用。它要求高级 SQL 让关系语义更清晰且可测试。一个一次扫描完成的窗口查询可能比多次 round trip 更易维护;一个嵌套过深、估算失真的单条 SQL 也可能应该拆开。这里需要查询合同与计划证据,不适合以关键字 lint 阻断。

偏好若被伪装成 safety,会制造大量无意义例外;真正的 safety 若被降成偏好,则让高影响风险依赖 reviewer 当天是否注意到。分级本身就是规约质量的一部分。

生命周期与版本

本章采用以下状态演进:

candidate
  → trial(在 L1/测试环境记录成本与误报)
  → active(进入版本化 baseline)
  → revised / deprecated(证据改变或被更好控制替代)

规则 statement、level、scope 或 exception 发生语义变化时必须升级 baseline 版本;只增加同一判断的证据可以追加 ledger,但仍应留下变更记录。任何 active rule 被废弃都要说明:风险已经消失、被哪个控制替代,以及旧检查何时移除。

本章的 evidence-ledger.md 把 ch01–ch05 记为 v0.1 输入,把 ch07–ch11 作为预留追加区。到 ch12,只有规则 ID、证据、自动化、例外和兼容说明共同稳定,才发布 v1.0。

本节检查清单

拿团队现有任意一条规范,若无法回答下列问题,就先降级为 candidate:

  1. 它阻止的具体失败机制是什么;
  2. 它适用于哪些对象、动作和环境;
  3. 哪个 artifact 或运行事实支持它;
  4. 哪些反例说明不能无限外推;
  5. 违反时是阻断、waiver 还是 review;
  6. 谁负责处理误报、例外与版本变化;
  7. 怎样知道控制已真正生效;
  8. 什么条件下应该修订或废弃。

这套问题比规则数量更重要。一个有证据、能检查、允许被修订的 25 条 baseline,远胜一份没人敢删也没人真正执行的 250 条“最佳实践”。


返回本章目录 · 下一节:连接与会话候选规则 · 查看全书目录 · 查看索引中心

最后更新于