No. 005Analytics4 мин чтения

The 15%-off dashboard: wrong joins corrupt analytics silently

A generated join that fans out rows doesn't crash. It inflates a metric by 15% and lets everyone keep steering by it. Silent wrongness is the real cost of text-to-SQL.

The worst database error is not the one that throws. It is the one that returns.

Picture the standard story. An AI assistant generates SQL against the warehouse. One question needs orders joined to payments. The generated join looks reasonable, runs clean, and quietly fans out — some orders have two payment rows, so those orders are counted twice. Revenue lands 15% high. The dashboard renders beautifully.

Nobody notices, because there is nothing to notice. No exception, no red cell, no anomaly flag. Just a plausible number, 15% away from the truth, feeding decisions for weeks until someone reconciles against finance and the archaeology begins.

Silent wrongness is structural, not incidental

Text-to-SQL failures come in two flavors. Loud ones — syntax errors, missing tables — are annoying and harmless; they announce themselves. Quiet ones return well-formed, wrong results, and the research on real schemas is sobering:

  • On idealized benchmarks, frontier models look strong. Move to actual enterprise schemas — hundreds of tables, ambiguous column names, tribal-knowledge join paths — and measured accuracy has been shown to collapse from the low nineties to roughly one in five.
  • Across studies, something like 40% of text-to-SQL agent queries fail outright or return incorrect results.
  • Generation is non-deterministic: teams evaluating these tools keep reporting that the same question produces different SQL — and different answers — on different runs. Your metric depends on which sample you got.

A join that fans out, a LEFT that should be INNER, a filter applied before an aggregation instead of after — every one of these returns a number. In analytics, wrong-but-plausible is strictly worse than absent.

The category fix: stop generating the risky artifact

You cannot lint your way out of this, because the defect is not in the SQL syntax — it is in the semantics of a query no one reviewed. SQAI's position is the categorical one: the model never authors SQL at all. It emits a typed plan, and the plan passes through checkpoints that a free-text string cannot offer.

Validation against a pinned contract. Every request is checked against a capability contract identified by hash — contract_hash sha256:79f1c5a6c716…. Name a field or function that doesn't exist and the answer is not creative improvisation; it is unsupported_operation, with nearest_matches suggesting what the model probably meant. Guessing is replaced by a typed negotiation.

Ambiguity surfaces instead of resolving silently. A text-to-SQL system asked something underspecified picks an interpretation and runs it — that choice is the silent corruption. SQAI's query tool returns one of four statuses: ok, needs_clarification, rejected, or error. When the question is ambiguous, the agent is told so, and asks you. The tools never throw and never guess.

No improvised joins. The fan-out bug class exists because row alignment was left to a generated ON clause. SQAI's engine resolves multi-column computations by row-aligned binding — columns are extracted and paired positionally, nulls handled pairwise. SQAI never zips misaligned series and never invents a join path. What the resolved plan binds is recorded, not inferred.

The plan itself is inspectable. Every resolved request produces a plan_hash (for example f87610d8afeb…) with its resolution recorded — decision_path: "exact_spec", deterministic_scope: "local_registered_source". When someone asks "what exactly produced this figure?", the answer is an artifact, not a shrug. There is even a dry-run tool, explainQuery, for seeing the plan before anything executes.

The drift you didn't generate

One more silent-corruption vector has nothing to do with the model: the data changes shape underneath a saved query. Yesterday's plan against today's schema is how a metric changes meaning without changing name.

SQAI pins that too. Every source carries a schema_revision, and a stale reference fails with schema_revision_mismatch — a refusal — rather than silently producing different numbers. The rule generalizes: anything that could change the answer either blocks execution or is recorded in the result.

Steering by numbers you can check

The 15%-off dashboard is not a story about a bad model. It is a story about an unreviewable artifact — generated SQL — sitting in the load-bearing position of an analytics pipeline. The fix is not a smarter generator, more prompt examples, or a second model grading the first. It is removing the generated artifact and replacing it with a typed plan that is validated, policy-checked, and hashed before it executes — the full pipeline is laid out in how it works.

Wrong addition is at least conceptually easy to catch — we covered that failure in the confidently wrong revenue number. A wrong join is worse: locally reasonable, globally false, invisible until reconciliation. Design it out. When every number on the dashboard traces to a hashed plan against a pinned schema, "is this figure right?" stops being a feeling and becomes a lookup.