跳至内容
17.6 实战:从单机证据到选型 ADR

17.6 实战:从单机证据到选型 ADR

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

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.4 / postgres_fdw 1.2 的受控本地开发实例。 Pigsty 4.4 的 topology 和职责已经映射,但没有执行 L1:

pigsty_l1=not-run

破坏边界

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

17.6.1 证明一个边界,拒绝一个伪瓶颈

前置连接

沿用第 4 章管理员 service:

[pg36-admin]
host=/path/to/socket-or-host
port=5432
dbname=pg36_shop
user=postgres
chmod 600 /path/to/pg_service.conf
export PGSERVICEFILE=/path/to/pg_service.conf
export PGSERVICE=pg36-admin

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

环境保护

协调端 context.sql 要求:

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 还核对:

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

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

资产清单

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 固定:

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

fixture.sql
  sha256=3110d0369b0c62fffeee643200f5320d1f6bd26ad5f9950b2ab2b58991080e10

fixture-remote.sql
  sha256=da02dbca8d2cfeb0294c369b15cad3619a9433d984f98866b5043eb4508e3e91

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

单步建立

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

setup 的顺序:

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

覆盖索引:

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。第一轮自动验收曾真实得到:

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

而审查器要求:

Index Only Scan
Heap Fetches: 0

正确修复不是放宽断言,而是在 COMMIT 后显式:

VACUUM (ANALYZE) shop_ch17.sales_fact;

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

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

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

本地事实

fixture-facts.sql

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

远端:

数据库租户账户销售unitsamountchecksum
pg36_shard_a2,4,6,8200120,000599,9881,188,000.00274002…24e3
pg36_shard_b1,3,5,7200120,000600,0121,068,000.000bb770…2629

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

并行边界

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 并注入。

冻结关键路径:

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

证明:

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

不证明:

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

work_mem 边界

低内存:

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

高内存:

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

两个文件:

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

汇总边界

raw-aggregate-plan.sql

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

summary-aggregate-plan.sql

Seq Scan on daily_tenant_summary
actual rows=2880 loops=1

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

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

更准确的结论:

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

BRIN 只记录候选

catalog 验证:

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 比较单机加速与一个分布式候选

四条相同结果路径

自动导出:

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

分别来自:

任务逐个:

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

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

单租户裁剪

tenant-pruned-plan.sql

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

关键计划:

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

审查器明确要求:

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 直接查询 分区父表:

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 显式分别查询两个 foreign partition:

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;

外层再按月合并。计划:

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

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。实测:

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 反例替代,也不能假定一定成功。

应用身份

成功读:

sale_count=7500
amount_total=69375.00

写入负例:

exit=3
SQLSTATE 42501

catalog:

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:

healthy_shard_tenant_2=30000

再执行全局 count,固定:

exit=3
SQLSTATE 08001

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

错误断开使事务回滚后,重新导出:

server-catalog-after-failure.csv

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

一次完整 evaluate

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

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。

仅验证现有数据库:

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

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

review 的职责

review.py 不连数据库,只审查 evidence:

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 证据、生产代价和撤退路线

ADR 不以产品名开头

analytics-distributed-adr.md 先写背景和决策顺序:

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 Btenant_id 可局部
朴素全局?240k foreign rows协调端/网络风险
两阶段?960 aggregate rows计算靠近数据
同分片 JOIN?coordinator Hash Join必须实测下推
shard 失败?scoped read 成功/global 08001定义 partial semantics

决策与限制同版本

baseline-v1.5-proposal.json 固定:

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:

3dcb7308cf6983122ee860ad3dc2a4b44651549e3d5631770839bb9a0be450c6

改变 ADR contract 会改变 release candidate identity。

进入生产 PoC 前的代价

ADR 要预算:

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 保留两个候选:

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 撤退:

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

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

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 需要:

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

错误 action token:

SQLSTATE P3660
reset refused: invalid ch17 action token

错误 target:

SQLSTATE P3661
reset refused: invalid ch17 target token

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

SQLSTATE P3663
reset refused: ch17 workers are active

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

精确 reset

协调端顺序:

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。输出:

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:

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

输出:

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

跨库非原子边界

任务先对三个数据库全部 preflight verify,再按:

coordinator -> shard A -> shard B

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

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

手工 reset

只有明确需要退出 fixture 时:

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

完整双周期

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

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

成功输出:

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

目录:

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

checklist:evidence

验收项evidence通过条件
manifestmanifest.txtPG18、FDW 1.2、三库、checksums
sourcemanifest + fixture JSONgenerator/golden SHA 精确
golden四份 monthly CSV与 frozen byte-identical
local-factsfixture-facts.csv240k、1.2m、2.256m
remote-factsremote-*-state.csv各 120k + shard checksum
parallellocal-parallel-plan.txtplanned/launched=2、partial/final
covering-indexselective-index-plan.txt7,500、Heap Fetches 0
spilllow/high plansexternal merge vs quicksort
summaryraw/summary plans240k vs 2,880 input shape
pruningtenant-pruned-plan.txtonly dist_1 + remote filters
naivenaive plan240k foreign rows
two-stagetwo-stage plan480+480 remote aggregate rows
join-counterexamplecollocated plancoordinator Hash Join
authmapping/security catalogssix named mappings, read-only app
app-failureapp-write.*exit 3, SQLSTATE 42501
shard-failureshard-failure.*healthy=30k, global 08001
catalog-rollbacktwo server catalogsbyte-identical
database-verifythree verify outputscoordinator + A + B status ok
final-statefinal-state.csvrelease/rows/checksums exact
reviewreview.txtstatus=review-ok
reset-guardsroot failure filesP3660/P3661/P3663
exact-resetreset outputsschemas/servers zero, DB shells retained
rebuildcycle-2same complete review

最终状态

final-state.sql 固定:

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_rowstwo_stage_transfer_rows 是与计划合同共同审查的固定 事实;如果 SQL 或版本改变,不能只保留硬编码值,必须重新采集 plan 并更新 proposal。

review 输出

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 之后仍需:

[ ] 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 · 返回本章目录 · 下一章:万法归宗:PostgreSQL 数据平台与替代边界 · 查看全书目录 · 查看索引中心

最后更新于