# 实战：从单机证据到选型 ADR

LLMS 索引： [llms.txt](/llms.txt)

---

本节把前五节压成一个可重复的 `1.5-proposal`：

```text
freeze generator and monthly golden
  -> bootstrap two retained shard database shells
  -> build two exact remote schemas
  -> build a local single-node baseline
  -> build LIST-partitioned foreign parents
  -> compare four byte-identical result paths
  -> collect parallel/index/spill/summary plans
  -> collect pruning/pushdown/transfer plans
  -> preserve a non-pushed JOIN counterexample
  -> prove application privilege denial
  -> prove partial shard failure semantics
  -> checksum catalogs and business state
  -> reject unsafe reset attempts
  -> pre-verify three databases
  -> exact per-database reset
  -> rebuild and repeat the entire review
```

正式证据来自 PostgreSQL 18.6 / `postgres_fdw` 1.2 的受控本地开发实例。
Pigsty 4.5 的 topology 和职责已经映射，但没有执行 L1：

```text
pigsty_l1=not-run
```

> **破坏边界**
>
> `task.sh all` 会删除并重建精确标记的 `shop_ch17`、`shop_ch17_ext`、
> 两个 foreign server、六个 user mapping，以及
> `pg36_shard_a`/`pg36_shard_b` 中的 `shop_ch17_shard`。两个数据库壳会
> 保留。只可在本书受控开发 fixture 中执行，禁止在生产运行。

## 17.6.1 证明一个边界，拒绝一个伪瓶颈 {#item-17-6-1}

### 前置连接

沿用第 4 章管理员 service：

```ini
[pg36-admin]
host=/path/to/socket-or-host
port=5432
dbname=pg36_shop
user=postgres
```

```bash
chmod 600 /path/to/pg_service.conf
export PGSERVICEFILE=/path/to/pg_service.conf
export PGSERVICE=pg36-admin
```

密码应放在受控 secret/service 机制中，不出现在命令行、脚本、evidence 或 Git。

### 环境保护

协调端
[`context.sql`](/labs/ch17/context.sql)
要求：

```text
database = pg36_shop
writable primary
PostgreSQL major = 18
session_user = postgres superuser
can SET ROLE pg36_owner
pg36_app = constrained LOGIN non-superuser
ch04-v1 physical model exists
postgres_fdw 1.2 available or exact managed state
pg36_shard_a/b database shell identity exact
fdw host/port exactly equal current instance
```

远端
[`remote-context.sql`](/labs/ch17/remote-context.sql)
还核对：

```text
expected database name
database owner/comment
shard remainder
shard marker
UTC/ISO session
timeouts
```

任何已有同名 schema、server、mapping、extension 或 database 身份不符都停止，
不会因名字相同就接管。

### 资产清单

```text
static/labs/ch17/
├── fixture-manifest.json
├── frozen-monthly.csv
├── fixture.sql
├── fixture-remote.sql
├── bootstrap.sql
├── remote-context.sql
├── remote-setup.sql
├── context.sql
├── setup.sql
├── verify.sql
├── remote-verify.sql
├── final-state.sql
├── reset.sql
├── remote-reset.sql
├── review.py
├── task.sh
├── analytics-distributed-adr.md
├── baseline-v1.5-proposal.json
└── pigsty-declaration.example.yml
```

另有四份月报导出、十份计划、五份 catalog、安全边界与单分片失败探针。

### 冻结输入身份

[`fixture-manifest.json`](/labs/ch17/fixture-manifest.json) 固定：

```text
frozen-monthly.csv
  rows=32
  sha256=64b045809e10364fd84a587121d919e8562a15335c4c6c015e91a0ead3a44323

fixture.sql
  sha256=3110d0369b0c62fffeee643200f5320d1f6bd26ad5f9950b2ab2b58991080e10

fixture-remote.sql
  sha256=da02dbca8d2cfeb0294c369b15cad3619a9433d984f98866b5043eb4508e3e91
```

生成器不读取当前时间，不使用随机数。冻结时间只是 fixture metadata，不参与
运行时决定。

### 单步建立

```bash
./static/labs/ch17/task.sh setup
```

setup 的顺序：

