Class ObservedCardinalities
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 Summary
FieldsModifier and TypeFieldDescriptionstatic final intHow many expressions are remembered.static final ObservedCardinalitiesA store that remembers nothing and can be handed anywhere one is wanted. -
Constructor Summary
ConstructorsConstructorDescriptionCreates a store holdingDEFAULT_MAX_ENTRIESexpressions.ObservedCardinalities(int maxEntries) Creates a store holding at mostmaxEntriesexpressions. -
Method Summary
Modifier and TypeMethodDescriptionforExpression(RelNode node) Returns the observed row count fornode, or empty if it has not been run.booleanisEmpty()Returns whether anything has been recorded.voidRecords thatnodeproducedrowsrows.voidRecords a count against an expression's digest directly.intsize()Returns how many expressions are remembered.toString()
-
Field Details
-
DEFAULT_MAX_ENTRIES
public static final int DEFAULT_MAX_ENTRIESHow 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
A store that remembers nothing and can be handed anywhere one is wanted.
-
-
Constructor Details
-
ObservedCardinalities
public ObservedCardinalities()Creates a store holdingDEFAULT_MAX_ENTRIESexpressions. -
ObservedCardinalities
public ObservedCardinalities(int maxEntries) Creates a store holding at mostmaxEntriesexpressions.- Parameters:
maxEntries- the bound; zero for a store that remembers nothing- Throws:
IllegalArgumentException- if negative
-
-
Method Details
-
record
Records thatnodeproducedrowsrows.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 nullrows- how many rows it produced; must not be negative
-
record
Records a count against an expression's digest directly.- Parameters:
digest- the expression'sAstEquivalence.digest(com.darkcollective.relix.ast.RelNode); must not be nullrows- how many rows it produced; must not be negative
-
forExpression
Returns the observed row count fornode, 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
-