Clamped from above
Whatever the model proposes is held under the provable ceiling. No configuration required — the DataFusion path derives one by default since 1.2.
Why now
Everybody wants a language model in the optimizer. Almost nobody ships one, and the blocker was never capability — it is blast radius. A planner does not fail gracefully on a bad row count: one confidently wrong number and it builds a hash table on the wrong side and exhausts memory. The expected case is fine. The tail eats you.
samkhya clamps whatever the model says under a bound the join provably cannot exceed — derived by counting, no model involved. The model is allowed to be wrong. It is not allowed to be load-bearing.
The ceiling sees four numbers — two row counts, two distinct counts — and never the data. Try to break it →
Everybody wants a language model in the query planner. Almost nobody ships one, and the blocker was never capability — it is blast radius.
A planner does not fail gracefully on a bad row count. Ask a model how many rows a six-way join emits, get back a confidently wrong number, and the optimizer builds a hash table on the wrong side and exhausts memory. The expected case is fine. The tail eats you. That asymmetry is why "let's try an LLM here" has stayed a demo for three years.
A ceiling changes the shape of that risk. The model's
answer is clamped under a number derived by counting, so the worst thing
a hallucination can do is be ignored. Transport failure, timeout, a
malformed reply, a provider outage — every one returns
Ok(None) and the engine falls back to its own estimate. The
model is allowed to be wrong; it is not allowed to be load-bearing.
Whatever the model proposes is held under the provable ceiling. No configuration required — the DataFusion path derives one by default since 1.2.
Any transport error, timeout or parse failure returns "no opinion". A provider going down must never surface as a query failure.
Behind the llm_http cargo feature, pointed at an endpoint you control. Anthropic, OpenAI, local Ollama, or a dummy echo.
The same Corrector interface takes gradient-boosted trees, TabPFN-2.5, or an LLM. The safety contract does not change with the backend.
The transport floor is measured. The benefit is not: every live-LLM accuracy cell in the campaign is marked projected, pending API keys, and this page will not pretend otherwise.
Worse, and more usefully — samkhya's own gradient-boosted corrector made things worse on held-out queries in the one honest measurement taken so far: q-error geomean 13.46 without it, 26.41 with it, fitted on a single usable row.
That result is not an embarrassment to route around — it is the case for the ceiling, stated in numbers. A corrector that doubles your error is exactly the situation the clamp exists for, and a planner running under a proved bound survives it. samkhya's claim is not that a model will help. It is that you can find out safely.
Before running a query, a database estimates how many rows each step will produce. Those estimates decide the plan: which side to build the hash table on, what join order to take, how much memory to reserve.
The estimates are often wrong, and wrong multiplicatively — a modest error at the bottom of a six-table join becomes a thousandfold error at the top. It is the most common reason a query that should take two seconds takes four minutes.
Everyone knows a model could learn to correct this from past queries. Almost nobody ships one, for a single reason: a model that is usually right and occasionally catastrophically wrong is worse than a dumb estimator that is consistently mediocre. One hallucinated row count and the planner picks something that exhausts memory.
samkhya's answer is not a better model. It is a ceiling to put underneath one — a number derived by counting, with no model involved, that the join cannot exceed. Clamp the model to it and it can be as wrong as it likes without moving the planner past a bound that has been proved. A seatbelt, not a faster car.
samkhya started next to another project. I maintain a GPU-accelerated analytical engine, and the thing that kept hurting was never the kernels — it was the planner acting on a row count that was off by three orders of magnitude.
The obvious move is to learn the correction. The literature has said so
for a decade, and it works: feed a model the plan shape and past
observations, and it will beat the cost model's guesses on the queries it
has seen. I built that. It is the Corrector trait, and it is
still in the box.
Then I tried to imagine putting it in front of anything I cared about, and could not. The problem is not the average case, it is the shape of the distribution. A learned estimator that is right 95% of the time and catastrophically wrong 5% of the time is worse to operate than a crude one that is mediocre every time — because the crude one is predictable, and predictability is what capacity planning, memory limits and on-call rotations are built on. Nobody wants to explain that the query planner got creative at 3am.
So the bet is inverted. Rather than make the estimate better, make the failure bounded. Compute a number the join provably cannot exceed, by counting rather than learning, and clamp whatever the model says underneath it. The model keeps its upside on the queries it understands. Its downside stops at a number that has been proved.
Everything else in samkhya follows from that one decision. The sketches exist because the bound needs statistics. The Puffin sidecar exists because those statistics are worth more when they travel between engines than when they are locked in one. The corrector is a trait with swappable backends rather than a fixed model, because if the clamp is what provides the safety then the backend is genuinely an operator's choice — trees today, a foundation model tomorrow, whatever exists in three years.
The name is the Sanskrit सांख्य — enumeration, counting. A classical school whose discipline was counting reality's constituents honestly. That turned out to be a higher bar than intended.
The whole design rests on the bound being real. In July 2026 an audit found it was not — the ceiling fell below the true cardinality in 58.8% of measured trials, and had done so since the first release. Repairing it meant withdrawing two published headline numbers. The design survived because it was the right shape; the implementation had to be rebuilt to deserve it.
Given only row counts and which pairs of relations are joined, nothing below the Cartesian product is sound — put every row of every relation on one key value and the equi-join degenerates to a cross product. To beat it you need one more statistic: a bound on the degree, how many rows can share a single value of a join attribute.
Let Q be an equi-join over R₁ … Rₙ
with join graph G, and let T be any spanning
tree of a connected component of G, rooted at
r. Then
Proof. Materialise in BFS order from r. The
partial result starts at |R_r| tuples. Joining child
v to its parent on attribute a: every partial
tuple already fixes a value of a, and at most
maxdeg(R_v, a) rows of R_v carry any single
value, so the count multiplies by at most that factor. Join edges
outside T only filter, never add. ∎
It holds for bag semantics — duplicates included — which is what engines actually execute, and it is exactly tight on the foreign-key joins that dominate analytical workloads.
Any over-estimate of the true maximum degree keeps the ceiling sound. Three sources, cheapest first:
| Source | Bound on maxdeg | Note |
|---|---|---|
| Row count | rows | Always true. The ceiling degrades to the product — sound, useless, never wrong. |
| Distinct count (HLL) | rows − distinct + 1 | Spend one row per distinct value, pile the rest onto one. Exact for a key column, which is why FK joins bound tightly. |
| Count-Min sketch | largest counter | Tightest under skew, and derivable without knowing which key is hot. |
The Count-Min route is what makes the ceiling portable. For any
key k, true_freq(k) ≤ estimate(k) ≤ max counter,
because Count-Min never undercounts. That sketch already rides in
samkhya's Puffin sidecar — so a bound proved from statistics written by
one engine holds in another, with no shared catalog and no re-scan.
The distinct-count derivation subtracts, so it needs a count that
is never above the truth. An HLL point estimate is two-sided — it
exceeds the truth about half the time — and feeding it in produces a
ceiling below the truth. The API therefore ships
from_hll_floor, which counts non-zero registers and so can
only under-count. Every constructor either derives the guarantee or states
the obligation on the caller.
Not through carelessness with tests. Through tests that checked the wrong thing, and a metric that deleted its own failure signal. Two published headline numbers did not survive it.
The property suite ran 1,024 cases per property and asserted
LpJoinBound ≤ AgmBound ≤ ProductBound, plus
finiteness and monotonicity. All green. Every one of those invariants
holds perfectly well for a family of bounds that are all wrong
together. Nothing ever compared a bound to a true cardinality.
The fix is one test: generate small relation instances, brute-force the
real join, assert ceiling ≥ truth. Six properties,
2,048 cases each. It fails immediately on the old code.
The tightness campaign did materialise instances and compute ground truth. Then it reported:
let r_lp = (lp_b / truth).max(1.0);
A ratio below 1.0 is precisely the signal that a bound has fallen below
the truth. .max(1.0) turned every such case into exactly
1.0 — indistinguishable from a perfectly tight sound bound. The campaign
averaged 2,179 violations into its headline and was structurally
incapable of reporting one.
| Bound | Defect | Witness |
|---|---|---|
LpJoinBound |
The LP added one cover constraint per predicate; AGM needs one per attribute, plus full weight for any relation carrying a private column. Invisible on a triangle — the only shape the tests used. | 10 × 100 FK join gave 10, truth 100 |
AgmBound |
min × max drops every relation but two. Not an AGM bound at all. |
3 × 3 × 3 chain gave 9, truth 27 |
ChainBound |
Dividing by max(D_i, D_j) is a uniform-distribution estimate, not an upper bound. |
skewed 20 × 20 gave 80, truth 260 |
ProductBound |
— | sound throughout |
Every witness is a concrete instance whose true cardinality was counted by brute force, not argued.
Including the ones that did not survive. A project whose selling point is honest measurement does not get to quietly drop the rows that failed.
| Claim | Measured | Status | Receipt |
|---|---|---|---|
| Bound soundness — does the ceiling ever fall below the truth? | 0 / 3,704 was 2,179 (58.8%) |
Reproduced | 20_bound_soundness |
| Ceiling on a foreign-key join, 10 orders ⋈ 100 line items | 100 — exact product: 1,000 |
Reproduced | 20_bound_soundness |
| LpJoinBound vs AGM tightness, star-5 | Withdrawn | 07 (retracted) | |
| JOB-Slow end-to-end speedup | Withdrawn | OPEN_AUDIT §2 | |
| Corrector on held-out queries, fit S1–S5 evaluate S6–S10 | q-error 13.46 → 26.41 i.e. worse |
Reproduced | corrector_flow |
| HLL precision, p=14 n=10⁶ | RSE 0.676% | Measured | 03_hll_precision |
| Test surface | 345 passing, 0 failing | Reproduced | cargo test --workspace |
It was published as a cardinality-correction result. Reading the
committed run scripts: no --corrector flag is passed, so the
runner attaches nothing — and the flag's only non-none value
was identity, which returns the baseline unchanged.
The bench CLI had no option that could attach a trained corrector
at all. The two arms differed by exactly one thing: whether
portable sidecar statistics were injected.
Alongside that: all four trials exited 137 (OOM-killed) at query 15d
against a header announcing 113; arm order was fixed so the coldest run
sat in the baseline, and removing it takes 1.038× to 1.013×;
every query had two trials per arm, where the smallest attainable exact
p is 0.167, so the "24 of 55 significant" count is an artefact of
a degenerate bootstrap; and the published q-error is 1.0 everywhere
because every JOB query is SELECT MIN(...).
The finding underneath is real, and better than the headline it replaced: portable sidecar statistics, with no model anywhere, moved a real workload at all. That deserves publishing as what it is, after an honest re-run.
A sketch written in a browser deserialises unchanged in a Rust query engine. That is the portability claim, and it is the same code path in every binding.
import init, { HllSketch, joinCeiling } from 'samkhya';
await init();
const hll = new HllSketch(12);
for (const row of rows) hll.add(row.orderKey);
// 10 orders joined to 100 line items over 10 distinct keys.
joinCeiling([10, 100], [0, 1], [10, 10]); // 100 — exactly the truth
joinCeiling([10, 100], [0, 1], []); // 1000 — the product
84 KB WebAssembly, generated TypeScript definitions, no server and no native module.
use samkhya_core::degree::{AttributeDegree, JoinGraph, JoinRelation};
let orders = JoinRelation::new(10)
.with_degree(ORDER_KEY, AttributeDegree::from_distinct(10, 10));
let lineitem = JoinRelation::new(100)
.with_degree(ORDER_KEY, AttributeDegree::from_distinct(100, 10));
let graph = JoinGraph::new(vec![orders, lineitem]).with_edge(0, 1, ORDER_KEY);
assert_eq!(graph.ceiling(), 100);
The harness separates the three steps so a held-out measurement is enforceable rather than aspirational. Freezing the model between steps 2 and 3 is the point: a model written before the evaluation queries ran cannot have seen them.
# 1. Record plan features on a training subset.
samkhya-bench run --suite synthetic --feedback train.db --only S1,S2,S3,S4,S5
# 2. Fit and freeze.
samkhya-bench train --feedback train.db \
--template samkhya-bench-synthetic --out model.json
# 3. Evaluate on the complement — queries the model has never seen.
samkhya-bench run --suite synthetic --exclude S1,S2,S3,S4,S5 \
--corrector gbt --model model.json
Skip the split and you measure memorisation. On this suite that reads as q-error improving 4.58 → 1.86; the honest held-out run shows it worsening 13.46 → 26.41. Same model, same code, opposite conclusion.
Five sketches into one Iceberg Puffin sidecar, read back unchanged by Iceberg, DataFusion and DuckDB.
A provable bound from those same statistics. No model, no training, no network.
One trait, swappable backends — gradient-boosted trees, TabPFN, or an LLM behind HTTP.
Whatever the backend proposes is held under the ceiling. Cold start falls back to the engine's own estimate.
Optimizer integration needs a process you control. The portable sidecar does not — it is a file, and anything that reads Iceberg can consume it. That asymmetry is the real shape of this project.
| Target | Status | Notes |
|---|---|---|
| DataFusion 46 | Production | Pre-join physical rule inserted before join_selection; ceiling derived by default since 1.2. |
| Apache Iceberg | Production | Puffin sidecar reader and writer, snapshot-aware, strict validation. |
| Arrow | Production | IPC round-trip for all five sketches. |
| JavaScript / TypeScript | New in 1.2 | 84 KB wasm, generated types, sketches plus the ceiling. |
| DuckDB | Beta | Rust client works; the loadable extension waits on upstream. |
| Polars | Beta | Series-to-sketch helpers behind a feature flag. |
| Vector search, Qdrant-shaped | Bounds only | Provable match-count ceilings for the pre-filter / post-filter decision. Computes bounds; does not link an engine. |
| PostgreSQL | Scaffold | Reachable via get_relation_info_hook. Not built — the crate's own docs say so. |
| SQL Server · Snowflake · BigQuery · Pinecone | Not possible | No injection point for a cardinality estimator. No amount of work changes this. |
The same audit raised questions this release does not close. They are listed because leaving them unlisted would be the more comfortable choice.
Full register, with each item marked reproduced, credible-but-unverified, or open, in OPEN_AUDIT_ITEMS.md.
Or try the live one: a two-table join you control, with the true output counted from the data and the ceiling computed beside it by the same WebAssembly binary the npm package ships. A scripted sweep of 73,205 configurations through it found zero violations — the page invites you to look for the first.
cargo run -p samkhya-core --example honest_demo --features lp_solver
cargo add samkhya-corepip install samkhyanpm install samkhyaEleven crates on crates.io. The JavaScript package is built and verified but not yet published.