```text
connect maintenance database postgres
  -> create/reuse exact pg36_shard_a/b shells

connect pg36_shard_a
  -> exact remote schema rebuild
  -> load even tenant IDs
  -> analyze

connect pg36_shard_b
  -> exact remote schema rebuild
  -> load odd tenant IDs
  -> analyze

connect pg36_shop
  -> exact FDW/data schema rebuild in one transaction
  -> load full local baseline
  -> create indexes/materialized summary
  -> create LIST foreign partitions/views/grants
  -> analyze
  -> commit
  -> VACUUM ANALYZE local sales fact
```

协调端 DDL 在单事务内，避免半成品；数据库壳创建和三个数据库的 schema
建立不能放在一个全局事务里。

### 为什么 setup 后显式 vacuum

覆盖索引：

```sql
CREATE INDEX sales_fact_tenant_day_idx
ON shop_ch17.sales_fact (
  tenant_id,
  occurred_on,
  account_id
)
INCLUDE (amount, units, channel);
```

理论上包含查询所需列，但 Index Only Scan 是否免 heap 访问还取决于 visibility
map。新装载表的 page 未必 all-visible。第一轮自动验收曾真实得到：

```text
Bitmap Heap Scan on sales_fact
  -> Bitmap Index Scan on sales_fact_tenant_day_idx
```

而审查器要求：

```text
Index Only Scan
Heap Fetches: 0
```

正确修复不是放宽断言，而是在 `COMMIT` 后显式：

```sql
VACUUM (ANALYZE) shop_ch17.sales_fact;
```

随后两个完整周期都稳定得到：

```text
Index Only Scan using sales_fact_tenant_day_idx
  actual rows=7500
  Heap Fetches: 0
```

这个经历说明：计划回归不仅依赖 DDL 和数据，也依赖 vacuum/visibility
状态。实验必须显式制造自己的前置条件。

### 本地事实

[`fixture-facts.sql`](/labs/ch17/fixture-facts.sql)：

```text
distributed_amount=2256000.00
distributed_sales=240000
distributed_units=1200000
first_day=2026-01-01
last_day=2026-04-30
local_amount=2256000.00
local_sales=240000
local_units=1200000
shard_rows=dist_0:120000,dist_1:120000
summary_rows=2880
summary_sales=240000
```

远端：

| 数据库 | 租户 | 账户 | 销售 | units | amount | checksum |
|---|---|---:|---:|---:|---:|---|
| `pg36_shard_a` | 2,4,6,8 | 200 | 120,000 | 599,988 | 1,188,000.00 | `274002…24e3` |
| `pg36_shard_b` | 1,3,5,7 | 200 | 120,000 | 600,012 | 1,068,000.00 | `0bb770…2629` |

总数相等不够，物理分片 checksum 也必须匹配。

### 并行边界

```bash
psql -X -w \
  --dbname="service=$PGSERVICE" \
  --set=fdw_host=/path/to/socket \
  --set=fdw_port=5432 \
  --file=static/labs/ch17/local-parallel-plan.sql
```

通常不需要手工运行；task 会动态读取 socket/port 并注入。

冻结关键路径：

```text
Finalize HashAggregate
  -> Gather
       Workers Planned: 2
       Workers Launched: 2
       -> Partial HashAggregate
            -> Parallel Seq Scan on sales_fact
                 actual rows=80000 loops=3
```

证明：

```text
parallel path exists
two workers actually launched
aggregation decomposes into partial/final
240k rows become 32 groups
```

不证明：

```text
production speedup
safe system-wide worker count
behavior under concurrent reports
```

### work_mem 边界

低内存：

```text
Sort actual rows=240000
Sort Method: external merge
Disk: about 5920kB
temp read/write present
```

高内存：

```text
Sort actual rows=240000
Sort Method: quicksort
Memory: about 13645kB
no temp read
```

两个文件：

- [`spill-low-plan.sql`](/labs/ch17/spill-low-plan.sql)
- [`spill-high-plan.sql`](/labs/ch17/spill-high-plan.sql)

这证明 spill 可被定位到一个节点；不授权全局调大 `work_mem`。

### 汇总边界

[`raw-aggregate-plan.sql`](/labs/ch17/raw-aggregate-plan.sql)：

```text
Parallel Seq Scan on sales_fact
actual rows=80000 loops=3
```

