A 4B model outplans Postgres
A 4B open-weights model, post-trained with SFT and RL, cut Postgres query latency 44.7% across 113 join-heavy benchmark queries using pg_hint_plan.
A 4B-parameter open-weights model, post-trained with supervised fine-tuning and reinforcement learning, cut query latency 44.7% across the 113 join-heavy queries of the Join Order Benchmark, measured against Postgres’s own default plans. Before training, the same model could not produce a valid plan for 99 of those 113 queries.
The result rests on an asymmetry. Picking a good join order is NP-hard, but checking whether a plan is good is trivial: run it and time it. Execution time is a single scalar, which makes faster-plan production a clean reinforcement target. You reinforce whatever leads to lower latency.
Why Postgres leaves time on the table
Leis et al. benchmarked query optimizers in 2015, asked how good they really are, and asked again ten years later. Both times the answer was that optimizers leave a lot to be desired, and the reason is cardinality estimation.
Take a three-table join. Counting the valid join orders, the three join algorithms (hash, merge, nested loop), the outer/inner orientations, and four scan types (sequential, index, index-only, bitmap), there are 4,608 ways to run the query, and the author notes that is an undercount. Postgres does not evaluate all of them. It prunes with dynamic programming and switches to a genetic algorithm once a query touches 12 or more joins.
The weak point is that Postgres cannot count the rows each join will produce without running the joins, which would defeat the purpose of a fast planner. So it estimates. The planner reads pg_statistic for each column (most-common values and their frequencies, a histogram for the rest) and, for joins, assumes the value distribution in one table carries over uniformly to the other.
That uniformity assumption fails hard on correlated data. In the author’s IMDb example, if 5% of companies are Japanese the planner expects a join to shrink accordingly, but if those 5% of companies produced 50% of the movies, the real intermediate result is an order of magnitude larger than the estimate. One bad estimate early in a join tree corrupts every estimate after it, and Postgres picks whatever plan its cost model rates cheapest.
The formulation that makes a model worth running
Training a model to replace Postgres’s cardinality estimator would mean fighting decades of research, and the inference latency alone would dwarf Postgres’s planner on a one-off query. The author discards that idea.
The workload that pays off is repeated analytic queries. If the same query shape runs thousands of times on a default plan, every run is leaving efficiency unclaimed. Spend tens to hundreds of executions up front to learn a better plan for that specific query, and the amortized cost across all later runs drops. On a one-off query Postgres’s fast planner wins, and the model does not try to compete there; the payoff is the query that runs over and over.
Postgres’s cost model cannot be changed without patching its source, so the model steers it from outside with pg_hint_plan. Hints go in as comments above the SQL and pin a join order, a join method, or a scan type. The model’s job is to emit good hints.
The harness
The base model is empero-ai/Qwen3.8-4B-Distill, a 4B distillation of Qwen 3.8’s 2.4T teacher into Qwen 3.5 4B. It scores better than its base on MMLU and slightly worse on GSM8K, trading some multi-step math for breadth.
Around it sits qo-agent, with six tools: inspect_relation (columns, types, indexes, estimated rows and bytes), get_column_stats (planner statistics for up to eight columns), get_plan (default or candidate plan estimates), evaluate_candidate (validate a proposed plan, run it, return timing and diagnostics), keep_default (return Postgres’s own plan and stop), and finish. The model emits PlanAction JSON objects, and evaluate_candidate compiles them into hints and prepends them to the query. Training combined off-policy distillation from roughly 500 GPT-6 Astra agent trajectories with a custom GRPO variant built to score rollouts in a noisy environment, with vLLM and the trainer on a rented dual-H100 node and four Postgres containers running locally.
Training used the Cardinality Estimation Benchmark (about 13,600 queries across 16 templates), and validation used JOB (113 queries, 33 templates). Both sit on the same IMDb database, which is intentional, since the deployment target is one company’s own analytic workload and learning that one database deeply is the goal. To keep JOB out of training, each query was reduced to its join-graph topology (tables as nodes, joins as edges, selectivity predicates dropped) and CEB was checked for overlap. There was none, so nothing was pruned.
Measuring Postgres without lying to yourself
The thesis depends on deciding whether one plan is faster than another, and Postgres execution times are noisy. Run a query 20 times and the numbers spread. Two caches sit in the path: Postgres’s shared_buffers and the Linux page cache beneath it.
The rig ran four Docker containers, each pinned to 4 cores and capped at 8 GB, each loading an identical 8.5 GB slice of IMDb. Before timing, it warmed each query and watched two counters from EXPLAIN (BUFFERS): shared hit blocks (pages found in shared_buffers) and shared read blocks (pages it had to ask Linux for). When both stayed within 2% of the previous run and the plan had not changed, the query counted as warm.
With shared_buffers set to a conservative 128 MB against an 8.5 GB database, the read-block count never fell to zero. Postgres missed its own cache on every run and leaned on the Linux page cache instead. Stable counters meant the query had settled into a steady miss rate, not that the data was resident in Postgres. That was fast enough from the page cache, but not what the counters appeared to promise.
The residual noise was not even uniform. For job-13b, 14 of 20 runs landed between 186 and 204 ms while the other 6 landed between 227 and 253 ms, 14% to 26% slower, a bimodal split rather than a clean distribution. An average over those runs would hide the structure that matters when you are trying to tell two plans apart.
Keep Reading
data breachA February 2025 migration exposed DigiCert customer records online
A February 2025 server migration exposed a DigiCert internal system, and a self-registered account read archived customer data and identity documents.
OpenSSHLong Passwords Bypass Alpine SSH Authentication
OpenSSH 10.6p2 fixes a critical password authentication bypass affecting Alpine Linux and other potentially vulnerable musl systems.
opensshLong passwords unlock disabled accounts
The "*sshole" flaw lets oversized passwords log into *-disabled accounts on musl-based OpenSSH servers like Alpine under certain configurations. Fixed in 10.6p2.
Latest on the Wire
Full wire →- Bending Spoons Destroys Harvest With 10x Price Hike and LLM SupportHacker News
- CISA Flags SonicWall MFA Bypass Amid Exploitation ReportsThe Hacker News
- Claude Opus Ports 3D Models from Three.js to Vanilla WebGLSimon Willison
- Collapsing AI PR descriptions with HTML details tagsSimon Willison
New signal daily · RSS
Stay in the loop
New writing delivered when it's ready. No schedule, no spam.