6.1 规约不是口号
数据库规约最容易写,也最容易失效。“SQL 必须高效”“事务尽量短”“禁止复杂查询”都很像正确的话,却没有告诉执行者:什么叫高效,什么情况下必须阻断,怎样证明事务已经足够短,复杂是语法复杂还是计划代价高。这样的句子不能被机器检查,评审者之间也无法稳定复现判断,最后只剩资历和语气在决定结果。
可执行规约必须把判断过程显式化。本节先不急着罗列 PostgreSQL 技巧,而是定义规则本身的工程合同。
6.1.1 从事故、评审和测量中形成规则
一条规则应当从可描述的失败机制出发。输入通常来自三类渠道:
| 输入 | 它提供什么 | 常见误区 |
|---|---|---|
| 事故与险情 | 真实损失、传播路径、原有控制为何失效 | 用一次事故无限外推所有场景 |
| 代码/变更评审 | 重复争议、接口漂移、维护成本 | 把 reviewer 个人风格写成安全要求 |
| 测量与实验 | 计划、等待、WAL、容量、错误码、耗时分布 | 用一次样本或单一环境宣称普遍规律 |
例如,“脚本连接数据库后应先做 context guard”不是因为显式检查看起来严谨,而是因为 ch02 已经展示:同一组合法 SQL 可以成功连接到错误 database、错误 role 或 standby。失败机制是目标身份未被证明,后果是对错误对象执行正确动作,检测信号则是 current_database()、current_user、pg_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 必须非零退出;运行证据保存连接事实反过来,若团队只是觉得 text 比 varchar(n) 更“PostgreSQL”,它最多是候选偏好。第 4 章给出的证据是:没有长度业务合同的时候,varchar(n) 多引入一个并不属于模型的不变量;但若字段协议确实规定最大长度,或者跨系统交换需要在数据库边界拒绝超长值,varchar(n) 或显式 CHECK 都可能合理。因此本章把它记为 PREF-TEXT-001,而不是 safety。
从现象到规则的六步推导
遇到一个值得写进规范的现象时,依次问:
- 现象是什么:保存 query、SQLSTATE、catalog snapshot、时间窗和输入,而不是只写“数据库异常”;
- 失败机制是什么:名称解析、权限、快照、锁、计划估算、资源耗尽,还是外部系统语义;
- 影响是什么:数据错误、越权、不可用、性能退化,还是可读性成本;
- 范围在哪里:只约束 migration,还是所有 application query;只适用于 OLTP,还是也适用于批处理;
- 可检查信号是什么:source pattern、catalog fact、负向测试、运行指标或人工证明;
- 反例和例外是什么:在哪些前提下原失败机制不存在,偏离时用什么补偿控制。
只有完成这六步,候选规则才值得进入试行。一次事故可以提高优先级,却不能跳过适用范围;一次 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:
- 它阻止的具体失败机制是什么;
- 它适用于哪些对象、动作和环境;
- 哪个 artifact 或运行事实支持它;
- 哪些反例说明不能无限外推;
- 违反时是阻断、waiver 还是 review;
- 谁负责处理误报、例外与版本变化;
- 怎样知道控制已真正生效;
- 什么条件下应该修订或废弃。
这套问题比规则数量更重要。一个有证据、能检查、允许被修订的 25 条 baseline,远胜一份没人敢删也没人真正执行的 250 条“最佳实践”。
返回本章目录 · 下一节:连接与会话候选规则 · 查看全书目录 · 查看索引中心