[`summary-aggregate-plan.sql`](/labs/ch17/summary-aggregate-plan.sql)：

```text
Seq Scan on daily_tenant_summary
actual rows=2880 loops=1
```

两者输出同一 32 行月报。ADR 因而可以拒绝：

```text
“全局月报每次扫描 24 万行，所以必须分片”
```

更准确的结论：

```text
这条重复聚合可先用 2,880 行日粒度处理；
生产还要评审刷新、新鲜度、迟到和恢复成本。
```

### BRIN 只记录候选

catalog 验证：

```text
sales_fact_day_brin_idx
  access_method=brin
  operator_class=pg_catalog.date_minmax_ops
  size > 0
  size < covering B-tree in this fixture
```

没有强制一条 BRIN 查询并宣称更快。小表无法代表物理相关性和 block range
收益。

## 17.6.2 比较单机加速与一个分布式候选 {#item-17-6-2}

### 四条相同结果路径

自动导出：

```text
monthly-local.csv
monthly-summary.csv
monthly-distributed.csv
monthly-two-stage.csv
```

分别来自：

- [`monthly-local-export.sql`](/labs/ch17/monthly-local-export.sql)
- [`monthly-summary-export.sql`](/labs/ch17/monthly-summary-export.sql)
- [`monthly-distributed-export.sql`](/labs/ch17/monthly-distributed-export.sql)
- [`monthly-two-stage-export.sql`](/labs/ch17/monthly-two-stage-export.sql)

任务逐个：

```bash
cmp static/labs/ch17/frozen-monthly.csv \
    evidence/.../monthly-local.csv
```

四个文件都要 byte-identical，不做“行数相等即可”的弱比较。

### 单租户裁剪

[`tenant-pruned-plan.sql`](/labs/ch17/tenant-pruned-plan.sql)：

```sql
SELECT count(*), sum(amount)
FROM shop_ch17.sales_fact_distributed
WHERE tenant_id = 3
  AND occurred_on >= DATE '2026-04-01';
```

关键计划：

```text
Foreign Scan on shop_ch17.sales_fact_dist_1
  actual rows=7500
  Remote SQL:
    SELECT amount
    FROM shop_ch17_shard.sales_fact
    WHERE occurred_on >= '2026-04-01'
      AND tenant_id = 3
```

审查器明确要求：

```text
sales_fact_dist_1 present
sales_fact_dist_0 absent
tenant/date in Remote SQL
```

这同时证明 partition pruning、column projection 和 filter pushdown。

### 朴素全局聚合

[`distributed-naive-plan.sql`](/labs/ch17/distributed-naive-plan.sql) 直接查询
分区父表：

```text
HashAggregate
  -> Append actual rows=240000
       -> Foreign Scan dist_1 actual rows=120000
            Remote SQL: SELECT tenant_id, occurred_on, units, amount ...
       -> Foreign Scan dist_0 actual rows=120000
            Remote SQL: SELECT tenant_id, occurred_on, units, amount ...
```

远端没有 `GROUP BY`，协调端获得 24 万条事实再聚合。

这不表示 `postgres_fdw` 永远不能下推 aggregate；它说明这条父表查询在本次
目标版本和 SQL 形状下没有得到期望的跨分片预聚合。

### 两阶段聚合

[`distributed-two-stage-plan.sql`](/labs/ch17/distributed-two-stage-plan.sql)
显式分别查询两个 foreign partition：

```sql
SELECT tenant_id, occurred_on,
       count(*), sum(units), sum(amount)
FROM shop_ch17.sales_fact_dist_0
GROUP BY tenant_id, occurred_on

UNION ALL

SELECT tenant_id, occurred_on,
       count(*), sum(units), sum(amount)
FROM shop_ch17.sales_fact_dist_1
GROUP BY tenant_id, occurred_on;
```

外层再按月合并。计划：

```text
GroupAggregate actual rows=32
  -> Sort actual rows=960
       -> Append actual rows=960
            -> Foreign Scan Aggregate dist_0 actual rows=480
                 Remote SQL ... GROUP BY 1, 2
            -> Foreign Scan Aggregate dist_1 actual rows=480
                 Remote SQL ... GROUP BY 1, 2
```

数据流：

\[
\frac{960}{240000} = 0.004
\]

