Class ObservedCardinalities

java.lang.Object
com.darkcollective.relix.cost.ObservedCardinalities

public final class ObservedCardinalities extends Object
Row counts a previous run actually produced, keyed by the expression that produced them.

The cost model otherwise estimates: it multiplies a leaf's row count by a selectivity it inferred from a predicate and a distinct count. Where a query has already run, there is something better than an estimate available — the number of rows it really returned — and this is where that number is kept so the next plan can use it.

The key is the expression, not the relation

A row count for a base relation has a home already: RelationStatistics, keyed by relation name, which is where an introspected count goes and where an observed one goes too. An observed post-selection or post-join count has no such home, because it is not a fact about any relation. It is a fact about an expression.

So the key is AstEquivalence.digest(com.darkcollective.relix.ast.RelNode), and three of its properties are why it is the right key rather than a new one. It returns a String — the location-free printed form, a full structural key and not a hash — so there is no collision risk and a persisted map is readable and diffable. It is already load-bearing for shared-sub-expression detection, so the two features share one canonicalisation rather than drifting apart. And being location-free, the same expression written at a different offset in a different script hits the same entry.

Two limits, and they are the interesting part

It memoises; it does not learn. Measuring σ status = 'OPEN' (Orders) teaches this nothing about σ status = 'CLOSED' (Orders), because their digests differ. Inferring the cost of an unseen predicate from measured ones is adaptive query optimisation, which is a different thing and not what this is.

A parameterised workload explodes the keyspace. σ id = 12345 (Orders) mints a distinct entry per literal — the classic plan-cache failure. The lever for that is the bound, not canonicalisation: normalising literals to widen the hit rate would discard exactly the precision that makes a recorded count worth more than an estimate. So this map is bounded and evicts its least recently used entry, and a workload that overflows it degrades to estimating, which is where it started.

Being a memo of what happened, it is only ever as current as the last run. A count recorded before a bulk load is stale in the ordinary way statistics are stale: it costs a worse plan, never a wrong answer.

  • Field Details

    • DEFAULT_MAX_ENTRIES

      public static final int DEFAULT_MAX_ENTRIES
      How many expressions are remembered. Large enough that a hand-written workload never evicts, small enough that a parameterised one cannot grow without bound.
      See Also:
    • NONE

      public static final ObservedCardinalities NONE
      A store that remembers nothing and can be handed anywhere one is wanted.
  • Constructor Details

    • ObservedCardinalities

      public ObservedCardinalities()
      Creates a store holding DEFAULT_MAX_ENTRIES expressions.
    • ObservedCardinalities

      public ObservedCardinalities(int maxEntries)
      Creates a store holding at most maxEntries expressions.
      Parameters:
      maxEntries - the bound; zero for a store that remembers nothing
      Throws:
      IllegalArgumentException - if negative
  • Method Details

    • record

      public void record(RelNode node, long rows)
      Records that node produced rows rows.

      Only ever call this with a count the caller can prove: a stream that was drained to the end, not one a consumer stopped reading. A partial read is a number about the consumer, and filing it here would make the planner confident and wrong.

      Parameters:
      node - the expression that was evaluated; must not be null
      rows - how many rows it produced; must not be negative
    • record

      public void record(String digest, long rows)
      Records a count against an expression's digest directly.
      Parameters:
      digest - the expression's AstEquivalence.digest(com.darkcollective.relix.ast.RelNode); must not be null
      rows - how many rows it produced; must not be negative
    • forExpression

      public OptionalLong forExpression(RelNode node)
      Returns the observed row count for node, or empty if it has not been run.
      Parameters:
      node - the expression; must not be null
      Returns:
      the observed row count for node, or empty if it has not been run
    • isEmpty

      public boolean isEmpty()
      Returns whether anything has been recorded.
      Returns:
      whether anything has been recorded
    • size

      public int size()
      Returns how many expressions are remembered.
      Returns:
      how many expressions are remembered
    • toString

      public String toString()
      Overrides:
      toString in class Object