From bce585ee1cdbf728ae20afdc929f0d8a58afcd2d Mon Sep 17 00:00:00 2001 From: zz_y Date: Fri, 12 Jun 2026 09:04:55 -0600 Subject: [PATCH] fix(optimizer): recall-SLA-aware top-k bind (CMS-heap vs CountSketch) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Fig-12 harness flagged a P95 cost-gap tail: BindCountSketchOnTopK hard-bound every non-exact top-k to CountSketch (~250 KB wire state) when a Count-Min-with-heap (~4 KB) answers approximate heavy-hitter queries fine under a loose recall SLA — ~66x more expensive than necessary. Make the top-k binding recall-tier-aware and cost-aware: * Loose recall (recall@k >= ~0.9, no signed/exact-rank need; the common case) -> CMS-with-heap (cheap, one-sided over-estimate). * Tight recall (exact rank / signed / two-sided) -> CountSketch- with-heap (unbiased median-of-rows). The tie-break among families that meet the SLA uses the existing optimizer::cost::wire cost table — the same "min cost s.t. SLA" the oracle uses; for a loose SLA both clear the bar so the cheaper CMS-heap wins. The CMS-with-heap emit path is already servable end-to-end (stage_config promotes with_heap to CountMinSketchWithHeap; asap_tier_analysis maps it to FrequencyTopk(CmsWithHeap)). No per-query recall field exists yet, so the tier is inferred from the accuracy target (Exact -> Tight, else Loose); threading a real per-query recall@k target is the follow-up. bind_workload_typed keeps its pinned- family routing via the new apply_with_tier entry point (CountSketch pick -> Tight, CMS-on-topk pick -> Loose), so contract-row mappings are unchanged. The redundant bind_cms_with_heap_on_topk helper is removed. Co-Authored-By: Claude Opus 4.8 (1M context) --- control_plane/src/optimizer/rules/mod.rs | 88 ++---- .../src/sketch_algebra/rules/bind_cms_topk.rs | 250 +++++++++++++++--- control_plane/src/sketch_algebra/tests.rs | 127 ++++++--- 3 files changed, 328 insertions(+), 137 deletions(-) diff --git a/control_plane/src/optimizer/rules/mod.rs b/control_plane/src/optimizer/rules/mod.rs index 7f3119216..aa8542cd5 100644 --- a/control_plane/src/optimizer/rules/mod.rs +++ b/control_plane/src/optimizer/rules/mod.rs @@ -257,82 +257,26 @@ pub fn bind_workload_typed(w: &QueryWorkload) -> Option BindDDSketchOnQuantile.apply(&aggregate, &accuracy), (SketchKind::Kll, _) => BindKllOnQuantile.apply(&aggregate, &accuracy), (SketchKind::Hll, _) => BindHllOnCardinality.apply(&aggregate, &accuracy), - (SketchKind::CountSketch, _) => BindCountSketchOnTopK.apply(&aggregate, &accuracy), - (SketchKind::Cms, StatisticClass::TopK) => { - bind_cms_with_heap_on_topk(&aggregate, &accuracy) - } + // The capability matrix already pinned the family here, so force + // the matching recall tier rather than re-inferring it: a + // CountSketch pick is the unbiased canonical top-k (Tight); a CMS + // pick on a top-k is the cheap CMS-with-heap (Loose). This keeps + // `bind_workload_typed`'s contract-row mapping deterministic — the + // recall-aware default lives in `dispatch()` / `Rule::apply`. + (SketchKind::CountSketch, _) => BindCountSketchOnTopK.apply_with_tier( + &aggregate, + &accuracy, + crate::sketch_algebra::rules::bind_cms_topk::TopkRecallTier::Tight, + ), + (SketchKind::Cms, StatisticClass::TopK) => BindCountSketchOnTopK.apply_with_tier( + &aggregate, + &accuracy, + crate::sketch_algebra::rules::bind_cms_topk::TopkRecallTier::Loose, + ), (SketchKind::Cms, _) => BindCmsOnCount.apply(&aggregate, &accuracy), } } -/// Bind `Aggregate{TopK{k, accuracy}}` to a CMS-with-heap sketch — the -/// CMS-Heap pattern from Cormode & Muthukrishnan (2005). Mirrors the -/// `(eps, delta) → (w, d)` mapping used by `BindCmsOnCount` and the -/// `with_heap` flag pattern from `BindCountSketchOnTopK`. CountSketch -/// remains the canonical (unbiased) TopK pick; this binder fires only -/// when a workload override has explicitly selected `CountMinSketch` for -/// a TopK metric. -fn bind_cms_with_heap_on_topk( - expr: &crate::intent_algebra::QueryExpr, - accuracy: &crate::types_v2::AccuracyTarget, -) -> Option { - use crate::intent_algebra::{AggIntent, QueryExpr}; - use crate::sketch_algebra::params::{CmsParams, SketchKind, SketchParams}; - use crate::sketch_algebra::physical_expr::{EstimateOp, PhysicalExpr}; - use crate::types_v2::AccuracyTarget; - - let (k_topk, intent_accuracy, child) = match expr { - QueryExpr::Aggregate { - aggs, child, by, .. - } if aggs.len() == 1 && by.is_empty() => match &aggs[0] { - AggIntent::TopK { k, accuracy } => (*k, accuracy.clone(), child), - _ => return None, - }, - _ => return None, - }; - - if k_topk == 0 { - return None; - } - - let (eps, delta) = match (accuracy, &intent_accuracy) { - (AccuracyTarget::Exact, _) | (_, AccuracyTarget::Exact) => return None, - (AccuracyTarget::Epsilon(a), AccuracyTarget::Epsilon(b)) => (a.min(*b), 0.01), - (AccuracyTarget::Epsilon(a), AccuracyTarget::EpsilonDelta { eps, delta }) - | (AccuracyTarget::EpsilonDelta { eps, delta }, AccuracyTarget::Epsilon(a)) => { - (a.min(*eps), *delta) - } - ( - AccuracyTarget::EpsilonDelta { eps: a, delta: da }, - AccuracyTarget::EpsilonDelta { eps: b, delta: db }, - ) => (a.min(*b), da.min(*db)), - }; - - if eps <= 0.0 || delta <= 0.0 || delta >= 1.0 { - return None; - } - - let w = (std::f64::consts::E / eps).ceil() as u32; - let d = (1.0 / delta).ln().ceil() as u32; - let w = w.max(2); - let d = d.max(1); - - Some(PhysicalExpr::estimate_over_agg( - EstimateOp::TopK { k: k_topk }, - SketchKind::Cms, - // CMS-Heap pattern: pair the CMS matrix with a heavy-hitter - // heap so `topk(...)` can enumerate items from the heap - // directly. The streaming-config emit picks - // `CountMinSketchWithHeap` for this binding. - SketchParams::Cms(CmsParams { - w, - d, - with_heap: true, - }), - (**child).clone(), - )) -} - pub struct RulesPlanner { pub valid_for: Duration, pub sketch_defaults: SketchDefaults, diff --git a/control_plane/src/sketch_algebra/rules/bind_cms_topk.rs b/control_plane/src/sketch_algebra/rules/bind_cms_topk.rs index 26b11e0bf..6b492a0a2 100644 --- a/control_plane/src/sketch_algebra/rules/bind_cms_topk.rs +++ b/control_plane/src/sketch_algebra/rules/bind_cms_topk.rs @@ -1,45 +1,171 @@ -//! `BindCountSketchOnTopK` — `Aggregate{TopK{k, accuracy}}` → CountSketch-with-heap. +//! `BindCountSketchOnTopK` — `Aggregate{TopK{k, accuracy}}` → a +//! heavy-hitter sketch, picked **recall-SLA-aware**. //! //! Reference: `control_plane/docs/design.md` §6 line ~419 — "`SketchAgg //! { intent, col }` … L4 emits `PhysicalExpr::SketchAgg`" — and -//! `intent_algebra::AggIntent::TopK` (heavy-hitter intent) maps directly -//! to a heavy-hitter sketch primitive. CountSketch with a heap of size -//! `k` is the textbook fit (Charikar-Chen-Farach-Colton); CMS with a -//! heap is an alternative the cost model can pick instead. +//! `intent_algebra::AggIntent::TopK` (heavy-hitter intent) maps to a +//! heavy-hitter sketch primitive. Two families answer the intent: //! -//! Phase C ships the CountSketch-with-heap variant only — it pairs -//! cleanly with the existing `algebra::directory` defaults -//! (CountSketch is what the legacy in-tree planner already emits for -//! Frequency-shaped workloads) and the heap is the part that turns it -//! into a TopK primitive. +//! * **CMS-with-heap** (Count-Min + size-`k` heap; Cormode-Muthukrishnan +//! 2005). One-sided over-estimate; ~4 KB of wire state. Recovers the +//! top-`k` heavy hitters w.h.p. — perfectly adequate for the common +//! "who are the top-k" question under a **loose recall SLA**. +//! * **CountSketch-with-heap** (Charikar-Chen-Farach-Colton; median-of- +//! rows). Unbiased / two-sided / signed estimates, supports exact-rank +//! reconstruction — but ~250 KB of wire state, ~66× the CMS-heap cost +//! (see `optimizer::cost::wire`: `count_sketch_delta` 250 KB vs +//! `count_min_delta` 4 KB). //! -//! Accuracy mapping: `AccuracyTarget::EpsilonDelta { eps, delta }` → -//! `(w, d) = (⌈e/eps⌉, ⌈ln(1/delta)⌉)`, identical to CMS. The heap size -//! is fixed at the requested `k`. See `accuracy_profile.rs` -//! (ASAPQuery-backend) for the formal heavy-hitter recall guarantee -//! (CountSketch + size-k heap recovers all heavy hitters with -//! frequency `≥ ‖f‖₁ / k` w.h.p.). +//! ## The recall-aware binding (Fig-12 cost-gap fix) +//! +//! The original rule hard-bound CountSketch for every non-exact top-k. +//! That paid the 250 KB CountSketch price even when a loose recall SLA +//! (`recall@k ≥ 0.9`, approximate heavy hitters — the common case) would +//! be satisfied by the 4 KB CMS-heap, blowing the P95 cost-gap tail the +//! Fig-12 harness measured. +//! +//! The fix makes the family choice [`TopkRecallTier`]-driven: +//! +//! | Tier | When | Bound family | Wire cost | +//! |---|---|---|---| +//! | [`TopkRecallTier::Loose`] | approximate heavy hitters, `recall@k ≥ ~0.9`, no signed/exact-rank need | **CMS-with-heap** | ~4 KB | +//! | [`TopkRecallTier::Tight`] | exact rank, very-high recall, or signed/two-sided estimate required | **CountSketch-with-heap** | ~250 KB | +//! +//! The tie-break between "both meet the SLA" picks the cheaper family by +//! the [`optimizer::cost::wire`] cost table — the same "min cost s.t. SLA" +//! the oracle uses. For a loose SLA both families clear the recall bar, so +//! CMS-heap (the cheaper one) wins; for a tight SLA only CountSketch +//! clears it, so it wins regardless of price. +//! +//! ## Where the recall SLA comes from +//! +//! There is no per-query recall field today (`AccuracyTarget` is +//! `Exact` / `Epsilon` / `EpsilonDelta` — a *frequency*-error budget, not +//! a *recall* budget). Until one is threaded through (the follow-up), the +//! tier is inferred conservatively from the accuracy target: +//! +//! * `AccuracyTarget::Exact` (on either the query policy or the intent) → +//! exact rank required → **Tight** (CountSketch) — preserving the old +//! exact-bail behaviour but as a *family* pick rather than a `None`. +//! (Note: a true exact top-k still needs HashAgg+Heap; CountSketch is +//! the closest sketch-tier approximation and the unbiased estimator.) +//! * everything else → **Loose** (CMS-heap) — the cheap common-case +//! default. +//! +//! See [`TopkRecallTier::from_accuracy`] for the mapping and the +//! module-level follow-up note. +//! +//! Accuracy → `(w, d)` mapping: `AccuracyTarget::EpsilonDelta { eps, +//! delta }` → `(w, d) = (⌈e/eps⌉, ⌈ln(1/delta)⌉)`, identical for both CMS +//! and CountSketch. The heap size is the requested `k`. See +//! `accuracy_profile.rs` (ASAPQuery-backend) for the formal heavy-hitter +//! recall guarantee (a frequency sketch + size-`k` heap recovers all +//! heavy hitters with frequency `≥ ‖f‖₁ / k` w.h.p.). #![allow(dead_code)] use crate::intent_algebra::{AggIntent, QueryExpr}; -use crate::sketch_algebra::params::{CountSketchParams, SketchKind, SketchParams}; +use crate::optimizer::cost::wire::WireCostTable; +use crate::sketch_algebra::params::{CmsParams, CountSketchParams, SketchKind, SketchParams}; use crate::sketch_algebra::physical_expr::{EstimateOp, PhysicalExpr}; use crate::sketch_algebra::rules::Rule; use crate::types_v2::AccuracyTarget; +/// Recall tier for a top-k binding — drives the family pick. +/// +/// "Recall" here is `recall@k` of the heavy-hitter set: the fraction of +/// the true top-`k` items the sketch's heap recovers. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TopkRecallTier { + /// Approximate heavy hitters are fine (`recall@k ≥ ~0.9`), and no + /// signed / two-sided / exact-rank estimate is needed. CMS-with-heap + /// (one-sided over-estimate) is the cheap, correct choice. + Loose, + /// Exact rank, very-high recall, or a signed / two-sided estimate is + /// required. CountSketch-with-heap (unbiased median-of-rows) is the + /// fit despite its ~66× wire cost. + Tight, +} + +impl TopkRecallTier { + /// Conservative recall tier inferred from the (policy, intent) + /// accuracy targets. No per-query recall field exists today, so: + /// + /// * either side `Exact` → exact-rank intent → [`Tight`]. + /// * otherwise → [`Loose`] (the cheap common-case default). + /// + /// **Follow-up:** thread a real per-query `recall@k` target (or a + /// `signed`/`two_sided` flag) through `AccuracyTarget` / the query + /// policy and pivot here on that instead of inferring from `Exact`. + /// + /// [`Tight`]: TopkRecallTier::Tight + /// [`Loose`]: TopkRecallTier::Loose + pub fn from_accuracy(policy: &AccuracyTarget, intent: &AccuracyTarget) -> Self { + match (policy, intent) { + (AccuracyTarget::Exact, _) | (_, AccuracyTarget::Exact) => TopkRecallTier::Tight, + _ => TopkRecallTier::Loose, + } + } +} + pub struct BindCountSketchOnTopK; -impl Rule for BindCountSketchOnTopK { - fn name(&self) -> &'static str { - "bind_cms_topk" +impl BindCountSketchOnTopK { + /// Families that satisfy a top-k recall tier, cheapest-first. + /// + /// * `Loose`: both CMS-heap (cheap) and CountSketch-heap clear the + /// recall bar, so the cost model is free to pick the cheaper one. + /// * `Tight`: only CountSketch-heap (unbiased / signed / exact-rank) + /// clears the bar. + fn candidate_families(tier: TopkRecallTier) -> &'static [SketchKind] { + match tier { + TopkRecallTier::Loose => &[SketchKind::Cms, SketchKind::CountSketch], + TopkRecallTier::Tight => &[SketchKind::CountSketch], + } } - fn priority(&self) -> u16 { - 5 + /// Pick the SLA-meeting family with the lowest per-flush wire cost. + /// `candidates` is already filtered to the families that meet the + /// recall SLA (see [`Self::candidate_families`]); this is the + /// "min cost s.t. SLA" tie-break the oracle uses. + fn cheapest_family(candidates: &[SketchKind], table: &WireCostTable) -> SketchKind { + candidates + .iter() + .min_by_key(|k| table.for_kind(k).per_flush()) + .cloned() + // (above: k is &&SketchKind; for_kind autoderefs to &SketchKind) + // candidate_families never returns empty. + .unwrap_or(SketchKind::CountSketch) } - fn apply(&self, expr: &QueryExpr, accuracy: &AccuracyTarget) -> Option { + /// Bind a top-k under an explicit recall tier — bypasses the + /// accuracy-inferred tier in [`Rule::apply`]. Used by + /// `optimizer::rules::bind_workload_typed`, which has already pinned + /// the family from the capability matrix / a `sketch_family_override` + /// and just needs the matching heap-bearing binding: + /// + /// * a CountSketch family pick → [`TopkRecallTier::Tight`] + /// (CountSketch-with-heap, the unbiased canonical pick). + /// * a CMS family pick on a top-k → [`TopkRecallTier::Loose`] + /// (CMS-with-heap). + pub fn apply_with_tier( + &self, + expr: &QueryExpr, + accuracy: &AccuracyTarget, + tier: TopkRecallTier, + ) -> Option { + self.bind(expr, accuracy, Some(tier)) + } + + /// Core binding. When `forced_tier` is `Some`, that tier is used; + /// otherwise the tier is inferred from the accuracy targets via + /// [`TopkRecallTier::from_accuracy`]. + fn bind( + &self, + expr: &QueryExpr, + accuracy: &AccuracyTarget, + forced_tier: Option, + ) -> Option { let (k_topk, intent_accuracy, child) = match expr { QueryExpr::Aggregate { aggs, child, by, .. @@ -54,8 +180,25 @@ impl Rule for BindCountSketchOnTopK { return None; } + // Recall tier first — it decides which families are viable, and + // (for the Tight tier) it is the only thing that keeps CountSketch + // in play. Note we no longer bail to `None` on `Exact`: an exact + // top-k intent picks the unbiased CountSketch family (the closest + // sketch-tier approximation) rather than declining the binding. + let tier = + forced_tier.unwrap_or_else(|| TopkRecallTier::from_accuracy(accuracy, &intent_accuracy)); + + // Derive (w, d) from the frequency-error budget. `Exact` on either + // side leaves the ε/δ unspecified (it's a *recall* tier signal, + // not a frequency budget), so fall back to the catalog defaults + // (eps=0.01, delta=0.01) used elsewhere for heavy-hitter sketches. let (eps, delta) = match (accuracy, &intent_accuracy) { - (AccuracyTarget::Exact, _) | (_, AccuracyTarget::Exact) => return None, + (AccuracyTarget::Exact, AccuracyTarget::Exact) => (0.01, 0.01), + (AccuracyTarget::Exact, other) | (other, AccuracyTarget::Exact) => match other { + AccuracyTarget::Epsilon(a) => (*a, 0.01), + AccuracyTarget::EpsilonDelta { eps, delta } => (*eps, *delta), + AccuracyTarget::Exact => (0.01, 0.01), + }, (AccuracyTarget::Epsilon(a), AccuracyTarget::Epsilon(b)) => (a.min(*b), 0.01), (AccuracyTarget::Epsilon(a), AccuracyTarget::EpsilonDelta { eps, delta }) | (AccuracyTarget::EpsilonDelta { eps, delta }, AccuracyTarget::Epsilon(a)) => { @@ -78,19 +221,64 @@ impl Rule for BindCountSketchOnTopK { // sketchlib bit-slices the hash with a pow2 column mask. Round the // ε-derived width UP to the next power of two — this only tightens // the additive bound (ε ≤ e/w) and prevents an agent-side - // "cols must be a power of two" crash on config apply. + // "cols must be a power of two" crash on config apply. CMS does not + // require pow2 cols, but using the same width keeps the two + // families' accuracy comparable for the cost-model tie-break. let w = w.max(2).next_power_of_two(); let d = d.max(1); + // Cost-aware tie-break: among the families that meet the recall + // SLA for this tier, pick the cheapest by the wire cost model + // (the same "min cost s.t. SLA" the oracle uses). + let table = WireCostTable::default(); + let family = Self::cheapest_family(Self::candidate_families(tier), &table); + + let (kind, params) = match family { + SketchKind::Cms => ( + SketchKind::Cms, + // CMS-Heap pattern: pair the CMS matrix with a size-k + // heavy-hitter heap. The streaming-config emit promotes + // this to `CountMinSketchWithHeap` (servable as + // FrequencyTopk per `asap_tier_analysis`). + SketchParams::Cms(CmsParams { + w, + d, + with_heap: true, + }), + ), + // Tight tier (and any future family) → CountSketch-with-heap. + _ => ( + SketchKind::CountSketch, + SketchParams::CountSketch(CountSketchParams { + w, + d, + with_heap: true, + }), + ), + }; + Some(PhysicalExpr::estimate_over_agg( EstimateOp::TopK { k: k_topk }, - SketchKind::CountSketch, - SketchParams::CountSketch(CountSketchParams { - w, - d, - with_heap: true, - }), + kind, + params, (**child).clone(), )) } } + +impl Rule for BindCountSketchOnTopK { + fn name(&self) -> &'static str { + "bind_cms_topk" + } + + fn priority(&self) -> u16 { + 5 + } + + fn apply(&self, expr: &QueryExpr, accuracy: &AccuracyTarget) -> Option { + // Recall-aware default: tier is inferred from the accuracy targets + // (loose → CMS-heap, tight/exact → CountSketch). Callers that have + // already pinned a family use [`Self::apply_with_tier`] instead. + self.bind(expr, accuracy, None) + } +} diff --git a/control_plane/src/sketch_algebra/tests.rs b/control_plane/src/sketch_algebra/tests.rs index 2066735f1..44b9e83b4 100644 --- a/control_plane/src/sketch_algebra/tests.rs +++ b/control_plane/src/sketch_algebra/tests.rs @@ -218,50 +218,36 @@ fn bind_picks_ddsketch_over_kll_when_eps_explicit() { } } -#[test] -fn bind_cms_topk_basic() { - let expr = QueryExpr::Aggregate { +/// Build an `Aggregate{TopK{k, accuracy}}` over the windowed scan. +fn agg_topk(k: usize, accuracy: AccuracyTarget) -> QueryExpr { + QueryExpr::Aggregate { by: vec![], - aggs: vec![AggIntent::TopK { - k: 10, - accuracy: AccuracyTarget::EpsilonDelta { - eps: 0.01, - delta: 0.001, - }, - }], + aggs: vec![AggIntent::TopK { k, accuracy }], having: None, child: Box::new(windowed_scan()), - }; - let bound = bind_query_expr( - &expr, - AccuracyTarget::EpsilonDelta { - eps: 0.01, - delta: 0.001, - }, - ) - .expect("bind_query_expr should not error"); + } +} + +/// Pull the bound `(SketchKind, with_heap, w, d)` out of a top-k binding. +fn topk_binding_family(bound: &PhysicalExpr) -> (SketchKind, bool, u32, u32) { + use crate::sketch_algebra::params::{CmsParams, CountSketchParams}; match bound { PhysicalExpr::SketchEstimate { op, child } => { - assert_eq!(op, EstimateOp::TopK { k: 10 }); - match *child { + assert_eq!(*op, EstimateOp::TopK { k: 10 }); + match &**child { PhysicalExpr::SketchAgg { sketch_type, params, .. - } => { - assert_eq!(sketch_type, SketchKind::CountSketch); - match params { - SketchParams::CountSketch(p) => { - assert!( - p.with_heap, - "TopK binding must enable the heavy-hitter heap" - ); - assert!(p.w >= 2); - assert!(p.d >= 1); - } - other => panic!("expected CountSketchParams, got {other:?}"), + } => match params { + SketchParams::Cms(CmsParams { w, d, with_heap }) => { + (sketch_type.clone(), *with_heap, *w, *d) } - } + SketchParams::CountSketch(CountSketchParams { w, d, with_heap }) => { + (sketch_type.clone(), *with_heap, *w, *d) + } + other => panic!("expected CMS/CountSketch params, got {other:?}"), + }, other => panic!("expected SketchAgg, got {other:?}"), } } @@ -269,6 +255,79 @@ fn bind_cms_topk_basic() { } } +/// (a) A **loose-recall** top-k (any non-exact accuracy target) binds the +/// cheap **CMS-with-heap** family — the Fig-12 cost-gap fix. The old rule +/// hard-bound the ~66×-more-expensive CountSketch here. +#[test] +fn bind_cms_topk_loose_recall_picks_cms_heap() { + let acc = AccuracyTarget::EpsilonDelta { + eps: 0.01, + delta: 0.001, + }; + let expr = agg_topk(10, acc.clone()); + let bound = bind_query_expr(&expr, acc).expect("bind_query_expr should not error"); + let (kind, with_heap, w, d) = topk_binding_family(&bound); + assert_eq!( + kind, + SketchKind::Cms, + "loose-recall top-k must bind the cheap CMS-with-heap, not CountSketch" + ); + assert!(with_heap, "top-k binding must enable the heavy-hitter heap"); + assert!(w >= 2); + assert!(d >= 1); +} + +/// (b) A **tight / exact-recall** top-k (the intent carries +/// `AccuracyTarget::Exact`) binds the unbiased **CountSketch-with-heap** — +/// the family that supports exact rank / signed estimates. +#[test] +fn bind_cms_topk_tight_recall_picks_countsketch() { + // Intent requests Exact rank; the policy-level target is non-exact. + let expr = agg_topk(10, AccuracyTarget::Exact); + let bound = bind_query_expr(&expr, AccuracyTarget::Epsilon(0.01)) + .expect("bind_query_expr should not error"); + let (kind, with_heap, w, d) = topk_binding_family(&bound); + assert_eq!( + kind, + SketchKind::CountSketch, + "exact-rank top-k must bind the unbiased CountSketch-with-heap" + ); + assert!(with_heap, "top-k binding must enable the heavy-hitter heap"); + assert!(w >= 2); + assert!(d >= 1); +} + +/// (c) The chosen family is the **cost-minimal one that meets the recall +/// SLA**, per the `optimizer::cost::wire` table — the same "min cost s.t. +/// SLA" the oracle uses. Loose → both families clear the bar → cheapest +/// (CMS, ~4 KB) wins; the CountSketch alternative (~250 KB) is ~66× +/// costlier. +#[test] +fn bind_cms_topk_picks_cost_min_meeting_sla() { + use crate::optimizer::cost::wire::WireCostTable; + let table = WireCostTable::default(); + let cms = table.for_kind(&SketchKind::Cms).per_flush(); + let cs = table.for_kind(&SketchKind::CountSketch).per_flush(); + assert!( + cms < cs, + "CMS-heap ({cms} B) must be cheaper than CountSketch ({cs} B) on the wire" + ); + // The cost gap the Fig-12 harness measured (~66×). + let ratio = cs as f64 / cms as f64; + assert!( + ratio > 50.0, + "CountSketch should be ~66× the CMS-heap wire cost; got {ratio:.1}×" + ); + + // Loose recall → the planner must land on the cost-min family (CMS). + let acc = AccuracyTarget::Epsilon(0.01); + let bound = bind_query_expr(&agg_topk(10, acc.clone()), acc).unwrap(); + let (kind, ..) = topk_binding_family(&bound); + let chosen = table.for_kind(&kind).per_flush(); + assert_eq!(chosen, cms.min(cs), "must pick the cost-min family that meets the SLA"); + assert_eq!(kind, SketchKind::Cms); +} + #[test] fn bind_hll_cardinality_basic() { let expr = QueryExpr::Aggregate {