即返回行数为朴素路径的 0.4%。这个比例只描述冻结 fixture 的行数，不能直接
转成“性能提升 250 倍”。

### 同分片 JOIN 反例

[`collocated-parent-plan.sql`](/labs/ch17/collocated-parent-plan.sql)：

```sql
SELECT
  account.segment,
  count(*) AS sale_count,
  sum(sale.amount) AS amount_total
FROM shop_ch17.sales_fact_distributed AS sale
JOIN shop_ch17.account_dim_distributed AS account
  ON account.tenant_id = sale.tenant_id
 AND account.account_id = sale.account_id
WHERE sale.tenant_id = 3
  AND sale.occurred_on >= DATE '2026-04-01'
GROUP BY account.segment;
```

物理上两表的 tenant 3 都在 shard B。实测：

```text
Hash Join on coordinator actual rows=7500
  -> Foreign Scan sales_fact_dist_1 actual rows=7500
  -> Hash
       -> Foreign Scan account_dim_dist_1 actual rows=50
```

两条 Remote SQL 都没有 JOIN。这个反例写进 ADR：

> 对目标抽象层而言，共置只是设计前提，不是下推证据。必须检查目标产品、
> 版本、SQL 与 `EXPLAIN VERBOSE`。

若生产候选是 Citus，应在真实 Citus L1 上创建目标 distributed/reference
tables，重新验证 colocated join；不能用 FDW 反例替代，也不能假定一定成功。

### 应用身份

成功读：

```text
sale_count=7500
amount_total=69375.00
```

写入负例：

```text
exit=3
SQLSTATE 42501
```

catalog：

```text
app_schema_usage=true
app_local_sales_select=true
app_local_sales_write=false
app_distributed_sales_select=true
app_distributed_sales_write=false
app_server_a_usage=true
app_server_b_usage=true
```

六个 mapping 都是具名；`password_required=false` 被 review 当成必须显式存在
的 lab-only 警告，而不是悄悄依赖环境。

### 单分片失败

执行器先查询 tenant 2：

```text
healthy_shard_tenant_2=30000
```

再执行全局 count，固定：

```text
exit=3
SQLSTATE 08001
```

任务同时捕获 stdout/stderr；如果只看 stderr，就无法证明健康分片路径曾成功。

错误断开使事务回滚后，重新导出：

```text
server-catalog-after-failure.csv
```

并与故障前 `server-catalog.csv` 逐字节比较，确保 shard B port 没有残留为 1。

### 一次完整 evaluate

如果不需要 reset/rebuild 双周期：

```bash
evidence_dir="$PWD/evidence/ch17/evaluate-$(date -u +%Y%m%dT%H%M%SZ)"

PG36_EVIDENCE_DIR="$evidence_dir" \
  ./static/labs/ch17/task.sh evaluate
```

`evaluate` 会重建一次、采集所有证据并运行 review。

仅验证现有数据库：

```bash
PG36_EVIDENCE_DIR="$PWD/evidence/ch17/verify" \
  ./static/labs/ch17/task.sh verify
```

它分别运行 coordinator、shard A、shard B 的完整数据库内断言。

### review 的职责

[`review.py`](/labs/ch17/review.py) 不连数据库，只审查 evidence：

```text
manifest version/target/checksum
source generator and frozen CSV hashes
four byte-identical monthly exports
local/remote cardinality and checksums
server/mapping/relation/index/security/size catalogs
parallel/index/spill/summary plans
tenant pruning and Remote SQL
naive/two-stage row shape
non-pushed JOIN counterexample
application read/write
shard failure and restored server catalog
final state
coordinator/remote verify outputs
baseline ADR contract
```

这使 evidence 可以离线审阅，也避免“数据库后来变了，旧报告仍假装当前”。

## 17.6.3 输出 ADR、PoC 证据、生产代价和撤退路线 {#item-17-6-3}

### ADR 不以产品名开头

[`analytics-distributed-adr.md`](/labs/ch17/analytics-distributed-adr.md)
先写背景和决策顺序：

```text
1. fix correctness, SQL, statistics, paths
2. prove parallel/index/BRIN/spill/summary on one node
3. assess offline replica for tolerable-staleness reads
4. enter distribution only after measured resource boundary
5. evaluate Citus when PostgreSQL-compatible sharding fits
6. compare specialized OLAP only when PostgreSQL paths miss SLO
```

