跳至内容
7.7 实战:解释订单查询的计划变化

7.7 实战:解释订单查询的计划变化

综合实验不追求把某个 node 调成最快,而是建立一条可审计推理链:确定性数据制造已知分布,保存修复前 JSON 计划,只改变一个统计/条件因素,再保存修复后计划并由程序检查关系。

7.7.1 用固定数据种子制造估算偏差

确认 service 指向可写 pg36_shop L1:

export PGSERVICEFILE=/absolute/private/path/pg_service.conf
export PGSERVICE=pg36-admin
cd static/labs/ch07
export PG36_EVIDENCE_DIR="$PWD/evidence/ch07/all-$(date -u +%Y%m%dT%H%M%SZ)"

./task.sh all

all 的顺序:

ch04-v1 verify-before
  → marker collision guard + deterministic setup
  → correlated plans before
  → CREATE STATISTICS + ANALYZE
  → correlated plans after
  → custom/generic parameter plans
  → constant/wrapped/generic partition plans
  → parent ANALYZE
  → Python semantic assertions
  → fixture verify + ch04-v1 verify-after

fixture 分布固定:

rows=100000
east+paid=25000
east+cancelled=0
tenant 1=90000
tenant 1001=10

region/status 分别四等分但完全对应。普通单列统计近似把两个 25% predicate 相乘,所以两种组合都估约 6250。具体值随 ANALYZE sample 波动,不能 hard-code;raw stats-before-*.json 保存本次 estimate、actual、buffers、settings 和时间。

参数实验选择 128-byte payload,确保 hot tenant 的 heap fetch 与 cold tenant 有明显路径取舍。analyzer 不强制节点必须是 Seq/Index,只强制 custom estimate 接近实际、generic 对两个参数使用同一 estimate,且 hot generic 偏差至少 100 倍。

7.7.2 修复统计与条件表达后重新比较

扩展统计只改变 planner knowledge,不改变数据、index 或 SQL:

present:    ~6300 → 25000 / actual 25000
impossible: ~6300 → 1     / actual 0

这证明问题属于跨列相关,不需要先建组合 index;若查询性能仍不达标,再用第 9 章方法评估 index。扩展统计改善 estimate 也可能不改变 node,因为本查询没有 region/status index 且需要 seq scan;“plan shape 没变”不代表修复无效。

分区实验只改变 predicate:

occurred_on = constant          → one child at plan time
date_trunc(... occurred_on ...) → four children, child filters
generic $1 equality             → one executed child, 3 removed

修复 wrapped 条件应在应用边界先算月初/月末,再直接写 partition-key 半开范围。创建 expression index 可能帮助分区内 filter,却不保证 partition bounds 能由该表达式裁剪;index 与 pruning 是两种机制。

父表统计单独验证 0→4。若只看 child autoanalyze,会遗漏 parent。生产维护频率由分布变化而非固定日历决定,并观察 pg_stat_progress_analyze、锁与资源。

查看结果:

cat "$PG36_EVIDENCE_DIR/plan-summary-all.txt"
python3 -m json.tool "$PG36_EVIDENCE_DIR/plan-summary-all.json"

一次实测:

status=ok
correlated_estimate=6352->25000/actual=25000
impossible_estimate=6275->1/actual=0
custom_hot=Seq Scan/estimate=90000/actual=90000
custom_cold=Index Scan/estimate=10/actual=10
generic_estimate=100/hot_actual=90000/cold_actual=10
partition_counts=constant:1,wrapped:4,generic:1
partition_parent_stats=0->4

时间与 buffers 用来比较同一次受控实验,不进入稳定 summary。要比较性能,应重复运行并记录 cache/并发条件;本章只验 cardinality 与裁剪机制。

安全复位

实验对象保留供观察。确认不再需要后:

export PG36_RESET_TOKEN=RESET_CH07_PLAN_LAB
export PG36_RESET_TARGET=pg36_shop/shop_private/ch07
export PG36_EVIDENCE_DIR="$PWD/evidence/ch07/reset-$(date -u +%Y%m%dT%H%M%SZ)"
./task.sh reset

reset 先复核 database、primary、ch04-v1、两个 relation marker 与两个 token,再只删除 ch07 relation。错误 token 必须 exit 3,且对象仍存在;成功后 remaining_ch07_relations=0,ch04-v1 relation checksum 仍为:

f8a7bfae59c6d16cd323abecfefe1014

7.7.3 把一条有证据的计划规则追加到规约

v0.1 的 PREF-PLAN-005 已声明:

看到 Seq Scan、Nested Loop 或高 cost 不直接判错;
先定位 estimate/actual、loops、buffers、wait 与 workload,
再用可回退对照验证 statistics、SQL 或 index 变更。

baseline-v0.2-proposal.json 不改写不可变 v0.1,而是基于其 canonical checksum 提案:

base=0.1.0
candidate=0.2.0
rule=PREF-PLAN-005
statement_change=none
change=evidence-and-runtime-check

新增 runtime check:

保存机器可读 plan,比较 estimate/actual、参数计划与裁剪范围;
不得以节点名或 cost 单独判定回归。

Python analyzer 会重新计算 v0.1 canonical checksum,拒绝 proposal 绑定到被悄悄修改的 base;也验证三项 evidence artifact 存在。当前 proposal 仍是 candidate,因为 promotion 还要求:

  1. PostgreSQL 14–18 兼容矩阵通过;
  2. 至少一个不同硬件或规模复测;
  3. 合并为独立、不可变的 v0.2 release artifact。

这体现规则证据的正确节奏:单次 PostgreSQL 18.4 实验足以形成候选 runtime check,不足以宣称跨版本普遍阈值。后续复测若 node type 不同但 cardinality/cropping 关系相同,规则被确认;若机制变化,就修 scope/compatibility,而不是让测试强行 grep 旧节点。

完成本章后,读者应提交的不是一张漂亮计划图,而是:

manifest + source hashes
raw plans before/after
machine summary
fixture distribution
PostgreSQL/session facts
business model verify-before/after
rule proposal + known promotion gap

下一章会从 Pigsty 的真实慢查询时间窗选择 query family,再沿本章方法进入单条计划。


上一节:建立计划证据基线 · 返回本章目录 · 下一章:抽丝剥茧:慢 SQL 诊断方法论 · 查看全书目录 · 查看索引中心

最后更新于