立木取信:开发规约与交付基线
6 立木取信:开发规约与交付基线
前五章已经留下了一批反复出现的工程判断:连接前先确认目标,运行角色不能等于对象所有者,关键不变量要进入约束,事务失败后必须显式恢复,分页需要稳定全序,实验结束要证明状态复原。它们此时还散落在不同章节里;如果只把这些句子摘成一张“最佳实践清单”,读者很快就会遇到两个问题:规则为什么成立,以及遇到例外时该听谁的。
本章不追求一份永远正确的规范,而是建立一条可持续的规则生产线:
最终产物是 pg36_shop 的开发规约 baseline v0.1。它既有人可以阅读的指南,也有机器可以校验的 JSON registry;既声明 PostgreSQL 对象与查询合同,也展示如何把角色、数据库和接入路径映射到 Pigsty。更重要的是,它公开记录目前尚未自动化的缺口,而不把“写进文档”冒充“已经落实”。
本章目标
完成本章后,读者应当能够:
- 从失败机制出发写规则,而不是从个人偏好出发写口号;
- 为每条规则补齐 owner、scope、rationale、evidence、exception 和 checks;
- 区分 safety、default 与 preference,知道三类规则采用不同的阻断和例外机制;
- 把连接目标、凭据、会话参数和
application_name组成可验证的连接合同; - 解释
search_path为什么同时是便利机制与信任边界; - 用 schema、NOLOGIN owner、命名约束和注释表达对象边界;
- 审查数据类型、默认值、标识键和时间语义,而不是机械套用类型表;
- 把“可恢复迁移”理解为兼容演进、停止线和 forward repair,而不是承诺任意 DDL 都能自动
down; - 为查询建立显式投影、稳定排序和分页合同;
- 把事务超时、整体重试、幂等与外部副作用放在一个失败模型里审查;
- 区分静态检查、catalog 验证、负向测试、并发实验与计划证据;
- 写出包含风险、停止条件、恢复路径和验收证据的数据库变更说明;
- 区分 Pigsty inventory 中的期望状态、PostgreSQL catalog 中的实际状态与 service 的实际路由;
- 运行 v0.1 质量门,读懂每个通过项和目前唯一的 safety 自动化缺口;
- 给后续 ch07–ch11 的计划、索引、并发与发布证据预留可追踪的追加位置。
baseline v0.1 的边界
本章基线含 25 条 active 规则:
| 等级 | 数量 | 默认处置 | 允许的例外 |
|---|---|---|---|
| safety | 10 | 阻断 merge/deploy | none 或受控 breakglass |
| default | 10 | 团队默认 | 有 owner、expiry 与补偿检查的 waiver |
| preference | 5 | 场景评审 | reviewer 根据证据决定 |
规则数量不是成熟度指标。v0.1 的证据只来自 ch01–ch05,范围限定为 pg36_shop 教学应用及 Pigsty L1 工作流;PostgreSQL 兼容目标为 14–18,当前真实验证版本为 18.6,Pigsty 说明以 v4.5.0 为准。Ubuntu 24.04 是 L1 目标平台,本章同时在 macOS/Homebrew 的 PostgreSQL 18.6 本地实验实例上验证 SQL 与 gate。任何内核分支、驱动、连接池模式和组织安全要求都可能收紧或改写规则边界。
v0.1 还故意保留一个可见缺口:10 条 safety 中,SAFE-RETR-008“只按 SQLSTATE 整体重试且副作用幂等”目前只有 review check,尚无自动或运行时检查。第 10 章完成并发与重试实验前,它不能被宣称为自动闭合;质量门会输出:
这里的 9 不是失败,而是一张不可被悄悄抹掉的债务凭证。若到 ch12 仍未补齐,baseline 不得升为 v1.0。
人、机器与运行时三份合同
flowchart LR
A["baseline-guide.md<br/>人类阅读与评审"] --> D["同一组 Rule ID"]
B["baseline-v0.1.json<br/>机器可读 registry"] --> D
C["evidence-ledger.md<br/>来源与未来证据"] --> D
D --> E["check_baseline.py<br/>结构 / 引用 / 安全扫描"]
E --> F["quality-gate.sh static"]
G["PostgreSQL catalog<br/>session / model / query contract"] --> H["quality-gate.sh live"]
I["故意错误上下文<br/>wrong session / target"] --> J["quality-gate.sh negative"]
F --> K["gate-summary.txt"]
H --> K
J --> K三份合同各自回答不同问题:
- 人类指南说明规则是什么意思、为何存在以及怎样申请例外;
- JSON registry 固定字段、ID、版本和证据引用,防止文档与自动化各说各话;
- 运行时 gate 连接真实 PostgreSQL,证明当前 target、session、catalog 与 query contract 符合预期。
静态通过不证明数据库已经部署;inventory 已提交不证明 playbook 已应用;catalog 正确也不证明连接流量经过了预期的 HAProxy/PgBouncer 服务。只有把配置、运行和路由证据放在一起,才能声称这条交付链已经闭合。
实验资产
下载并审查以下资产:
- 机器可读 baseline v0.1
- registry JSON Schema
- 人类可读规则指南
- 交付清单
- 证据账本
- 数据库变更说明模板
- 规约例外模板
- Pigsty cluster-vars 示例
- 连接上下文 guard
- 正确会话 profile
- 错误会话 fixture
- 查询合同验证
- baseline 结构检查器
- 统一质量门
这些资产不包含密码,也不会创建或删除数据库。live 与 negative 只在已经通过 ch04-v1 验收的可写 L1 上运行:正向检查全部只读,负向检查只故意设置错误会话参数或错误 expected database,并要求以精确 SQLSTATE 拒绝。
本章目录
6.1 规约不是口号
先建立“规则也需要证据”的方法,再定义一条规则从 candidate 到 active、waived、deprecated 的生命周期。
6.2 连接与会话候选规则
把“能连上”升级为包含目标、身份、会话语义、超时预算和可归因性的连接合同,并让错误连接真的失败。
6.3 模式与 DDL 候选规则
把前两章的逻辑模型与物理合同收敛为 DDL 评审问题,并准确界定 rollback、forward repair 和兼容发布的关系。
6.4 查询与事务候选规则
查询规约约束的是外部合同和失败语义,不是 SQL 风格偏好;高级语法也不因“高级”而自动正确或错误。
6.5 交付物与质量门
一项数据库变更只有同时携带代码、验证、失败路径、风险和 owner 才是可接手的交付物。
6.6 将规约接入统一实验环境
Pigsty 提供可复现的基础设施入口,PostgreSQL catalog 和实验后验负责证明实际状态;本节把两种证据接起来。
6.7 实战:发布规约 baseline v0.1
- 6.7.1 审查 ch01–ch05 已出现的候选规则
- 6.7.2 为
pg36_shop建立最小质量门 - 6.7.3 预留 ch07–ch11 的证据追加区
- 6.7.4 在 ch12 汇总为 v1.0 的验收条件
最后运行 static、live 与 negative 三层 gate,发布带 checksum、证据范围和已知缺口的 v0.1,而不是一份没有版本的规范文档。
章节验收
- 能把一条口号改写成有 scope、rationale、evidence、exception、checks 和 owner 的规则;
- 能解释 safety/default/preference 的差异,不用大写“必须”冒充风险分级;
- baseline guide 与 JSON registry 的 Rule ID 一一对应;
- registry 引用的 ch01–ch05 资产都存在,且五章均被实际证据覆盖;
- source 与 evidence 中没有明文凭据、credential URI 或
PGPASSWORD; - 连接 gate 能确认 database、effective role、primary、模型版本和 session profile;
- 错误会话固定以
P0601失败,错误 target 固定以P0001失败; - 查询 gate 能验证 view shape、显式稳定排序、keyset 两页不重叠以及业务键/幂等键唯一;
- 能解释 Pigsty
primary:5433与default:5436的不同使用边界; - 能从 catalog、
pg_settings和 service 路由分别验证 inventory 声明; - 能说明为什么“可恢复”通常依赖 expand/contract 与 forward repair,而非通用 down migration;
- 能指出 v0.1 的 9/10 safety enforcement 缺口及其预定闭合章节;
quality-gate.sh all生成status=ok,且保存 baseline 与关系模型 checksum。
下一章 ch07《追本溯源:执行计划与统计信息》 将开始给
PREF-PLAN-005 和查询成本审查补充第一批专门证据。
参考资料
- PostgreSQL 18:The Connection Service File
- PostgreSQL 18:The Password File
- PostgreSQL 18:Database Connection Control Functions
- PostgreSQL 18:Schemas
- PostgreSQL 18:Function Security
- PostgreSQL 18:CREATE FUNCTION
- PostgreSQL 18:Sorting Rows
- PostgreSQL 18:LIMIT and OFFSET
- PostgreSQL 18:WITH Queries
- Pigsty v4.5:User/Role
- Pigsty v4.5:Database
- Pigsty v4.5:Service/Access
上一章:运筹帷幄:查询、事务与锁的核心心智模型 · 返回上卷导读 · 下一章:追本溯源:执行计划与统计信息 · 查看全书目录 · 查看索引中心
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:
反过来,若团队只是觉得 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 要可反驳
比较两种写法:
第二句仍不替团队决定“所有查询必须 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 当天是否注意到。分级本身就是规约质量的一部分。
生命周期与版本
本章采用以下状态演进:
规则 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 条“最佳实践”。
返回本章目录 · 下一节:连接与会话候选规则 · 查看全书目录 · 查看索引中心
6.2 连接与会话候选规则
应用拿到一个 PostgreSQL connection 时,业务代码通常把它当作“数据库”。实际上,它是一组会改变 SQL 含义和失败方式的上下文:host/service 把流量送到某个 instance,database 决定 catalog 边界,role 决定权限,GUC 决定名称解析、时间展示与超时,连接池还可能让同一个 server connection 被多个 client request 复用。
因此,连接规约不能只检查 TCP 和认证成功。它必须同时约束目标、身份、语义、预算与归因。
6.2.1 连接上下文、超时与 application_name
本书把连接上下文拆成两层:
第一层表达意图,第二层证明意图落在了正确对象上。只做其中一层都不够:连接字符串写对了仍可能遇到 DNS、service 或 failover 配置错误;连接后只看 SELECT 1 又无法知道自己是谁、在哪个库、是否落到只读副本。
用 service name 固定目标身份
libpq service file 把一组连接参数绑定到一个稳定名称:
客户端以 service=pg36-admin 或 PGSERVICE=pg36-admin 连接。用户级 service file 默认为 ~/.pg_service.conf,也可以用绝对路径 PGSERVICEFILE 指定;直接连接字符串中的同名参数又会覆盖 service file 值。因此 service 是集中声明,不是不可绕过的安全边界,gate 仍须检查运行事实。
自动化入口使用:
-X 不读取用户 psqlrc,避免本地宏、变量或 SET 改变脚本;-w 禁止在无人值守任务里退回交互密码提示。第 2 章的 service file 示例 没有 secret,密码应来自 mode 0600 的 passfile 或组织 secret provider。Unix 上权限过宽的 password file 会被 libpq 忽略;这是一项客户端保护,不等于凭据已经完成轮换、审计和最小授权。
四类超时不是同一个旋钮
| 预算 | 控制的阶段 | 典型失败 |
|---|---|---|
connect_timeout |
建立连接 | 网络、DNS、endpoint 不可达 |
statement_timeout |
单条 statement 执行 | 查询/写入超过请求预算 |
lock_timeout |
等待任意单次 lock acquisition | DDL/DML 被冲突锁长期阻塞 |
idle_in_transaction_session_timeout |
transaction 中无客户端活动 | 遗忘事务长期持锁或 snapshot |
本章实验值为 5 秒连接、30 秒 statement、5 秒 lock、60 秒 idle-in-transaction。它们是 L1 教学 workload 的默认,不是生产通用答案。在线请求、报表、批处理、migration 应分别从 SLO、锁风险和恢复方式推导预算。
statement_timeout=0 和 lock_timeout=0 表示禁用超时,不表示“立刻超时”。lock_timeout 若等于或大于 statement_timeout,通常没有独立触发机会。超时也不是资源隔离:30 秒内仍可能消耗大量 CPU/I/O,后续章节还要加入并发、连接数、work memory 和 workload routing。
application_name 是归因键,不是认证身份
application_name 会出现在 pg_stat_activity,配置允许时也可进入日志。命名至少区分:
实验脚本还加入 action,例如 pg36-ch06-all。生产 trace 应在应用侧把 request/trace ID 与 backend PID、backend_start、transaction/query start 和 dashboard 时间窗关联;不要把每个 request ID 都塞进一个高基数、长度受限的 application_name。
客户端可以自行声明这个值,所以它不能替代 session_user、证书身份或审计主体。它的作用是把等待、日志和指标归到正确 workload/owner;安全判断必须使用服务器验证的身份。
由此形成两条规则:
SAFE-CONN-001:写入前验证 database、effective role、primary、search_path和模型版本;DEFAULT-SESS-001:每类 workload 声明可归因 application name 与连接/statement/lock/idle transaction 预算。
6.2.2 时区、编码、search_path 与会话状态
SQL 文本相同,不保证会话语义相同。下面这些 session state 都可能改变结果:
client_encoding决定客户端字节怎样转换为数据库字符;TimeZone改变timestamptz的输入解释和输出展示;DateStyle影响含歧义的日期文本;search_path决定未限定 table、function、type 与 operator 名称解析;- transaction isolation/read-only/deferrable 改变并发观察;
- role 与 row security 设置改变可见对象和数据;
- timeout、planner GUC 与 locale/collation 影响失败或计划选择。
本章默认固定:
UTC 是存储/接口默认,不妨碍 UI 按用户时区展示;UTF-8 是跨系统文本默认,不替代 collation 设计。若业务输入使用本地时区,接口必须同时携带 zone/offset 并测试 DST 重叠与跳跃,不能依赖 application server 的系统时区。
search_path 是信任边界
search_path 不只用于缩短表名。PostgreSQL 也按它解析 function、type 和 operator;把某个可被不受信用户 CREATE 的 schema 放入 path,就等于信任该用户可以影响未限定名称的解析。
对 application session,本书采用:
同时从 public 撤销 PUBLIC CREATE。需要注意版本和升级历史:PostgreSQL 15 新建数据库的默认权限与从 PostgreSQL 14 或更早升级的数据库可能不同,不能靠“大版本默认应该安全”代替 catalog 检查:
对 SECURITY DEFINER function 要更严格:只保留可信 schema,把 pg_temp 放到最后或明确排除不可信解析路径,敏感对象使用 schema-qualified name,并在创建 function 的同一事务中 REVOKE ALL ... FROM PUBLIC 后按需 GRANT EXECUTE。这是 SAFE-DEFR-004 的安全边界,第 4 章已有 catalog 反证。
会话默认与每次请求声明
配置可以有多个层次:
具体生效值应由 current_setting() 或 pg_settings 观察,不应只查看某一层配置文件。对稳定的 database/role 默认,可由 Pigsty pg_databases.parameters 或版本化 SQL 声明;对单次事务预算,优先在 transaction 开始后 SET LOCAL,让它随 commit/rollback 自动恢复。
在 PgBouncer transaction pooling 下,client session 与 PostgreSQL backend 不是永久一一对应。应用不能假设上一请求的 SET、临时对象、prepared statement 或 session lock 会在下一事务仍然存在,也不能让状态泄漏给后续请求。需要 session affinity 的 workload 应选择 session pooling 或 direct service,并把理由写进 waiver;普通 OLTP 则应把事务所需状态显式放进 startup/role default 或每个 transaction。
因此 DEFAULT-CONT-002 的真正要求不是“所有地方硬编码同一串 SET”,而是:
- 定义 canonical session profile;
- 选择一个可重复应用的层次;
- 在取得连接后验证关键语义;
- 对 pool reuse 不做隐式假设;
- 在 evidence 中保存实际值。
6.2.3 用错误连接案例验证规则价值
只有正例的 gate 可能永远绿色,即使检查本身已经失效。本章提供两个故意失败的 fixture:
错误会话
wrong-session.sql 主动设置:
然后要求 baseline 以自定义 SQLSTATE P0601 拒绝。gate 不是笼统检查“命令失败”,而是同时断言:
若脚本因为语法错误、认证失败或其他 SQLSTATE 退出,negative gate 仍失败;否则一个与规则无关的故障也会被误报为“安全控制成功”。
错误目标
session-profile.sql 默认期待 pg36_shop。negative action 将 expected database 改为一个确定不存在于合同中的名字:
context.sql 在任何业务读取前拒绝,预期 exit 3、SQLSTATE P0001。这证明 target guard 确实参与路径,而不是写在文件里却从未被调用。
正确会话
quality-gate.sh live 运行相同 profile,要求输出:
注意输出格式可能把 60s 规范化为 1min;验收 SQL 用 ::interval 比较语义,不比较展示文本。类似地,host、PID、server version 和 timestamp 是本次证据,不应做 golden value。
仍然没有证明什么
这个 gate 证明检查时刻的 PostgreSQL session 与 ch04-v1 模型符合预期,但没有证明:
- TLS、证书和 HBA 满足生产安全策略;
- 所有应用连接都使用同一个 profile;
- HAProxy endpoint 在 failover 后仍按预期路由;
- passfile/secret provider 的生命周期与轮换正确;
- 30 秒预算适合真实 SLO;
- PgBouncer reset 与 driver 行为没有其他差异。
这些边界必须明确写出,否则一个绿色实验会被错误扩大为生产认证。正确做法是把相邻控制接入同一证据链,而不是让单个脚本背负它无法观察的结论。
运行本节验证
静态检查不连接数据库:
在已经确认的 ch04-v1 L1 上运行 session 正反例:
检查 session-profile.txt、negative-summary.txt 和各自 stderr。只有正确上下文通过、两个错误上下文按精确原因失败,连接规约才同时拥有正向和负向证据。
参考资料
- PostgreSQL 18:The Connection Service File
- PostgreSQL 18:The Password File
- PostgreSQL 18:Database Connection Control Functions
- PostgreSQL 18:Client Connection Defaults
- PostgreSQL 18:Statement Behavior
- PostgreSQL 18:Schemas and
search_path - PostgreSQL 18:Reporting and Logging
上一节:规约不是口号 · 返回本章目录 · 下一节:模式与 DDL 候选规则 · 查看全书目录 · 查看索引中心
6.3 模式与 DDL 候选规则
DDL 不是把 ER 图翻译成 CREATE TABLE,而是在数据库中发布一组长期合同:名称怎样解析、谁拥有对象、什么输入被拒绝、什么标识保持稳定、旧应用与新 schema 能否共存。表一旦有数据和调用方,类型、约束和默认值就同时影响写入语义、锁、WAL、复制和恢复。
本节把 ch03 的逻辑问题与 ch04 的物理实现收敛成三组评审规则。具体的在线 schema change 编排留到第 11 章;这里先建立每个变更都必须携带的前提、停止线和证据。
6.3.1 命名、所有权、注释与对象边界
pg36_shop 用三个 schema 表达不同稳定性边界:
| Schema | 内容 | 调用约束 |
|---|---|---|
shop |
核心关系模型与业务写入对象 | application 通过精确 GRANT 读写 |
shop_api |
对外稳定 view/query interface | application/reader 只依赖发布列 |
shop_private |
migration 元数据与内部函数 | 不向普通 runtime role 暴露 |
schema 不是项目文件夹。它同时参与名称解析、USAGE/CREATE 权限与对象归属。边界成立至少要检查四件事:
预期不是“schema 名字存在”,而是 owner、USAGE、CREATE 和 search_path 共同符合合同。shop_private 即使名字带 private,若 application 有 USAGE/EXECUTE,仍不私有。
owner、migration identity 与 runtime identity 分离
本书采用:
对象所有者可以修改或删除自己拥有的对象,也能改变授权;把 owner 直接作为 application login,会让 SQL injection 或应用缺陷越过显式 GRANT。SAFE-ROLE-003 因此是 safety,而不是命名偏好。
NOLOGIN owner 也不等于“不需要保护”:能 SET ROLE 到 owner 的 membership、migration identity 和 SECURITY DEFINER function 都是进入该权限域的路径,必须在 catalog 中验证。日常应用不能为了省事获得 owner、superuser、CREATEDB、CREATEROLE 或 BYPASSRLS。
名称要支持定位,不要假装表达全部语义
默认使用不需双引号的小写 snake_case,原因是客户端、migration 和 catalog 查询更稳定,而不是 PostgreSQL 不支持其他命名。名称应回答:
- relation 表达什么事实,不以当前 UI 页面命名;
- column 的单位或时间语义是否可见,例如
_minor、_at、_date; - constraint 出错时能否定位业务不变量;
- index 名能否关联 key/order/predicate;
- function 名是否表明它是 command、calculation 还是 trigger implementation。
例如:
稳定的 constraint name 使负向测试可以同时断言 SQLSTATE 与 CONSTRAINT_NAME,不会把任何 23505 都误认为目标唯一约束。命名本身不保证正确,但让错误、catalog、migration 和 incident evidence 可以指向同一对象。
PostgreSQL identifier 最长受 NAMEDATALEN 限制,默认最多存储 63 bytes;过长名称会被截断。规约应保证关键语义在截断前仍可辨识,并用 catalog 检查真实名称,而不是依赖生成器在内存中的原始字符串。
comment 是运行目录的一部分
COMMENT ON 应覆盖关键 database、role、schema、relation、column 与非显然约束,至少说明:
comment 不是放完整设计文档,也不能包含 secret、个人数据或随请求变化的值。它的优势是跟对象一起出现在 catalog、\d+ 和元数据工具中。设计文档说明“为什么”,comment 则帮助值班者快速确认“这是什么、谁负责”。
由此形成 DEFAULT-NAME-003:边界由 schema、owner、稳定名称和 comment 共同表达;legacy 例外必须有 mapping、owner、迁移计划和 expiry。
6.3.2 类型、约束和默认值的审查问题
“应该用哪个 PostgreSQL 类型”不能只靠一张类型对照表。评审时先完成语义句,再选类型:
| 维度 | 要回答的问题 | pg36_shop 示例 |
|---|---|---|
| 单位 | 数值代表什么,能否相加 | amount_minor + currency_code |
| 范围/精度 | 是否允许负数、上限、舍入点在哪里 | nonnegative named CHECK |
| 时间 | 瞬间、民事时间、日期还是持续时间 | placed_at timestamptz |
| 文本 | identity、大小写、Unicode、排序和长度合同 | order_no text + format/unique |
| 标识 | 谁生成、唯一范围、稳定期、是否公开 | internal ID / order no / request key |
| 缺失 | NULL 是未知、不适用还是尚未发生 |
paid_at only after payment |
| 演进 | 新值、新状态和旧客户端怎样共存 | status transition contract |
类型名不等于业务合同
numeric 不知道币种和舍入规则,text 不知道 Unicode identity,timestamptz 不保存原始时区名称,jsonb 不自动提供领域约束。DDL 需要组合 type、column name、NOT NULL、default、named constraint、reference 与 comment。
本书把金额写成 minor unit integer 并显式保存 currency。这适合当前教学订单,但并非所有财务系统通用:支持任意精度计量、汇率、税务舍入或多币种分摊时,应重新建模,不能从“整数没有浮点误差”推出“整数能表达所有货币语义”。
若业务没有真实长度上限,PREF-TEXT-001 倾向 text,而不是习惯性 varchar(255)。若外部协议明确限制 64 个字符,则必须说明计数单位是字符、bytes 还是规范化后的 code points,并定义超长输入的错误合同。
约束优先保证单库可表达的不变量
适合进入数据库约束的包括:
- 值域与行内一致性:
CHECK、NOT NULL; - 候选键和幂等键:
UNIQUE; - 引用完整性:
FOREIGN KEY; - 时间/空间排斥:
EXCLUDE; - 需要 transaction 末尾成立的关系:可延迟 constraint。
每个关键约束至少有一个反例,且验证“因预期约束失败”:
只检查“INSERT 失败”可能掩盖权限错误、类型解析错误或另一个约束先触发。只测试正向 seed 则无法证明数据库真的拒绝坏状态。
跨 database、外部支付系统或时间变化事实通常不能靠单个 declarative constraint 完整表达。此时 SAFE-CONS-005 允许受控 breakglass,但必须记录 application control、reconciliation、owner 和到期复查;“数据库做不了”不是“不需要保证”。
四种标识不要混成一列
DEFAULT-KEYS-005 区分:
它们的 authority、生命周期、隐私和唯一范围不同。把可变外部字符串同时当 primary key、公开 URL 和 retry key,会让 provider 变化、内部迁移和 API 合同互相绑死。并非每个模型都需要四列;若用一个标识,review 必须证明这些责任确实一致。
default 是写入规则,不是历史真相
default 回答“调用方省略列时写入什么”,不回答旧行原本是什么。常见审查点包括:
now()是 transaction start time;是否需要 statement/clock time;- identity/sequence 生成的是唯一候选值,不承诺无间隙或按 commit 排序;
- 空字符串、空 JSON 与
NULL是否真的同义; - volatile default 是否触发表重写或让重跑结果不稳定;
- 新增 default 后,旧应用显式发送
NULL时会发生什么; - backfill 值能否从已有事实确定,还是在伪造历史。
默认值方便不应覆盖领域语义。一个字段若在业务上必须由调用方明确选择,省略 default 反而能尽早暴露错误。
JSON、array、enum 与 partition 都要证据
PREF-SEMI-002 只把 shape 可变、整体读写、有明确 owner/validation/retention 的附属值放进 JSONB/array;需要独立引用、唯一、局部更新或生命周期的事实优先拆成 relation。这不是“永远范式化”,而是让数据库能对核心事实提供统计和约束。
分区同理。PREF-PART-003 要求 retention/drop lifecycle、可测规模瓶颈或稳定 pruning 证据先出现,再设计 partition key、unique/PK、FK 和迁移。预计未来会有一亿行不是设计完成;分区会立刻增加约束、索引和运维复杂度,收益却可能多年不出现。
6.3.3 可逆迁移与版本化 DDL
“所有 migration 都必须可回滚”听起来安全,实际上容易产生虚假承诺。删除列后,down 可以重新创建空列,却不能恢复已经丢失的业务语义;旧应用已经按新格式写入后,数据库回滚也不保证旧代码能理解数据。
更准确的目标是服务可恢复:
expand—backfill—validate—switch—contract
跨 application release 的变更按五阶段设计:
- expand:添加旧代码可以忽略的新对象,避免立即收紧;
- backfill:分批填充,限制 lock/WAL/replica lag,过程可重入;
- validate:检查无坏值、约束成立、读写双路径一致;
- switch:先切写路径,再切读路径,保留观测和回退窗口;
- contract:确认旧代码/旧数据路径退出后再删旧对象。
PostgreSQL 的 transaction DDL 很强,但并非所有命令都能放在同一个事务,锁取得时机与强度也不同。CREATE INDEX CONCURRENTLY、VACUUM 等有自己的事务限制;大型 backfill 即使可回滚,也可能产生巨大 WAL、dead tuples 和 replica lag。不能用“BEGIN 包住了”替代容量与锁评估。
对新 CHECK/FK,可以在合适场景先 NOT VALID,使新写入受约束,再单独 VALIDATE CONSTRAINT 扫描旧数据;这不自动适用于 UNIQUE/PK,也不消除所有 lock。具体 lock mode、版本差异与 workload 影响必须在 ch11 用当前目标版本实测。
破坏动作之前先证明可表示
SAFE-MIGR-006 要求在类型收窄、列删除、表重写或约束收紧前完成:
precheck 必须在真正写入前失败。例如把 text 转成 integer,先找出所有无法转换的值并固定 conversion rule;不要让 ALTER TABLE ... TYPE 运行数十分钟后才撞到最后一个坏值。precheck 与执行之间仍可能有竞态,所以迁移还需限制并发写、使用兼容约束或在同一受控边界重新确认。
高风险 contract 不因为有 backup 就可以随时执行。backup/PITR 是最大故障恢复证据,不是低成本 undo;恢复时间、数据丢失窗口和对其他 database 的影响都要进入风险说明。
fresh install 与 upgrade 只有一条权威链
两套脚本最容易漂移:
如果两者由人独立维护,很快会产生“同版本不同 schema”。DEFAULT-VERS-010 要求 fresh install 也消费同一条 versioned migration chain,或者由这条链可重复生成并验证 latest snapshot。数据库内要有可查询的 schema version 和 migration identity;重复执行要么幂等成功,要么在修改状态前明确拒绝。
本书使用 shop_private.schema_version 标记 ch04-v1,并由每章 context guard 检查。版本号本身不证明 schema 正确,所以还需要 catalog assertions 与稳定 relation checksum;checksum 又不能覆盖全部权限、function body 和运行参数,因此验收必须是多项事实,不是单个魔法哈希。
变更说明先于执行
使用 change-template.md 填写:
- target、owner、窗口与 application release;
- 当前 schema version、relation size、write rate 和依赖方;
- 五阶段迁移与重跑行为;
- lock/WAL/temporary space/lag 预算;
- transaction rollback、application rollback 与 forward repair;
- precheck、负向 SQLSTATE、post-state checksum;
- 最大可信故障、停止条件和审批。
无法填写的字段不是“文档以后补”,而是设计尚未完成。真正执行在线 DDL 前,还要在第 11 章为具体 PostgreSQL/Pigsty 环境补齐锁实验、监控窗口和发布编排。
本节验收问题
评审任意一个 DDL change,应能回答:
- schema、owner、runtime role 和 API boundary 是否明确;
- 名称/注释能否从 catalog 定位领域语义和 owner;
- 每个类型是否闭合单位、范围、时间、文本与 NULL 语义;
- 关键约束是否命名,并有精确 SQLSTATE/constraint 反例;
- 内部、业务、外部和幂等标识是否被有意区分;
- JSON/array/partition 的收益与边界是否有 workload 证据;
- fresh install 与 upgrade 是否进入同一 version authority;
- destructive step 前是否有可表示性 precheck、timeout 和停止线;
- application rollback 时 schema 是否仍兼容;
- 丢失语义时是否诚实声明只能 forward repair 或 restore。
如果其中任何高影响问题只能回答“应该没事”,该变更仍是 candidate,不应进入生产发布队列。
参考资料
- PostgreSQL 18:Schemas
- PostgreSQL 18:Privileges
- PostgreSQL 18:Data Definition
- PostgreSQL 18:Constraints
- PostgreSQL 18:Date/Time Types
- PostgreSQL 18:Numeric Types
- PostgreSQL 18:Transactional DDL Caveats
- PostgreSQL 18:ALTER TABLE
上一节:连接与会话候选规则 · 返回本章目录 · 下一节:查询与事务候选规则 · 查看全书目录 · 查看索引中心
6.4 查询与事务候选规则
查询规约要保护的是调用合同,事务规约要保护的是失败后的正确性。两者都不适合简化成 SQL 风格检查:SELECT * 在交互诊断中很方便,在持久 API 中却会制造列漂移;CTE 可能清晰表达关系步骤,也可能引入不必要 materialization;短事务通常更友好,但把本应原子的一组写入拆开只会得到更快的错误结果。
这一节先固定语义合同,再讨论代价。第 7–10 章会继续为计划、索引和并发规则补证据。
6.4.1 明确列、稳定排序与分页语义
持久 query interface 至少声明五件事:
DEFAULT-QUER-006 因此要求稳定接口显式投影:
这不是因为 SELECT * 在服务器内部必然更慢,而是因为隐式列集合会随 DDL 变化,扩大网络与权限面,破坏 positional decoder,并让调用方不知不觉依赖内部列。短期 psql 探索可以使用 *;稳定 view consumer、API query 和 migration copy contract 不应使用。
没有 ORDER BY 就没有顺序合同
PostgreSQL 文档明确指出,不指定 ORDER BY 时,返回顺序未定义。一次执行看起来按 primary key 或 heap 顺序返回,只是当前 plan、数据布局和并发状态的结果。加 LIMIT 也不会把偶然顺序变成合同:
唯一 tie-breaker 是 SAFE-PAGE-010 的底线。若排序列可为 NULL,API 还要固定 NULLS FIRST/LAST;若排序受 collation 影响,要固定 collation/normalization,或用稳定 binary/normalized key。否则 cursor 编码相同值时,不同环境可能得到不同边界。
keyset cursor 必须编码完整排序键
本章样例按:
向后取下一页:
成立前提是两个键都非 NULL、比较语义与排序一致,最后的 order_id 唯一。cursor 至少编码两个值、sort version/direction 和必要的 filter identity;对外暴露时通常还需要签名或完整性保护,避免调用方伪造超范围条件。
若混用 ASC/DESC、NULL 或不同 collation,不能机械复制 row comparison;应展开为与排序完全等价的 predicate,并写边界测试。反向翻页也不是把 < 改成 > 就结束,还要反转内部 order、取得一页后恢复 API 顺序。
OFFSET 不是永远禁止:小型后台界面、稳定 snapshot 内的有限页数可以接受。但大 offset 仍要计算并丢弃前面的行;在 Read Committed 下跨页查询之间发生 insert/delete 时,还可能重复或遗漏。keyset 避免按位置跳过,却不能自动提供跨页 snapshot 一致性;排序键被更新时也可能移动。API 必须声明自己提供“实时游标”还是“固定快照导出”。
用结果合同而不是 SQL 文本做验收
query-contract.sql 不要求 application 复制某一段 SQL 字符串,而是验证:
shop_api.order_summary恰好包含 11 个发布列;- 排序显式为
placed_at DESC, order_id DESC; - 第一页和第二页 cursor 严格前进且不重叠;
order_nobusiness key 与request_keyidempotency key 仍唯一。
典型输出:
教学 fixture 只有两笔订单,所以这不是性能 benchmark,也没有覆盖 NULL、同 timestamp、大页数和并发移动。它证明 baseline 的最小语义;API 上线前还要添加这些边界用例。
6.4.2 事务大小、超时、重试与幂等
“事务越短越好”缺少一个关键限定:事务必须先覆盖保持不变量所需的完整正确性单元,然后才在这个边界内缩短。
以“创建订单并预占库存”为例:
如果这些数据库事实必须共同成立,就不能为了缩短 transaction 把它们拆成无补偿的独立 commit。真正应该移出去的是用户输入、HTTP 调用、邮件发送、长时间计算和无边界 sleep。DEFAULT-TXNN-007 要求 transaction diagram 标出:
对大批处理则分批 commit,但必须定义 partial progress、restart cursor、幂等与最终 reconciliation。分批不是放弃原子性,而是把正确性单元重新定义为可恢复的小批次。
首个错误才是根因
显式 transaction 中第一条 statement error 会使 transaction 进入 failed state;后续普通 SQL 通常只返回 25P02 in_failed_sql_transaction。应用必须保存第一个 SQLSTATE,然后:
- 整体
ROLLBACK;或 - 回到事先建立、且业务语义允许的 savepoint。
不能在收到 25P02 后继续发业务 SQL,也不能把 failed/idle-in-transaction connection 原样归还 pool。driver/framework 的 cleanup 必须在归还连接前 rollback,并检查 transaction 状态。
第 5 章实验已经证明:
savepoint 是局部恢复工具,不是“忽略错误继续”。若失败改变了后续决策所依赖的业务语义,最安全的边界仍是整体重试。
timeout 是失败合同的一部分
statement/lock timeout 触发后,当前 statement 失败;若处于显式 transaction,transaction 同样需要 rollback/savepoint 恢复。应用必须区分:
- query 被 server 明确取消;
- 获取 lock 超时;
- client deadline 先到并关闭/取消连接;
- 网络断开导致 commit outcome 不明确。
它们不能统一成“再执行一次”。数据库可能明确回滚 statement,也可能已经 commit 但 ACK 丢失。
重试整个正确性单元
SAFE-RETR-008 目前定义:
40001 serialization_failure 与 40P01 deadlock_detected 常常可以整体重试,但“可以”仍依赖操作幂等、时间预算和 contention。不能只重放最后一条 SQL:前面的读取与判断来自已经失效的 snapshot。也不能把所有 08xxx connection exception 无条件重试,因为 commit 可能已经成功。
allowlist 要按 driver 暴露的 SQLSTATE/class 检查,不能按本地化 message substring。最大次数之外还要有总 deadline,避免数据库过载时 retry storm;jitter 用于打散竞争者,不保证消除热点。
幂等要闭合 ambiguous outcome
创建订单使用独立 request_key:
但 DO NOTHING 只是起点。冲突后必须查询权威结果,并验证同一个 idempotency key 对应的业务 payload 是否一致;否则客户端错误复用 key 会被误当成成功。key 的作用域、保留时间和并发行为都要写入合同。
外部支付、HTTP、消息和邮件不随 PostgreSQL rollback 自动撤销。常见方案是先在同一 database transaction 内写 durable intent/outbox,再由独立 worker 幂等投递;或者由外部系统提供相同 idempotency key 和可查询 outcome。无论采用哪种方案,都要回答:
本章只把这些问题固化为 review rule。真正的自动重试、deadlock/serialization fixture 与 ambiguous outcome 演练安排在 ch10。因此 v0.1 诚实输出 safety 自动/运行覆盖 9/10。
6.4.3 CTE、窗口函数与 LATERAL 的可读性门槛
高级 SQL 的评审不能变成关键字黑名单。WITH、window 和 LATERAL 都能让关系责任更直接,也都可能在错误数据分布下产生高成本。PREF-ASQL-004 的门槛是:reviewer 能用一句话说明每个构造负责什么,并且样例、边界测试与 plan evidence 支持它。
CTE:命名关系步骤,也可能改变优化边界
CTE 适合给复杂关系步骤命名:
在当前支持版本中,一个无副作用、非递归、只引用一次的 CTE 通常可折叠进父查询;多次引用通常会 materialize。MATERIALIZED 与 NOT MATERIALIZED 可以显式影响决策,但不是性能咒语:materialization 可能避免重复昂贵计算,也可能阻止父查询 predicate 下推。含 volatile function 或数据修改的 CTE 又有不同语义。
因此,不能继续沿用“PostgreSQL 的 CTE 永远是优化栅栏”这类跨版本口号。每个显式 materialization 都要说明是为了稳定语义、避免重复工作,还是经过计划对照后的成本选择。
Window:在同一行集上分析,不替代输出排序
window function 保留输入行,同时计算 partition/order/frame 内的值:
window 的 ORDER BY 决定窗口计算顺序,不保证最终 result order;对外返回仍需顶层 ORDER BY。last_value 等函数还受默认 frame 影响,必须显式审查 frame。多个不同 window order 可能引入多次 sort;计划与 work_mem/spill 证据留到第 7、8 章。
LATERAL:表达逐行依赖,也可能放大外层基数
LATERAL 允许 FROM item 引用左侧 item,适合“每个 customer 最近两笔订单”:
它可以把 application N+1 合并为一次 SQL,也常对应按外层每行执行的参数化路径。外层基数、内层索引和 loops 决定它是高效 top-N 还是放大器。评审不能因为“只有一条 SQL”就判断更快。
计划证据不做节点名 golden test
PREF-PLAN-005 明确:
- Seq Scan 不自动错误,小表/低选择性时可能最优;
- Nested Loop 不自动错误,参数化小结果与合适索引时可能最优;
- planner cost 不是毫秒;
- 一次
EXPLAIN ANALYZE不是未来预测; - 强制 planner GUC 或新增索引前,先看 estimate/actual、loops、buffers、wait、参数和数据分布。
本章 gate 只验证 query semantics,不固定 plan node。精确 plan evidence 在 ch07 引入,慢查询闭环在 ch08,索引写放大与收益在 ch09。这样的章节边界防止 baseline v0.1 提前把尚未实验的性能偏好升级成 safety。
本节验收问题
- 稳定 query 是否显式列出输入、输出和 cardinality;
- 对外 result 是否显式排序,并以唯一键形成全序;
- cursor 是否编码全部 sort keys、direction、NULL/collation 与失效语义;
- 是否明确需要实时分页还是跨页一致 snapshot;
- transaction 是否覆盖完整不变量,同时排除用户/远程等待;
- 首个 SQLSTATE 是否保留,失败连接是否在回 pool 前 rollback;
- retry 是否重跑完整 transaction,带 allowlist、backoff、jitter、次数和总 deadline;
- ambiguous commit 是否能通过 idempotency key 与权威查询闭合;
- 外部副作用是否有 durable intent、幂等或 reconciliation;
- 每个 CTE/window/LATERAL 是否有一句话职责、边界用例和计划证据;
- 是否避免用节点名、cost 或一次耗时做 blanket rule。
当这些答案进入 query contract 和变更证据后,SQL 才从“现在能跑”升级为“失败后仍可推理”。
参考资料
- PostgreSQL 18:Sorting Rows
- PostgreSQL 18:LIMIT and OFFSET
- PostgreSQL 18:SELECT
- PostgreSQL 18:WITH Queries
- PostgreSQL 18:Window Functions
- PostgreSQL 18:Table Expressions and
LATERAL - PostgreSQL 18:Error Codes
- PostgreSQL 18:Transaction Isolation
上一节:模式与 DDL 候选规则 · 返回本章目录 · 下一节:交付物与质量门 · 查看全书目录 · 查看索引中心
6.5 交付物与质量门
数据库代码通过 review 只是交付的一部分。接手者还需要知道它针对哪个状态、怎样重跑、错误时停在哪里、是否可以恢复、运行后怎样证明没有漂移。没有这些信息,一段正确 DDL 仍可能在错误 database 上、错误窗口里,以错误的应用版本执行。
本节定义“一个可交付数据库变更”需要携带的产物和质量门。它不是要求每个小改动都写几十页,而是让风险越高的动作拥有越强的前验、后验和接管信息。
6.5.1 DDL、迁移、数据生成与回滚
一个完整交付包按职责拆分,而不是把所有内容塞进 deploy.sql:
| 产物 | 责任 | 必须避免 |
|---|---|---|
| contract/ADR | 目标、非目标、业务不变量、兼容边界 | 只写实现,不写为什么 |
| migration | 从已知 version 到下一 version | 同时猜测多个未知起点 |
| fresh install | 复用/生成自 migration authority | 独立维护另一套 latest schema |
| seed/fixture | 构造确定性最小场景 | 隐式当前时间、无 seed 随机数 |
| precheck | 在写入前证明输入可表示、依赖可控 | 迁移中途才发现坏值 |
| verify | catalog、权限、不变量、checksum 后验 | 只看脚本 exit 0 |
| negative cases | 证明错误状态被正确规则拒绝 | 捕获所有异常后宣称通过 |
| reset/cleanup | 仅用于明确可销毁范围 | 把 reset 冒充生产 rollback |
| runbook/evidence | 输入、命令、版本、stdout/stderr、结果 | 只保存截图或手工摘要 |
并非每项变更都需要 seed 或 reset。例如只读诊断没有持久对象,不应为了“模板完整”添加 destructive cleanup。生产 migration 通常也不提供一键 reset;它需要兼容回退和 forward repair。交付矩阵的价值是要求作者明确“适用/不适用及原因”,不是追求文件数量。
migration 的起点必须可识别
执行前至少检查:
若起点未知,应在修改任何状态前拒绝。所谓“幂等”不应等价于到处写 IF EXISTS 后吞掉漂移;当对象存在但 shape、owner 或语义不同,安全行为是报错并交给 owner 判断。
migration 记录唯一 identity,成功后原子推进 schema version。重跑时:
- 已以同一 checksum 成功:可以明确报告 no-op;
- 尚未开始:从确定起点执行;
- 中途失败但 transaction rollback:确认起点仍成立;
- 包含非事务步骤或 outcome 不明:进入专门 reconcile/repair,不能盲重放。
fixture 要可重建、可比较
DEFAULT-FIXT-008 要求教学/测试数据使用稳定业务值、显式 timestamp 和受控序列策略。随机数据可以用于 property/load test,但必须记录 seed、generator version 与规模参数。
验收不要依赖易变物理标识:
动态值可以保存在 evidence 中帮助取证,却不能硬编码成跨运行 golden value。第 5 章 rollback 实验前后比较业务 fingerprint/checksum,同时允许 WAL LSN 前进,就是这一原则。
reset 是独立的破坏动作
reset 的目标是清理教学/测试状态,不是自动恢复生产。SAFE-DEST-009 要求 DROP/reset/terminate:
- 先把 service、database、schema/relation 或 PID+identity 解析成精确目标;
- 拒绝空变量、通配符、workspace root 与 broad target;
- 要求与目标绑定的独立确认 token;
- 执行时保存 before state;
- 执行后验证目标消失、预期保留对象仍存在、实验 worker 清零;
- target identity 不再精确时停止自动清理。
例如终止 backend 不能只凭 PID,因为 PID 会复用;至少结合 database、user、application_name、backend_start 与当前 query。文件清理不能把未解析环境变量交给递归删除。成功 exit 只说明命令执行,不能证明范围正确。
本章自己的 quality gate 全部只读或故意失败,不创建持久对象,所以没有 reset action。这是设计结论,不是交付缺失。
6.5.2 自动测试、静态检查与计划证据
数据库质量门应逐层增加成本与环境依赖:
flowchart TD
A["Static<br/>schema / syntax / source safety"] --> B["Catalog contract<br/>shape / owner / grants / GUC"]
B --> C["Positive + Negative SQL<br/>result / SQLSTATE / constraint"]
C --> D["Integration<br/>driver / pool / service / application"]
D --> E["Concurrency<br/>blocking / isolation / retry"]
E --> F["Plan + workload<br/>estimate / actual / buffers / WAL"]
F --> G["Release observation<br/>SLO / lag / error / rollback window"]不是每次提交都同步运行最昂贵层,但进入下一环境前必须知道哪些层已通过、哪些仍待验证。用“CI 绿了”概括所有层会丢失决策信息。
Static:无数据库也能拒绝结构漂移
本章 check_baseline.py 只使用 Python 标准库,检查:
- JSON 没有重复 key,registry 满足固定 shape;
- 25 个 Rule ID 唯一,level 与 ID prefix 一致;
- evidence 只指向 ch01–ch05,且 artifact 实际存在;
- 人类指南中的 Rule ID 恰好各出现一次;
- delivery manifest 的 artifact 与 action 完整;
- ch01–ch06 受管 SQL/shell/JSON/YAML/config 中没有
PGPASSWORD、带凭据 PostgreSQL URI 或明文 password assignment; DROP DATABASE/ROLE只出现在带 token 的专用reset.sql。
quality-gate.sh static 还对 Python 做 bytecode compile,对全部 lab shell 做 bash -n。这些检查不连接 PostgreSQL,所以适合每次提交;它们能证明结构和已知危险模式,没有证明 SQL 在目标版本执行正确。
正则 secret scan 也不是 DLP。编码、模板展开、二进制或未知 secret 形式仍可能漏过;source reviewer 和 CI artifact policy 继续负责。检查器应报告自己的扫描文件数,使范围缩小时不会静默绿色。
Catalog 与正反例:验证数据库真正拒绝什么
Catalog contract 比解析 DDL 文本可靠,因为它看到服务器已经解释后的对象:
但 catalog 是检查时刻事实,不能自动证明迁移路径曾经安全。正向 case 证明有效输入工作;负向 case 必须断言稳定 SQLSTATE、constraint name 或自定义 error contract。不要用本地化 message 全文,也不要 EXCEPTION WHEN OTHERS THEN pass。
本章 wrong-session/wrong-target fixture 的意义就在于验证 gate 本身:若 guard 被意外删除,负向 case 会“错误成功”,CI 随即失败。
Integration 与 concurrency:跨边界验证
SQL 在 psql 中通过,不代表 driver、pool 或 application transaction management 正确。Integration test 要覆盖:
- 参数绑定与类型/OID;
- NULL、encoding、timezone 和 decoder;
- pool mode、connection reset 与 transaction cleanup;
- timeout/cancel 如何映射为应用错误;
- service failover/route 与 read-only 行为;
- idempotency、ambiguous outcome 和 trace attribution。
并发正确性不能由单 session 单元测试推出。lost update、write skew、deadlock 和 retry 必须使用多个可识别 session、明确同步点、前后 checksum 与失败清理。第 10 章会加入这层;在那之前 SAFE-RETR-008 只能保留 review check。
计划证据验证关系,不冻结节点名
计划测试应保存:
稳定断言通常是:
- estimate/actual 误差是否越过调查阈值;
- buffers/temp/WAL 是否超预算;
- 参数范围内 p95/p99 是否满足 SLO;
- 变更是否让目标 workload 改善且写入代价可接受。
不要把“必须出现 Index Scan”“总 cost 小于 1234”作为跨版本 golden。planner、统计、数据量和 cache 变化都可能选择另一条同样正确的 plan。第 7–9 章会把 PREF-PLAN-005 扩展为可操作流程。
Evidence directory 是可复核输入输出
DEFAULT-EVID-009 要求每次任务写入独立目录,至少包含:
证据包不能包含展开后的 secret。service name 可以保存,password、credential URI、private key 和含 token 的环境 dump 不可以。生产 evidence 还应有访问控制与保留策略;“为了审计”不是永久复制敏感数据的理由。
6.5.3 变更说明、所有者与风险等级
一份变更说明的首要作用是让另一个合格操作者可以在压力下判断:继续、停止、回退还是升级,而不是证明作者写过文档。
change-template.md 将信息分为七组:
- 身份:Change ID、owner、reviewer、target、窗口、application release;
- 目标/非目标:改变什么可观察事实,明确不解决什么;
- 当前事实:版本、对象大小、写入率、schema version、依赖方;
- 迁移设计:expand/backfill/validate/switch/contract 与重跑行为;
- 资源预算:lock mode、timeout、WAL、temp、lag、old snapshot;
- 失败恢复:哪一步可 rollback,哪一步只能 repair,outcome ambiguous 怎样确认;
- 验证风险:precheck、正反例、post-state、最大故障、停止条件与审批。
风险等级由影响与恢复共同决定
本书实验使用四级标签:
| 等级 | 含义 | 典型动作 |
|---|---|---|
R0·观察 |
只读、无主动状态改变 | catalog/query/metric 采集 |
R1·可逆变更 |
范围精确,可低成本恢复 | 创建专属 fixture、可验证配置 |
R2·受控演练/破坏 |
会写入、持锁、取消或删除实验对象 | rollback write、reset 专属 schema |
R3·生产敏感 |
影响真实流量/数据、恢复昂贵或范围较大 | failover、contract DDL、restore/cutover |
风险不是由 SQL 关键字单独决定。同一个 ALTER TABLE 在空 L1 和高写入生产表上不是同一级;只读 EXPLAIN ANALYZE 也会真实执行查询,可能成为 R2/R3。评估至少考虑 blast radius、可逆性、锁/WAL/容量、持续时间、权限和环境价值。
R3 不进入自动教学 harness。它必须使用生产 runbook、实时观测、双人/组织审批和明确 incident authority;本书后续章节可以演练机制,不会因为用户会运行实验就默认获得生产处置授权。
owner 与 reviewer 责任不同
- change owner 对设计、前提、执行证据和结果负责;
- service owner 确认业务窗口、兼容与 SLO;
- database/platform reviewer 复核 PostgreSQL/Pigsty 机制;
- operator 有权在停止条件命中时终止;
- incident commander 只在预先声明的 breakglass 条件下扩大权限。
“DBA 批准”不能替代业务 owner 对数据语义负责,“应用团队说可以”也不能替代平台对恢复与容量负责。责任要落到具名角色和时间窗,而不是群聊。
停止条件必须在开始前写
可执行停止线使用可观察量:
“感觉不对就停”不能在压力下形成一致行为。停止也要对应下一步:rollback current transaction、停止新批次、切回旧 application path、进入 forward repair,还是升级 incident。
waiver 也要进入交付链
default 或 preference 被偏离时,使用 waiver-template.md 记录:
- 哪条 Rule ID、在哪个 target/scope 偏离;
- 为什么失败机制在此场景不同;
- 剩余风险与补偿控制;
- owner、reviewer、expiry;
- 怎样验证、怎样回归默认。
Safety breakglass 不是普通 waiver。它要求更严格的身份、时限、撤销和事后复核;标记为 exception.mode=none 的规则则必须重新设计,不能靠审批覆盖。
本节最小质量门
进入下一环境前,交付包至少应能回答:
任何一个高风险答案缺失,都不应通过“先上线再观察”。质量门的目的不是增加仪式,而是在变更仍便宜时暴露未知。
上一节:查询与事务候选规则 · 返回本章目录 · 下一节:将规约接入统一实验环境 · 查看全书目录 · 查看索引中心
6.6 将规约接入统一实验环境
规约若只存在于 repository,就无法约束实际环境;平台若只负责“把 PostgreSQL 装起来”,又无法知道业务对象是否满足合同。Pigsty 与版本化 SQL 在这里承担不同职责:
这三层共同组成统一实验环境。平台声明不能替代业务 migration,migration 成功也不能证明 HAProxy/PgBouncer 路由正确。
6.6.1 角色、数据库与服务声明
Pigsty 是配置驱动平台:inventory 的 global、cluster、host 层按覆盖顺序形成最终参数,再由 playbook 生成并应用 Patroni、PostgreSQL、PgBouncer、HAProxy 与相关配置。pg_users 和 pg_databases 允许在 cluster vars 中声明业务身份和数据库。
本章提供一个不含凭据的 pigsty-declaration.example.yml。它是应合并到目标 cluster vars 的片段,不是完整 inventory:
完整样例还包含连接池预算和 database-level timeout/UTC 默认。数值是教学起点,必须按真实 connection budget 与 workload 调整。
为什么先声明 role,再声明 database
PostgreSQL role 属于整个 cluster,不属于单个 database;database owner 在创建 database 时必须已经存在。Pigsty 的 pg_users 又按数组顺序创建,所以样例先创建 NOLOGIN owner,再创建 application/read-only LOGIN role,最后创建由 owner 持有的 database。
LOGIN role 的 credential 没有进入样例。实际 inventory 必须从受控 secret overlay 注入 SCRAM secret 或采用组织认证方案;不能把展开后凭据提交到本书 repository。若直接应用这份无密码片段,role 可以创建,但不能靠密码认证登录——这是有意的 fail-closed,不是可直接上线的完整安全配置。
revokeconn: true 会撤销 PUBLIC CONNECT,并保留 owner/管理/监控等受控入口。pg36_app 与 pg36_ro 的精确 CONNECT、schema USAGE、table/sequence privilege 和 default privilege 仍由 ch01 versioned SQL 授予。这里故意不把所有业务授权改成 Pigsty 内置全局 dbrole_readwrite:本书要验证 pg36_shop 的对象级最小权限,而不是让跨库角色隐式扩大范围。
为什么不把业务 schema 塞进一次性 baseline
Pigsty pg_databases.baseline 会在 database 首次创建时执行 SQL,已有 database 会跳过;encoding、locale、template 等字段又具有创建时不可变的边界。它适合明确的一次性引导,但不能单独承担持续 schema migration。
本书让:
fresh install 与 upgrade 因此复用同一 migration authority。即使 schemas 已由 Pigsty 创建,SQL 使用 CREATE SCHEMA IF NOT EXISTS 后仍验证 owner/privilege;若同名 schema 形状或 owner 不符合合同,后验会失败,而不是因为“存在”就默认正确。
使用默认 service,而不是再造一个名字
Pigsty v4.5 每个 PostgreSQL cluster 默认提供:
| Service | Port | 本章用途 |
|---|---|---|
primary |
5433 | production read/write,经 primary PgBouncer |
replica |
5434 | production read-only,经 replica PgBouncer |
default |
5436 | admin/ETL/direct primary PostgreSQL |
offline |
5438 | OLAP/ETL/个人只读类 direct workload |
pg36_app 的日常 OLTP 连接应使用 primary:5433;受审计 migration、catalog 诊断和本章 SET ROLE gate 使用 default:5436 direct path。两者都指向当前 primary,但 pool/session 语义不同。read-only role 也不能仅凭名字就发送到 replica:调用方要选择 replica service,并接受复制延迟与 read-after-write 语义。
本章无需自定义 pg_services。只有默认 selector、health check、destination 或端口不能表达 workload 时才增加 service,并同时说明 failover、fallback 与容量边界。多一个 service 名不是更安全;没有调用合同的 service 只会增加误路由。
6.6.2 初始化、验证与重置入口
统一环境需要把“基础设施声明”和“书中 SQL”排成可重复顺序。
第一次初始化
先在 Pigsty repository 中把样例片段合并到已确认的目标 cluster。不要照抄 cluster 名;先查看 inventory graph、最终 host vars 和 diff。对于已有 cluster,官方 v4.5 的精确入口是:
这些命令只是说明 apply 顺序。真正执行前必须:
- 用
-l/wrapper 的 cluster 参数限制到单一已确认目标; - 确认 secret overlay 已生效但不会打印到 evidence;
- 确认同名 role/database 没有另一业务含义;
- 对 immutable database 字段检查现状,不用
state: recreate强制收敛; - 保存 inventory commit、resolved target 和 playbook result。
新 cluster 可以在受控 pgsql.yml -l <cluster> 初始化中创建这些对象;已有 cluster 应用专用 pgsql-user/pgsql-db,不要为了新增一个 database 重新运行无范围的全局 playbook。
然后通过 default:5436 的私有 libpq service 执行书中版本链:
实际目录中各章的 task.sh 固定 action 与 evidence。不要把这些步骤复制成一条不检查中间状态的长 shell command;每个 version boundary 成功后保存 summary,失败时停在已知状态。
每次任务只有一个 action 合同
action 名应表达风险和后置状态:
统一入口负责:
- 解析 action,未知值以 usage/exit 64 拒绝;
- 检查依赖工具与
PGSERVICEFILE; - 创建 mode 0700/umask 077 evidence directory;
- 写 source manifest;
- 运行 context guard 和 verify-before;
- 执行 action;
- 即使预期报错,也核对精确 exit/SQLSTATE;
- 写 verify-after 与 machine-readable summary;
- 清理本次启动的精确 worker。
脚本不应根据“这是开发机”自动猜测 database 可以删除。环境分类可以决定是否允许 R1/R2,但 destructive target 与 token 仍要精确。
reset 不属于正常升级路径
ch01/ch03/ch04 的 reset 用于放弃整个教学模型并重建,属于 R2,必须使用章节定义的双重令牌。它不能用于:
- 清理未知生产漂移;
- 让失败 migration 看起来重新成功;
- 在保留价值不明时重建 database;
- 替代 application/schema 兼容回退。
本章没有持久写入,所以不提供 reset。成功的 all 应保证 relation checksum 不变;若 checksum 漂移,正确动作是停下来调查,不是自动调用上一章 reset。
应用流量还要单独验收 pooled path
本章 quality gate 使用 pg36-admin direct service,因为它需要稳定 session、catalog visibility 与 SET ROLE pg36_owner。它没有证明 application 经 primary:5433 的行为。应用交付前还应使用 pg36_app service 测试:
这层将在 ch12 的“从数据库到服务”中成为 v1.0 验收项。
6.6.3 配置事实与运行事实分开审查
一次平台变更至少有四类事实:
| 层次 | 证据 | 能证明什么 | 不能证明什么 |
|---|---|---|---|
| Git/inventory | reviewed YAML + commit | 期望状态和变更意图 | 已应用到哪个 target |
| apply | playbook target/diff/result | 某次动作在某批 host 执行 | 所有运行事实持续正确 |
| PostgreSQL | catalog、GUC、SQLSTATE、checksum | 当前数据库实际对象与语义 | 客户端经过哪个 frontend service |
| routing/observability | HAProxy/PgBouncer state、连接 endpoint、dashboard/log | service 路由、pool 与时间趋势 | 业务不变量全部正确 |
“配置里写了”只能回答第一行。一次严谨审查同时保留 desired、apply 和 actual。
从 YAML 回到 PostgreSQL catalog
pg_users 的后验不是搜索配置文本,而是:
database/schema 后验包括 owner、encoding、locale/collation、CONNECT、schema owner/USAGE/CREATE。role membership 在 Pigsty 中可能是 additive;从 inventory 删除一个 role name 不一定等于数据库里自动撤销已有 membership,必须用显式 absent/revoke 和 catalog 后验。
database immutable 参数若与 inventory 不同,不应自动 state: recreate。先把漂移记录为 change,评估数据保留、backup/PITR 和 application downtime,再决定迁移或接受有 expiry 的 waiver。
从参数声明回到生效值和来源
ALTER DATABASE/ROLE SET 通常只影响新 session。检查:
再在目标 role/database 的新连接中 SHOW/current_setting()。pg_settings 的当前 backend 值与 source 能解释本会话,但不能仅凭 postgresql.conf 文件推断覆盖后的结果。pending restart、reload 与新连接边界也必须区分。
从 service 名回到真实路由
连接 primary:5433 时保存:
pg_is_in_recovery()=false 证明当前 backend 可写 primary,不证明客户端一定经过预期 HAProxy port;客户端 endpoint 证明请求入口,不证明 selector 在未来 failover 始终正确。要将两类事实与 PGSQL Service/Proxy/PgBouncer dashboard 或 HAProxy state 对齐。
连接 replica:5434 也不能只检查 default_transaction_read_only:健康 selector、实际 recovery state、replication lag 与 fallback policy共同决定读语义。对 read-after-write 敏感的请求通常应继续走 primary,或显式等待/携带一致性标记。
漂移处理不是“以谁为准”一句话
发现 inventory 与 actual 不同时,先分类:
然后选择:
- 重新 apply 并验证;
- 把合法 hotfix 回写 inventory/migration;
- 撤销未经授权的手工漂移;
- 为不可变差异设计迁移;
- 修正检查 target;
- 在有 owner/expiry 的 waiver 中暂时接受。
不能机械地让自动化“配置覆盖运行”,也不能把实际状态反向复制进 Git 就算解决。权威来源取决于对象:cluster/service desired state 通常在 Pigsty inventory,业务 schema version 在 migration ledger,当前故障处置可能暂时以 incident hotfix 为准,但结束后必须回写。
本节验收
把样例接入一个已确认 L1 后,应能提供三组独立证据:
只有三组吻合,才能说“规约已经接入环境”。本章实验只完成 direct admin 和 PostgreSQL actual 部分;真正 Pigsty cluster 的 apply 与 pooled application probe必须在读者自己的 L1 中完成并保存 target-specific evidence。
参考资料
- Pigsty v4.5:PostgreSQL Configuration
- Pigsty v4.5:User/Role
- Pigsty v4.5:Managing Users
- Pigsty v4.5:Database
- Pigsty v4.5:Managing Databases
- Pigsty v4.5:Service/Access
- Pigsty v4.5:PGSQL Playbooks
上一节:交付物与质量门 · 返回本章目录 · 下一节:实战:发布规约 baseline v0.1 · 查看全书目录 · 查看索引中心
6.7 实战:发布规约 baseline v0.1
本节把方法落到一个可发布对象:25 条 active rules、13 个交付资产、三类质量门、一份未来证据账本。发布的含义不是宣布“以后永不修改”,而是固定版本、范围、checksum、已知缺口和升级条件,让任何读者都能复核 v0.1 当时究竟承诺了什么。
实验风险:
static:R0·观察,只读 repository,不连接 PostgreSQL;live:R0·观察,连接已确认 ch04-v1 L1,只读 session/catalog/data;negative:R0·受控失败,只改变本 session 参数或 expected target,要求精确失败;review/all:组合上述三类,不创建持久对象、不写业务数据。
即使是 R0,也必须指向已确认 target:catalog 与 query text 可能包含业务信息,过宽监控身份也可能越权。本章使用教学 L1 的 direct admin service。
6.7.1 审查 ch01–ch05 已出现的候选规则
第一轮不从空白页“想 25 条最佳实践”,而是回看前五章的可重复证据:
| 来源 | 已验证事实 | 收敛出的规则族 |
|---|---|---|
| ch01 | target、role/schema ownership、危险 reset | CONN / ROLE / DEST |
| ch02 | service file、session context、脚本 evidence | CONN / SECR / SESS / EVID |
| ch03 | 业务不变量、关系/标识边界、fixture | NAME / KEYS / FIXT |
| ch04 | type、named constraint、migration、partition ADR | CONS / MIGR / TYPE / PART |
| ch05 | query path、failed transaction、lock/retry evidence | QUER / PAGE / TXNN / RETR / PLAN |
baseline-v0.1.json 中每个 evidence item 都包含:
artifact 必须存在于当前 repository,chapter 必须属于 v0.1 source set,observation 必须说出从资产观察了什么。checker 还要求五章都至少贡献 active evidence,防止版本说明宣称覆盖 ch01–ch05,实际只引用其中两章。
从候选到 25 条 active rule
审查按四步进行:
- 合并同一失败机制的重复句子;
- 把同时包含多个独立风险的句子拆开;
- 评估后果,定为 safety/default/preference;
- 为每条规则指定最小 check 与 exception mode。
最终分组如下:
| Safety(10) | Defaults(10) | Preferences(5) |
|---|---|---|
| target、secret、role、definer | session、context、name、type | text |
| constraint、migration、txn failure | key、query、txn size | semi-structured |
| retry、destructive action、pagination | fixture、evidence、version | partition、advanced SQL、plan |
完整标题在 baseline-guide.md 中。指南只出现一次每个 Rule ID;详细 statement/rationale/evidence/exception/checks 以 JSON registry 为准。若人类指南与 registry 分叉,static gate 失败。
对等级做一次对抗性复核
每条 safety 都反问:
- 违反是否真的可能直接导致数据错误、越权、不可恢复动作或不可归因事故;
- 是否存在同样安全但不满足当前 statement 的合理方案;
none与breakglass是否被正确选择;- 当前 check 是否能看见失败机制,而不是只检查格式。
每条 default 都反问:
- 统一默认真正减少了什么测试/维护矩阵;
- 哪些 workload 合理偏离;
- waiver 是否有可验证补偿和 expiry。
每条 preference 都反问:
- 它是否只是作者品味;
- 是否存在多个同样正确的方案;
- review 需要什么 workload/计划/维护证据。
这一步把“稳定分页”从普通 SQL 风格提升为 safety,因为无全序会直接破坏 API 跨页正确性;同时把 text、分区与高级 SQL 保留为 preference,避免把场景判断伪装成数据库铁律。
已知缺口必须进入输出
SAFE-RETR-008 的失败后果足以成为 safety,但 ch01–ch05 尚未提供完整多会话 retry harness。它的 check 仍只有:
因此 checker 计算“至少一个 automated/runtime check 的 safety”时输出 9,不允许作者手工写成 10。ch10 必须补入 40001/40P01 整体重试、bounded backoff 与 outcome reconciliation 的运行证据。这就是版本化 baseline 与普通文档清单的差别:未知被编码进验收,而不是藏在脚注里。
6.7.2 为 pg36_shop 建立最小质量门
先下载/进入资产目录:
第一道门:static
不设置任何数据库环境变量也能运行:
baseline-check.txt 的稳定摘要应为:
baseline_checksum 是按规范化 JSON 计算的 registry 内容指纹,不等于文件原始 bytes 的 sha256sum。改变缩进不会改变 canonical checksum,改变规则、兼容范围或证据会改变。manifest.txt 另行保存 13 个文件的原始 SHA-256,以便复现本次具体输入。
数量会在新版本有意变化;v0.1 内若静默变化,必须先解释 registry/manifest diff 并更新本文验收,不能为了让 CI 绿而改 expected count。
static action 还输出:
shell-syntax.txt:本书受管 lab shell 的bash -n结果与数量;baseline-check.stderr:成功时为空;- evidence-local Python bytecode:不污染 source tree;
gate-summary.txt:static=pass,live/negative 为 skipped。
第二道门:live
先确认 ch04-v1:
service 必须指向 pg36_shop 的 direct writable primary,登录身份可受控 SET ROLE pg36_owner。然后:
执行路径:
session-profile.txt 应证明 UTF8、UTC、pg_catalog, shop、三类 timeout 和 application name;model-verify.txt 应保留:
query-contract.txt 应证明 11 列 view shape、稳定 keyset 顺序、两页不重叠、business/idempotency key 唯一。live 全部是 read-only;如果 relation checksum 改变,说明前置状态已经漂移,不能用本章脚本修复。
第三道门:negative
在同一已确认 L1:
稳定摘要:
这里 status=ok 表示两个错误都按预期被拒绝,不是错误 SQL 成功。stdout/stderr 分开保存,可以复核 SQLSTATE 恰好出现一次。
发布候选:review/all
review 与 all 当前执行同一条完整路径;前者强调人工发布语义,后者适合作为自动任务 action:
预期:
只有同时检查 source diff、baseline canonical checksum、model checksum、wrong-session/target 和 evidence manifest,才批准 v0.1。动态 server version、timestamp、PID 不做 golden。
Gate 的失败边界
完整 action 未设置 PGSERVICEFILE 时必须在连接前以 usage/exit 64 拒绝;未知 action 同样退出 64。缺少工具退出 69。数据库或断言失败保留非零 psql/script exit,不改写成绿色 summary。
gate 不负责:
- 自动安装 PostgreSQL/Pigsty;
- 自动创建/修复 ch04 模型;
- 自动注入 credential;
- 自动 apply Pigsty inventory;
- 自动批准 waiver/breakglass;
- 自动对生产执行 migration/reset。
这些边界让错误前置条件尽早暴露,也防止“质量脚本”获得超出检查所需的修改权限。
6.7.3 预留 ch07–ch11 的证据追加区
evidence-ledger.md 已固定下一阶段的证据路线:
| 版本候选 | 章节 | 必须新增的运行证据 | 主要影响 |
|---|---|---|---|
| v0.2 | ch07 | estimate/actual、statistics、plan settings | PREF-PLAN-005 |
| v0.3 | ch08 | workload attribution、wait taxonomy、慢查询闭环 | DEFAULT-EVID-009 |
| v0.4 | ch09 | index benefit/cost、write amplification、concurrent build | PREF-PLAN-005 |
| v0.5 | ch10 | lost update、write skew、deadlock、40001 retry | SAFE-RETR-008 |
| v0.6 | ch11 | expand/contract、lock budget、application compatibility | SAFE-MIGR-006 / DEFAULT-VERS-010 |
版本号是候选节奏,不要求每章机械升级。若新证据只重复原结论,可以追加 ledger 而不改 statement;若发现 scope、level、exception 或 check 需要变化,则发布新 minor version,并说明:
不要改写 v0.1 文件后仍称 v0.1。最简单的历史保护是保留不可变 release artifact/tag 与 checksum;主干上的“current”可以指向最新版本。
新证据既可能收紧,也可能撤销规则
例如 ch07 可能证明某种统计问题才是估算失真的根因,因而 PREF-PLAN-005 应增加 statistics check,而不是升级成“禁止 Seq Scan”。ch10 可能发现某类 transaction 因外部副作用无法自动 retry,于是 safety statement 需要收紧 idempotency/reconciliation,而不是只加重试次数。
规则体系的价值不在于永远维护最初判断,而在于让反例可以有秩序地改变判断。
每章回写的最小格式
追加证据至少记录:
生产 incident 可以成为证据,但必须去除敏感数据并保留足够机制信息;不能只写 incident ticket URL,让离线读者无法理解结论。
6.7.4 在 ch12 汇总为 v1.0 的验收条件
ch12 不是把 v0.6 改名为 v1.0。它要在一个真实后端服务交付中贯通:
flowchart LR
A["Pigsty desired state"] --> B["角色 / DB / services"]
B --> C["versioned schema"]
C --> D["application query + transaction"]
D --> E["pool / routing / observability"]
E --> F["failure + retry + release"]
F --> G["v1.0 evidence bundle"]v1.0 必须同时满足:
- 每条 active rule 至少有一个可重复实验或已脱敏生产事件证据;
- 所有 safety 都有 automated 或 runtime gate,不只依赖文字 review;
- 每个 active exception 有 owner、expiry、补偿控制与复核结果;
- query/transaction/DDL 规则在同一个后端服务交付中实际走完;
- static、live、negative 可在统一 L1 重跑;
- v0.x 的 false positive、false negative、waiver 与 incident 已回写 rationale;
- compatibility matrix 对 PostgreSQL 14–18 与当前 Pigsty baseline 有明确结果或限制;
- pooled application path、direct migration path 与 failover route 均有证据;
- v1.0 有从 v0.x 迁移说明,不静默改变既有 Rule ID 语义;
- release artifact、source manifest、canonical checksum 与签署 owner 完整。
若 ch10 未把 safety 覆盖从 9/10 提升到 10/10,或者 ch12 只在 direct admin session 验证而没有 application/pool path,v1.0 必须推迟。deadline 不能改变验收事实。
v0.1 发布记录
当前发布候选:
这个记录只在完整 review gate 与全书 structure/link/build 检查通过后成立。任何 registry 语义变更都应产生新 checksum 和新版本;任何运行环境变化都应产生新的 evidence bundle,而不是覆盖旧证据。
到这里,我们没有得到一本万能的 PostgreSQL 风格指南,而是得到了一套可以被验证、质疑、例外、升级和审计的规则系统。下一章开始,性能与并发专题会不断用新证据挑战它。
上一节:将规约接入统一实验环境 · 返回本章目录 · 下一章:追本溯源:执行计划与统计信息 · 查看全书目录 · 查看索引中心