`postgres_fdw` 的定位是 mechanics/counterexample lab，不是生产性能结论。

### ADR 的冻结证据

| 问题 | 证据 | 决策影响 |
|---|---|---|
| 可并行？ | 2 workers launched | 单机还有并行路径 |
| 选择性查询？ | index-only，heap fetch 0 | 先修访问路径 |
| 内存？ | 64kB 外排 / 32MB 内排 | 按并发设局部预算 |
| 重复聚合？ | 240k vs 2,880 输入 | 先评估汇总 |
| 单租户路由？ | 只访问 shard B | `tenant_id` 可局部 |
| 朴素全局？ | 240k foreign rows | 协调端/网络风险 |
| 两阶段？ | 960 aggregate rows | 计算靠近数据 |
| 同分片 JOIN？ | coordinator Hash Join | 必须实测下推 |
| shard 失败？ | scoped read 成功/global 08001 | 定义 partial semantics |

### 决策与限制同版本

[`baseline-v1.5-proposal.json`](/labs/ch17/baseline-v1.5-proposal.json)
固定：

```text
target versions
default path
distribution key
routing warning
remote aggregation design
production Citus gate
fixture contracts
expected checksums
lab authentication warning
evidence inventory
rollback contract
limitations
```

canonical JSON SHA-256：

```text
3dcb7308cf6983122ee860ad3dc2a4b44651549e3d5631770839bb9a0be450c6
```

改变 ADR contract 会改变 release candidate identity。

### 进入生产 PoC 前的代价

ADR 要预算：

```text
schema/query changes for distribution key
backfill and dual-write/read
coordinator/worker nodes
HA and service routing
network/TLS/auth
backup repository and restore
monitoring/alerting
rebalance capacity
rolling/major upgrade
on-call training
license/cloud cost
exit rehearsal
```

不能只比较机器数量。

### Pigsty 交付分支

[`pigsty-declaration.example.yml`](/labs/ch17/pigsty-declaration.example.yml)
保留两个候选：

```text
Path A:
  pg-analytics primary + replica with pg_offline_query
  pg_conf: olap.yml

Path B:
  pg-citus0/1/2 groups
  pg_mode: citus
  pg_shard / pg_group
```

它们不是同一 inventory 的叠加方案，也不是完整生产配置。ADR 应先决定测哪条
假设，再补齐目标地址、database、users、extensions、HBA、secret、backup、
service 和 HA。

### 撤退路线

PoC 撤退：

```text
stop new lab sessions
verify coordinator + A + B exact state
drop coordinator views/matview/tables
drop mappings/servers/extension
drop remote tables/schemas
retain empty database shells
verify zero remaining managed objects
```

生产迁移撤退则应提前设计：

```text
canonical source of truth remains PostgreSQL
dual-read compares checksums
route change has version/epoch
old and new writers cannot both own same key
backfill has watermark and resume point
last reversible point is named
service/DNS rollback is tested
new system data can be exported back
```

### reset 的三道 guard

协调 reset 需要：

```text
PG36_RESET_TOKEN=RESET_CH17_ANALYTICS_FDW_LAB
PG36_RESET_TARGET=pg36_shop/shop_ch17+shop_ch17_ext+fdw
```

错误 action token：

```text
SQLSTATE P3660
reset refused: invalid ch17 action token
```

错误 target：

```text
SQLSTATE P3661
reset refused: invalid ch17 target token
```

存在 `application_name LIKE 'pg36-ch17-%'` 的其他 worker：

```text
SQLSTATE P3663
reset refused: ch17 workers are active
```

task 会主动启动一个 sleep worker，观察到 PID 后证明 P3663，再 cancel。

### 精确 reset

协调端顺序：

```sql
DROP VIEW ...
DROP MATERIALIZED VIEW ...
DROP TABLE exact parents/local tables ...
DROP SCHEMA shop_ch17;

DROP USER MAPPING ...  -- six exact mappings
DROP SERVER ...        -- two exact servers
DROP EXTENSION postgres_fdw;
DROP SCHEMA shop_ch17_ext;
```

不使用 `CASCADE`。输出：

