Skip to content

Commit a2acfd3

Browse files
committed
Optimize PostgreSQL metadata passthrough
1 parent 49ec0dc commit a2acfd3

18 files changed

Lines changed: 1395 additions & 8 deletions

benchmark/postgres/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
__pycache__/
2+
results/*.jsonl
3+
!results/release.jsonl
4+
!results/commit_stats.jsonl

benchmark/postgres/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# PostgreSQL metadata benchmarks
2+
3+
`run.py` creates deterministic metadata-only fixtures and measures metadata operations through DuckDB. Fixture setup is excluded from the measured interval. Every trial uses a fresh PostgreSQL schema so mutation benchmarks are independent.
4+
5+
The supported scales are `small` (100), `medium` (10,000), `large` (100,000), and `xlarge` (1,000,000). Depending on the operation, the scale controls snapshots, data files, or cleanup candidates.
6+
7+
The runner expects a PostgreSQL-enabled DuckLake build at `/work/ducklake`, the PostgreSQL scanner extension beside that build, and standard `PGHOST`, `PGPORT`, `PGDATABASE`, `PGUSER`, `PGPASSWORD`, and `PGSSLMODE` environment variables. For example:
8+
9+
```sh
10+
python3 benchmark/postgres/run.py \
11+
--variant feature \
12+
--scale xlarge \
13+
--operation expire_snapshots \
14+
--selectivity 0.5 \
15+
--trials 3 \
16+
--output /work/results/fast.jsonl
17+
```
18+
19+
The benchmark compares the existing PostgreSQL passthrough manager before and after the change; there is no separate adapter mode. The Kubernetes manifest is intentionally credential-free. It expects a `metadata-postgres` Secret containing the PostgreSQL environment variables.
20+
21+
Reported timings are medians of three trials from the same protected in-cluster pod. The release matrix contains pristine PostHog `posthog/v1.5.3` and the feature build, both using the existing PostgreSQL passthrough manager. Each timed trial launches a fresh DuckDB CLI, loads the PostgreSQL extension, attaches the catalog, runs the operation, and exits, so these are cold-process end-to-end measurements rather than PostgreSQL server execution time.
22+
23+
`stats_lookup_micro` is an isolated query-shape benchmark. It uses `postgres_query` so filtering happens in PostgreSQL, matching the metadata manager. It is not a full commit benchmark.
24+
25+
Each trial records actual PostgreSQL fixture counts before the timed operation in `fixture_row_counts`, their sum in `fixture_rows`, and the operation's logical output cardinality in `result_rows`. These are observed row counts, not estimates. They deliberately are not called “rows processed”: PostgreSQL may use indexes or other plans that avoid scanning every stored row.
26+
27+
`max_rss_kib` is the Python process's cumulative `RUSAGE_CHILDREN` high-water mark. It can include fixture children and earlier trials, so it is retained for diagnostics but is not used for memory-improvement claims. The curated JSONL files contain the raw measurements used by [RESULTS.md](RESULTS.md); do not summarize the whole results directory because exploratory runs may contain duplicate trial identities.

benchmark/postgres/RESULTS.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# PostgreSQL metadata benchmark results
2+
3+
These results compare pristine PostHog `posthog/v1.5.3` with the feature build. Both use the fork's existing PostgreSQL passthrough manager; the feature improves that path directly rather than introducing a parallel adapter. Measurements were collected on 2026-07-21 against PostgreSQL 18.3 from the protected `ducklake-bench-runner` pod.
4+
5+
The baseline is PostHog commit `49ec0dc880a749e059594ee699c87ba3a9f88524`. Both builds use DuckDB `14eca11bd9d4a0de2ea0f078be588a9c1c5b279c`, extension-ci-tools `795096d04b009c0d087468439ebb526a5460dfac`, and the official DuckDB v1.5.3 PostgreSQL extension. Trials alternate baseline and feature. There are 9 trials at small, 7 at medium, 5 at large, and 3 at xlarge for each variant and operation.
6+
7+
## Xlarge results
8+
9+
| Operation | Fixture metadata rows | Result rows | v1.5.3 baseline | Feature | Change | Speedup |
10+
|---|---:|---:|---:|---:|---:|---:|
11+
| Expire snapshots | 2,000,000 | 500,000 | 14.160 s | 6.586 s | -53.49% | **2.15x** |
12+
| Stats lookup microbenchmark | 1,010,000 | 100 | 1.182 s | 0.378 s | -68.00% | **3.12x** |
13+
| Aggregate table information | 1,101,000 | 1,000 | 0.799 s | 0.557 s | -30.27% | **1.43x** |
14+
| List snapshots | 2,000,000 | 1,000,000 | 4.997 s | 4.959 s | -0.77% | 1.01x |
15+
| Current snapshot | 1,000,000 | 1 | 0.363 s | 0.359 s | -0.94% | 1.01x |
16+
| Cleanup candidates | 1,000,000 | 1,000,000 | 1.805 s | 1.838 s | +1.86% | 0.98x |
17+
| Find snapshots to expire (`dry_run`) | 2,000,000 | 500,000 | 2.150 s | 2.186 s | +1.66% | 0.98x |
18+
| File discovery | 1,100,001 | 1,000,000 | 4.461 s | 4.500 s | +0.87% | 0.99x |
19+
20+
`Fixture metadata rows` is the sum of the actual relevant PostgreSQL table counts captured before timing; each JSON row also contains the per-table breakdown in `fixture_row_counts`. `Result rows` is the logical operation cardinality. These are observed counts, not planner-level “rows processed.”
21+
22+
The expiration fixture contains 1,000,000 snapshots and 1,000,000 snapshot-change rows, with 500,000 snapshots selected. The table-information fixture contains 1,000 tables, 1,000,000 data files, and 100,000 delete files. File discovery has one table plus the same file counts. The stats fixture contains 10,000 table-stat rows and 1,000,000 column-stat rows; the feature fetches the 100 rows for the written table.
23+
24+
Snapshot listing, current-snapshot lookup, cleanup-candidate listing, dry-run selection, and file discovery use the existing passthrough implementations. Their changes are within approximately 2% and are treated as noise, not claimed improvements.
25+
26+
The authoritative artifact contains 384 unique rows across eight operations, four scales, and two variants: [`results/release.jsonl`](results/release.jsonl). The 48 stats-only rows are extracted to [`results/commit_stats.jsonl`](results/commit_stats.jsonl).
27+
28+
`max_rss_kib` remains diagnostic only because Python's `RUSAGE_CHILDREN` high-water mark is cumulative.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
apiVersion: v1
2+
kind: Pod
3+
metadata:
4+
name: ducklake-bench-runner
5+
namespace: ducklake-bench
6+
annotations:
7+
karpenter.sh/do-not-disrupt: "true"
8+
labels:
9+
app.kubernetes.io/name: ducklake-bench-runner
10+
spec:
11+
restartPolicy: Never
12+
containers:
13+
- name: runner
14+
image: ubuntu:24.04
15+
command: ["sleep", "infinity"]
16+
envFrom:
17+
- secretRef:
18+
name: metadata-postgres
19+
env:
20+
- name: DEBIAN_FRONTEND
21+
value: noninteractive
22+
resources:
23+
requests:
24+
cpu: "4"
25+
memory: 16Gi
26+
ephemeral-storage: 40Gi
27+
limits:
28+
cpu: "8"
29+
memory: 32Gi
30+
ephemeral-storage: 80Gi
31+
volumeMounts:
32+
- name: work
33+
mountPath: /work
34+
volumes:
35+
- name: work
36+
emptyDir:
37+
sizeLimit: 80Gi
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

0 commit comments

Comments
 (0)