Contents
pg_sorted_heap operations runbook
This file captures the day-to-day diagnostics workflow for extension stability and performance checks.
Entry point
make help
Use this as the canonical list of project-specific operational targets and default parameters.
Policy contract lint (fast static preflight for comparator mode/polarity safety):
make policy-lint
Contract source:
- scripts/comparator_policy_contract.json
- current schema_version: 1
- requirement: python3 available in execution environment.
- optional env: POLICY_LINT_DUPLICATE_TARGET_MODE=error|warn (default error).
- optional env: POLICY_LINT_WARNINGS_MAX=<non-negative-int> (unset/empty disables warning-threshold gate).
Policy/doc/workflow safety aggregate preflight:
make policy-safety-selftest \
UNNEST_AB_SELFTEST_TMP_ROOT=/private/tmp \
UNNEST_GATE_SELFTEST_TMP_ROOT=/private/tmp
Related CI lane in repository checkouts:
.github/workflows/policy-safety-selftest.yml. Source archives intentionally
omit .github, but the local target above remains available.
Command selection quick map
- Use
make pg-core-regression-smokewhen you need a fast capability check for upstream-style entrypoints (installcheck, TAP/prove,pg_isolation_regress) without spinning up heavy probes. - Use
make policy-safety-selftest UNNEST_AB_SELFTEST_TMP_ROOT=/private/tmp UNNEST_GATE_SELFTEST_TMP_ROOT=/private/tmpbefore release-oriented edits that touch policy/docs/workflow safety boundaries. - Use
make selftest-lightweight LIGHTWEIGHT_SELFTEST_TMP_ROOT=/private/tmpfor canonical cross-surface regression validation across runtime, startup, perf, docs, and workflow contracts.
Quick checks
Single-backend stability smoke:
make fastpath-stress STRESS_CYCLES=10 STRESS_PORT=65462
Larger soak:
make fastpath-stress STRESS_CYCLES=50 STRESS_PORT=65462
Mixed-cardinality perf probe:
make fastpath-perf-probe \
PERF_ROWS=1024 PERF_ITERS=2000 \
PERF_CHURN_ROWS=5000 PERF_CHURN_ITERS=2000 \
PERF_PORT=65463
Median-of-N perf probe (reduces single-run jitter):
make fastpath-perf-probe-median \
PERF_MEDIAN_RUNS=5 \
PERF_ROWS=1024 PERF_ITERS=2000 \
PERF_CHURN_ROWS=5000 PERF_CHURN_ITERS=2000 \
PERF_PORT=65480 PERF_OUT_DIR=/private/tmp
Production perf sentinel (gating, steady-state default):
make unnest-ab-perf-sentinel \
UNNEST_SENTINEL_WARMUP_SELECTS=1 \
UNNEST_SENTINEL_MIN_INSERT_RATIO=0.90 \
UNNEST_SENTINEL_MIN_JOIN_UNNEST_RATIO=1.10 \
UNNEST_SENTINEL_MIN_ANY_ARRAY_RATIO=1.00
Cold-start observability lane (non-gating telemetry only):
make unnest-ab-perf-sentinel-cold-observe
In observability mode the sentinel emits status=observe and exits zero unless the probe itself fails.
Cold-vs-warm startup sensitivity probe (non-gating, emits warm-over-cold multipliers):
make unnest-ab-startup-sensitivity \
UNNEST_STARTUP_RUNS=1 \
UNNEST_STARTUP_BATCH_SIZE=400 \
UNNEST_STARTUP_BATCHES=20 \
UNNEST_STARTUP_SELECT_ITERS=60 \
UNNEST_STARTUP_PROBE_SIZE=64
Output marker: unnest_ab_startup_sensitivity status=ok|...|*_warm_over_cold=<ratio>.
Startup sensitivity guard (fails when cold-start penalty exceeds configured envelope):
make unnest-ab-startup-sensitivity-guard \
UNNEST_STARTUP_GUARD_MAX_INSERT_WARM_OVER_COLD=2.00 \
UNNEST_STARTUP_GUARD_MAX_JOIN_UNNEST_WARM_OVER_COLD=2.00 \
UNNEST_STARTUP_GUARD_MAX_ANY_ARRAY_WARM_OVER_COLD=1.80
Guard output marker:
- pass: unnest_ab_startup_sensitivity_guard status=ok|...
- fail: unnest_ab_startup_sensitivity_guard status=regression|...
PostgreSQL core-alignment smoke check (non-invasive, capability visibility):
make pg-core-regression-smoke
Output marker: pg_core_regression_smoke status=ok|installcheck_target=...|tap_prove=...|isolation_regress=....
Use it to verify whether local environment is ready for upstream-style extension checks (make installcheck, TAP/prove, pg_isolation_regress).
Compare two perf probe logs against slowdown threshold:
make fastpath-perf-compare \
PERF_REF=/private/tmp/perf_ref.log \
PERF_NEW=/private/tmp/perf_new.log \
PERF_MAX_SLOWDOWN=1.25
Compare two perf probe log sets (median-based):
make fastpath-perf-compare-median \
PERF_REF_DIR=/private/tmp/perf_ref_set \
PERF_NEW_DIR=/private/tmp/perf_new_set \
PERF_MAX_SLOWDOWN=1.25 \
PERF_SET_STAT_MODE=median \
PERF_SET_MIN_SAMPLES=1
Default PERF_SET_MIN_SAMPLES is mode-aware:
- median defaults to 1.
- p95 and trimmed-mean default to 3 (and reject lower values).
- comparator header includes metric_polarity=lower_is_better.
Comparator self-test (no PostgreSQL startup; validates parser/gating invariants):
make fastpath-perf-compare-selftest PERF_SELFTEST_TMP_ROOT=/private/tmp
Runtime perf-probe self-test (starts temporary PostgreSQL; validates churn eviction path and leak/panic guards):
make fastpath-perf-probe-selftest \
PERF_RUNTIME_SELFTEST_TMP_ROOT=/private/tmp \
PERF_RUNTIME_SELFTEST_PORT=65473 \
PERF_RUNTIME_SELFTEST_ROWS=128 \
PERF_RUNTIME_SELFTEST_ITERS=50 \
PERF_RUNTIME_SELFTEST_CHURN_ROWS=5000 \
PERF_RUNTIME_SELFTEST_CHURN_ITERS=50
When overriding runtime selftest parameters, keep PERF_RUNTIME_SELFTEST_CHURN_ROWS high enough to exercise eviction-path checks (local_hint_evictions > 0).
Optional high-churn profile target (useful for deeper nightly/local stress):
make fastpath-perf-probe-selftest-high-churn \
PERF_RUNTIME_SELFTEST_TMP_ROOT=/private/tmp \
PERF_RUNTIME_SELFTEST_HIGH_PORT=65474 \
PERF_RUNTIME_SELFTEST_HIGH_ROWS=256 \
PERF_RUNTIME_SELFTEST_HIGH_ITERS=100 \
PERF_RUNTIME_SELFTEST_HIGH_CHURN_ROWS=12000 \
PERF_RUNTIME_SELFTEST_HIGH_CHURN_ITERS=100
Print current runtime profile presets (machine-readable):
make fastpath-perf-probe-selftest-profiles
JSON variant:
make fastpath-perf-probe-selftest-profiles RUNTIME_PROFILE_FORMAT=json
JSON output includes schema_version for forward-compatible parser contracts (RUNTIME_PROFILE_SCHEMA_VERSION, positive integer).
When evolving JSON fields, keep additive changes on the same schema version and bump schema version for removals/renames/type changes.
Planner ratio-check self-test (no PostgreSQL startup; validates pass/fail parser semantics):
make planner-cost-check-selftest PLANNER_SELFTEST_TMP_ROOT=/private/tmp
Planner cost probe (quantifies plan/cost deltas for fastpath=off/on across row counts):
make planner-cost-probe \
PLANNER_PROBE_ROWS=1000,10000,50000 \
PLANNER_PROBE_PORT=65496 \
PLANNER_PROBE_OUT=auto:/private/tmp
Validate planner probe forced-index ratio against a minimum threshold:
make planner-cost-check \
PLANNER_PROBE_LOG=/private/tmp/pg_sorted_heap_planner_probe_YYYYMMDD_HHMMSS_PID.log \
PLANNER_MIN_OFF_OVER_ON=100.0
Validate that default point-query path uses an index for larger row sets:
make planner-cost-default-path-check \
PLANNER_PROBE_LOG=/private/tmp/pg_sorted_heap_planner_probe_YYYYMMDD_HHMMSS_PID.log \
PLANNER_MIN_DEFAULT_INDEX_ROWS=10000
Default-path checker self-test (no PostgreSQL startup):
make planner-cost-default-path-check-selftest PLANNER_SELFTEST_TMP_ROOT=/private/tmp
Generate machine-readable planner probe summary (for CI artifacts/trending):
make planner-cost-summary \
PLANNER_PROBE_LOG=/private/tmp/pg_sorted_heap_planner_probe_YYYYMMDD_HHMMSS_PID.log \
PLANNER_SUMMARY_FORMAT=json \
PLANNER_SUMMARY_OUT=/private/tmp/pg_sorted_heap_planner_probe_summary.json
One-command probe + summary artifact pipeline:
make planner-cost-probe-summary \
PLANNER_PROBE_ROWS=1000,10000,50000 \
PLANNER_PROBE_PORT=65496 \
PLANNER_PROBE_OUT=auto:/private/tmp \
PLANNER_SUMMARY_FORMAT=json \
PLANNER_SUMMARY_OUT=auto:/private/tmp
One-command strict gate (probe + summary + ratio check + default-path check):
make planner-cost-gate \
PLANNER_PROBE_ROWS=1000,10000,50000 \
PLANNER_PROBE_PORT=65496 \
PLANNER_PROBE_OUT=auto:/private/tmp \
PLANNER_SUMMARY_FORMAT=json \
PLANNER_SUMMARY_OUT=auto:/private/tmp \
PLANNER_MIN_OFF_OVER_ON=100.0 \
PLANNER_MIN_DEFAULT_INDEX_ROWS=10000
Planner gate self-test (mocked chain; no PostgreSQL startup):
make planner-cost-gate-selftest PLANNER_SELFTEST_TMP_ROOT=/private/tmp
Compare two planner probe log sets (noise-resistant aggregate gate):
make planner-cost-compare-median \
PLANNER_REF_DIR=/private/tmp/planner_ref_set \
PLANNER_NEW_DIR=/private/tmp/planner_new_set \
PLANNER_MIN_FRACTION=0.90 \
PLANNER_SET_STAT_MODE=median \
PLANNER_SET_MIN_SAMPLES=1
PLANNER_SET_STAT_MODE supports median, p05, p95, and trimmed-mean.
Default PLANNER_SET_MIN_SAMPLES is mode-aware (1 for median, 3 for non-median modes).
For off_over_on ratios (higher is better), prefer median, p05, or trimmed-mean; p95 is optimistic.
Planner set comparator header includes metric_polarity=higher_is_better.
Planner set-comparator self-test:
make planner-cost-compare-selftest PLANNER_SELFTEST_TMP_ROOT=/private/tmp
Planner summary self-test:
make planner-cost-summary-selftest PLANNER_SELFTEST_TMP_ROOT=/private/tmp
Probe output lines use normalized key/value format:
- planner_probe|rows=<n>|fastpath=<off|on>|query=<point_default|range_default|point_forced_index>|plan=<node>|startup_cost=<x>|total_cost=<y>|plan_rows=<n>|plan_width=<n>
- planner_probe_compare|rows=<n>|forced_point_off_total=<x>|forced_point_on_total=<y>|off_over_on=<ratio>
- planner_probe_summary|forced_index_hits=<n>|forced_index_cases=<n>
- optional warning marker when no index path is seen: planner_probe_warning|no_index_path_detected=1
- final success marker: planner_probe_status=ok.
Interpretation hint: very small tables can still prefer Seq Scan in default mode even with fastpath=on; use larger cardinalities to evaluate planner crossover behavior.
Perf output capture modes
Write probe output to explicit file:
make fastpath-perf-probe PERF_OUT=/private/tmp/my_probe.log
Auto-generate filename in /private/tmp:
make fastpath-perf-probe PERF_OUT=auto
Auto-generate filename in custom absolute directory:
make fastpath-perf-probe PERF_OUT=auto:/private/tmp
auto:<dir> requires an absolute directory path; relative paths are rejected by design.
UNNEST A/B probe knobs
make unnest-ab-probe accepts additional runtime knobs:
UNNEST_AB_LOCAL_HINT_MAX_KEYS=<n>maps topg_sorted_heap.pkidx_local_hint_max_keys(session-local hint cache key capacity).UNNEST_AB_ASSUME_UNIQUE_KEYS=<on|off>maps topg_sorted_heap.pkidx_assume_unique_keys.UNNEST_AB_WARMUP_SELECTS=<n>performs unmeasured select warmup iterations before timedJOIN UNNEST/ANY(...)loops (1by default; set0for cold-start latency measurement).UNNEST_AB_ALLOW_UNSAFE_UNIQUE=<on|off>is a fail-closed override required whenUNNEST_AB_ASSUME_UNIQUE_KEYS=on.- Safe baseline for probe-heavy workloads: start with
UNNEST_AB_LOCAL_HINT_MAX_KEYS=16384before enabling any unsafe assumptions.
Production drift sentinel (compact check):
make unnest-ab-perf-sentinel \
UNNEST_SENTINEL_MIN_INSERT_RATIO=0.90 \
UNNEST_SENTINEL_MIN_JOIN_UNNEST_RATIO=1.10 \
UNNEST_SENTINEL_MIN_ANY_ARRAY_RATIO=1.00
Notes:
- Sentinel always runs assume_unique_keys=off.
- Default UNNEST_SENTINEL_WARMUP_SELECTS=1 evaluates warm steady-state throughput.
- Set UNNEST_SENTINEL_WARMUP_SELECTS=0 when you explicitly want cold-start latency sensitivity.
Safety note:
- pg_sorted_heap.pkidx_assume_unique_keys is superuser-only and defaults to off.
- Turn it on only for workloads with guaranteed unique probe keys; otherwise correctness can degrade on duplicate-key data.
- Even with unique keys, high-scale throughput can degrade; keep this mode off for production baselines unless workload-specific evidence proves benefit.
- Direct run_unnest_ab_probe* execution with assume_unique_keys=on requires explicit override: UNNEST_AB_ALLOW_UNSAFE_UNIQUE=on.
UNNEST A/B logset comparator
For noise-resistant performance gating across multiple probe runs:
make unnest-ab-compare-median \
UNNEST_AB_REF_DIR=/abs/ref_logs \
UNNEST_AB_NEW_DIR=/abs/new_logs \
UNNEST_AB_MIN_FRACTION=0.90 \
UNNEST_AB_SET_STAT_MODE=median
Notes:
- log files must match pg_sorted_heap_unnest_ab_*.log and contain ratio_kv|insert=...|join_unnest=...|any_array=....
- comparator supports UNNEST_AB_SET_STAT_MODE=median|p05|p95|trimmed-mean.
- default UNNEST_AB_SET_MIN_SAMPLES is 1 for median and 3 for p05/p95/trimmed-mean.
- comparator header includes metric_polarity=higher_is_better for policy automation.
UNNEST A/B one-command gate
To auto-produce reference and candidate logsets and run comparator in one command:
make unnest-ab-gate \
UNNEST_GATE_REF_RUNS=3 \
UNNEST_GATE_NEW_RUNS=3 \
UNNEST_GATE_BATCH_SIZE=400 \
UNNEST_GATE_BATCHES=20 \
UNNEST_GATE_SELECT_ITERS=120 \
UNNEST_GATE_PROBE_SIZE=64 \
UNNEST_GATE_BASE_PORT=65488 \
UNNEST_GATE_OUT_ROOT=/private/tmp \
UNNEST_GATE_KEYCACHE_TRIGGER=1 \
UNNEST_GATE_KEYCACHE_MIN_DISTINCT=2 \
UNNEST_GATE_KEYCACHE_MAX_TIDS=262144 \
UNNEST_GATE_SEGMENT_PREFETCH_SPAN=128 \
UNNEST_GATE_LOCAL_HINT_MAX_KEYS=16384 \
UNNEST_GATE_ASSUME_UNIQUE_KEYS=off \
UNNEST_GATE_MIN_FRACTION=0.93 \
UNNEST_GATE_STAT_MODE=median
For throughput-ratio gating:
- UNNEST_GATE_STAT_MODE=median, p05, or trimmed-mean is recommended.
- p95 is intentionally blocked by default in gate mode because it is optimistic for “higher-is-better” metrics.
- override only when explicitly needed: UNNEST_GATE_ALLOW_OPTIMISTIC_TAIL=on.
- UNNEST_GATE_ASSUME_UNIQUE_KEYS=on is fail-closed by default in gate mode; explicit override is required:
- UNNEST_GATE_ALLOW_UNSAFE_UNIQUE=on
Output includes:
- comparator verdict (unnest_ab_set_compare status=ok|regression)
- produced directories (unnest_ab_probe_gate_output|reference_dir=...|candidate_dir=...)
- reference retention marker (reference_retained=0|1)
Reuse existing reference set (skip ref generation):
make unnest-ab-gate \
UNNEST_GATE_REF_RUNS=0 \
UNNEST_GATE_NEW_RUNS=3 \
UNNEST_GATE_EXISTING_REF_DIR=/abs/ref_logs \
UNNEST_GATE_OUT_ROOT=/private/tmp \
UNNEST_GATE_MIN_FRACTION=0.93 \
UNNEST_GATE_STAT_MODE=median
When UNNEST_GATE_EXISTING_REF_DIR is set:
- UNNEST_GATE_REF_RUNS may be 0.
To reduce /private/tmp growth when reference set is generated on the fly:
make unnest-ab-gate \
UNNEST_GATE_REF_RUNS=3 \
UNNEST_GATE_NEW_RUNS=3 \
UNNEST_GATE_OUT_ROOT=/private/tmp \
UNNEST_GATE_KEEP_NEW_DIR=on
When UNNEST_GATE_KEEP_NEW_DIR=on and reference source is generated:
- gate removes generated reference directory after comparison (reference_retained=0 in output).
- gate output includes reference_source=existing.
- calibrated safe default for gate mode is UNNEST_GATE_MIN_FRACTION=0.93 (median, 5-run set on current benchmark profile).
Interpreting observability in probe output
Expected direction in churn scenario (relative to baseline):
observabilityshould includemodes={segment_fastpath=on,...}in runtime probe lanes.segment_lookupsshould be greater than0(confirms fastpath lookup path is exercised).local_hint_touchesshould be greater than0(confirms local-hint write path is exercised).local_hint_evictionscan become non-zero after key-cap saturation.local_hint_map_resetsshould typically remain0during normal churn probes.local_hint_mergesis diagnostic (merge-shape dependent) and should not be used as a strict pass/fail invariant.defensive_state_recoversshould stay0in normal runs; non-zero indicates defensive self-recovery from inconsistent in-memory merge state.elapsed_msis typically higher than baseline due to higher cardinality and eviction churn.segment_lookup_errorsshould remain0.fastpath-perf-compareshould reportstatus=okunless either scenario slowdown exceedsPERF_MAX_SLOWDOWN.fastpath-perf-probe-medianprints per-run values and median/mean summaries for baseline and churn scenarios.fastpath-perf-compare-mediancompares aggregated stats across log sets and reportsstatus=ok|regression.PERF_SET_STAT_MODEsupportsmedian,p95, andtrimmed-mean.PERF_SET_MIN_SAMPLEScan override required set size; for non-median modes keep it at>=3for stable statistics.- comparator input values for
baseline_fastpath/churn_fastpathmust be strictly positive;0or negative values fail fast.
Cleanup policy
Operational scripts use ephemeral clusters under /private/tmp and cleanup on exit.
If a run is interrupted, remove stale dirs:
make tmp-clean TMP_CLEAN_ROOT=/private/tmp
Optional: clean only artifacts older than N seconds (protects very fresh run dirs):
make tmp-clean TMP_CLEAN_ROOT=/private/tmp TMP_CLEAN_MIN_AGE_S=3600
Cleaner now skips live PostgreSQL temp instances (directories with active postmaster.pid) and emits:
- skip_live <path> for active postmaster dirs.
- skip_recent age_s=<n> min_age_s=<n> <path> when age-threshold blocks deletion.
- summary line:
tmp_clean root=<dir> removed=<n> skipped_live=<n> skipped_recent=<n> min_age_s=<n> removed_kb=<n>.
Cleanup self-test:
make tmp-clean-selftest TMP_SELFTEST_ROOT=/private/tmp
Run full lightweight selftest bundle (all script-level checks, no PostgreSQL startup):
make selftest-lightweight LIGHTWEIGHT_SELFTEST_TMP_ROOT=/private/tmp
The runner emits per-script elapsed_s=<n> and final total_elapsed_s=<n> telemetry.
It also includes script-level guards for runtime TMPDIR, auto:<dir> output-target, PORT, shared define-parser behavior, runtime-selftest profile validation, and runtime-profile export format (selftest_runtime_tmpdir_validation.sh, selftest_runtime_output_target_validation.sh, selftest_runtime_port_validation.sh, selftest_extract_pg_sorted_heap_define.sh, selftest_runtime_perf_selftest_validation.sh, selftest_runtime_profile_export.sh).
For machine-readable output use:
make selftest-lightweight \
LIGHTWEIGHT_SELFTEST_TMP_ROOT=/private/tmp \
LIGHTWEIGHT_SELFTEST_FORMAT=jsonl
Optional: set LIGHTWEIGHT_SELFTEST_RUN_LABEL=<id> for a human-readable per-run label.
JSONL events include schema_version=1, stable per-run run_id, optional/custom run_label, runner_pid, monotonic event_seq, and event_ts (UTC RFC3339, second precision).
CI note: workflow perf-compare-selftest stores the JSONL stream as an artifact (clustered-pg-lightweight-selftest-<run_id>-<run_attempt>) for post-run diagnostics.
Vector tooling quick commands
Graph-sidecar tooling now has a dedicated lifecycle smoke:
make test-graph-builder TMP_SELFTEST_ROOT=/private/tmp TEST_GRAPH_PORT=65489
For the local bench_nomic synthetic ANN setup:
make build-graph-bench-nomic \
VECTOR_BENCH_DSN='host=/tmp port=65432 dbname=bench_nomic'
make bench-nomic-ann \
VECTOR_BENCH_DSN='host=/tmp port=65432 dbname=bench_nomic'
Both targets use ./scripts/find_vector_python.sh to locate a Python with
the dependency set from scripts/requirements-vector-tools.txt
(numpy + psycopg2-binary).