```text
status=coordinator-reset-ok
remaining_data_schema=0
remaining_extension_schema=0
remaining_ch17_servers=0
retained_shard_databases=pg36_shard_a,pg36_shard_b
```

每个 remote：

```text
full remote verify
BEGIN
drop sales_fact
drop account_dim
drop fixture_meta
drop schema
COMMIT
```

输出：

```text
status=remote-reset-ok
remaining_schema=0
database_shell=retained
```

### 跨库非原子边界

任务先对三个数据库全部 preflight verify，再按：

```text
coordinator -> shard A -> shard B
```

退出。每一步各自事务化，但整体不是一个事务。若 A reset 后 B 失败，系统处于
部分退出状态；恢复方式是按 exact identity 继续补偿或完整重建。

这项 limitation 同时写入 lab contract、baseline 和 ADR，不能用脚本“看起来
是一条命令”掩盖。

### 手工 reset

只有明确需要退出 fixture 时：

```bash
export PG36_RESET_TOKEN=RESET_CH17_ANALYTICS_FDW_LAB
export PG36_RESET_TARGET=pg36_shop/shop_ch17+shop_ch17_ext+fdw

PG36_EVIDENCE_DIR="$PWD/evidence/ch17/reset" \
  ./static/labs/ch17/task.sh reset
```

它会同时处理两个远端 schema。不要在生产、共享开发数据库或身份未知的目标
上执行。

## 17.6.4 验收采用 `checklist:evidence` {#item-17-6-4}

### 完整双周期

```bash
evidence_dir="$PWD/evidence/ch17/$(date -u +%Y%m%dT%H%M%SZ)"

PG36_EVIDENCE_DIR="$evidence_dir" \
  ./static/labs/ch17/task.sh all
```

成功输出：

```text
status=ok
fixture=frozen-byte-identical-four-paths
single_node=parallel+index+summary+spill
distributed=tenant-pruning+fdw+two-stage
counterexamples=hash-is-not-modulo+join-not-pushed
failure=healthy-shard-read+global-08001
guards=P3660+P3661+P3663
postgres_fdw=1.2
pigsty_l1=not-run
release_candidate_checksum=3dcb7308cf6983122ee860ad3dc2a4b44651549e3d5631770839bb9a0be450c6
```

目录：

```text
evidence/ch17/<run>/
├── cycle-1/
├── reset-wrong-token.*
├── reset-wrong-target.*
├── reset-active-worker.*
├── reset-exact/
└── cycle-2/
```

### `checklist:evidence`

| 验收项 | evidence | 通过条件 |
|---|---|---|
| `manifest` | `manifest.txt` | PG18、FDW 1.2、三库、checksums |
| `source` | manifest + fixture JSON | generator/golden SHA 精确 |
| `golden` | 四份 monthly CSV | 与 frozen byte-identical |
| `local-facts` | `fixture-facts.csv` | 240k、1.2m、2.256m |
| `remote-facts` | `remote-*-state.csv` | 各 120k + shard checksum |
| `parallel` | `local-parallel-plan.txt` | planned/launched=2、partial/final |
| `covering-index` | `selective-index-plan.txt` | 7,500、Heap Fetches 0 |
| `spill` | low/high plans | external merge vs quicksort |
| `summary` | raw/summary plans | 240k vs 2,880 input shape |
| `pruning` | `tenant-pruned-plan.txt` | only dist_1 + remote filters |
| `naive` | naive plan | 240k foreign rows |
| `two-stage` | two-stage plan | 480+480 remote aggregate rows |
| `join-counterexample` | collocated plan | coordinator Hash Join |
| `auth` | mapping/security catalogs | six named mappings, read-only app |
| `app-failure` | `app-write.*` | exit 3, SQLSTATE 42501 |
| `shard-failure` | `shard-failure.*` | healthy=30k, global 08001 |
| `catalog-rollback` | two server catalogs | byte-identical |
| `database-verify` | three verify outputs | coordinator + A + B status ok |
| `final-state` | `final-state.csv` | release/rows/checksums exact |
| `review` | `review.txt` | `status=review-ok` |
| `reset-guards` | root failure files | P3660/P3661/P3663 |
| `exact-reset` | reset outputs | schemas/servers zero, DB shells retained |
| `rebuild` | cycle-2 | same complete review |

### 最终状态

[`final-state.sql`](/labs/ch17/final-state.sql) 固定：

```text
business_checksum=42fb8ab5444469eba1f104a8e1e529dd
distributed_sales=240000
fixture=ch17-analytics-v1
local_sales=240000
monthly_checksum=644d45544ebbc2a80c42270c38ac6885
naive_transfer_rows=240000
postgres_fdw=1.2
release=1.5-proposal
shard_rows=dist_0:120000,dist_1:120000
summary_rows=2880
tenant3_april=7500:69375.00
two_stage_transfer_rows=960
```

`naive_transfer_rows` 和 `two_stage_transfer_rows` 是与计划合同共同审查的固定
事实；如果 SQL 或版本改变，不能只保留硬编码值，必须重新采集 plan 并更新
proposal。

### review 输出

```text
status=review-ok
fixture=frozen-byte-identical-four-paths
single_node=parallel+index+summary+spill
distributed=tenant-pruning+fdw+two-stage
counterexamples=hash-is-not-modulo+join-not-pushed
failure=healthy-shard-read+global-08001
business_checksum=42fb8ab5444469eba1f104a8e1e529dd
monthly_checksum=644d45544ebbc2a80c42270c38ac6885
release_candidate_checksum=3dcb7308cf6983122ee860ad3dc2a4b44651549e3d5631770839bb9a0be450c6
```

### 失败排查顺序

如果 task 失败：

1. 保留 evidence，不立即重跑覆盖；
2. 找到最后产生的 stderr；
3. 判断是环境 guard、业务 checksum、计划 shape、权限还是故障边界；
4. 连接三个数据库分别运行 verify；
5. 检查 server catalog 是否已回滚；
6. 修复原因，不降低断言掩盖差异；
7. 新建 evidence 目录完整重跑两个周期；
8. 比较两个 manifest。

第一轮 index-only 失败就是这个流程的例子：证据显示 Bitmap Heap Scan，
根因是 visibility map precondition，没有把 reviewer 改成接受任意 index
路径。

### 生产发布仍缺什么

本地 `status=ok` 之后仍需：

```text
[ ] representative production-scale data and skew
[ ] independent nodes and failure domains
[ ] target network/TLS/identity
[ ] Pigsty L1 inventory and runtime convergence
[ ] actual Citus or selected candidate, not FDW surrogate
[ ] OLTP+OLAP mixed concurrency
[ ] P50/P95/P99 and open-loop throughput
[ ] CPU/memory/I/O/temp/WAL/network cost
[ ] coordinator/worker HA
[ ] backup-to-empty restore checksum
[ ] rebalance interrupt/resume
[ ] schema change mixed-version test
[ ] rolling/major upgrade
[ ] RPO/RTO and partial-result semantics
[ ] migration dual-read and last rollback point
[ ] exit rehearsal
```

因此 `1.5-proposal` 是设计与本地机制候选，不是生产批准。

### 本章最终决策

在冻结工作负载上，当前可支持的结论是：

1. 单机仍有并行、覆盖索引、spill 治理和汇总空间；
2. 不能因 24 万行原表扫描直接宣布需要分布式；
3. `tenant_id` 对单租户访问有良好局部性；
4. 分布式全局聚合必须关注计算位置与协调端输入；
5. 同分片不自动证明 JOIN 下推；
6. 路由算法不一致会导致静默错误，HASH remainder 不能当整数取模；
7. 部分失败和跨数据库退出必须成为业务/运维合同；
8. 下一层应先比较 Pigsty offline replica；若代表性容量仍越界，再在真实
   Pigsty Citus L1 上验证分布键、HA、恢复和再平衡；
9. 专用 OLAP 只有在 PostgreSQL 路径无法满足已定义 SLO，且团队接受第二套
   数据管道与值班体系时进入终选。

这就是一份合格选型 ADR 的语气：它不承诺某产品必胜，而是清楚说明当前证据
允许做什么、禁止外推什么，以及什么新证据会触发下一次决策。

---

[上一节：部署最小分布式 PoC](../05/) · [返回本章目录](../) · [下一章：万法归宗：PostgreSQL 数据平台与替代边界](/data-platform-boundaries/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
