Group Caches: Difference between revisions

From MemCP
Jump to navigation Jump to search
(Created page with "<!-- Copyright (C) 2026 Carl-Philip Hänsch --> = Group Cache Dimensions: Equality, Additive Ranges, and Snapshots = Implementation reference: 25 September 2026, commit b22387664, [https://github.com/launix-de/memcp/pull/999 PR #999]. Examples describe query shapes; actual eligibility and plan selection depend on semantic proofs, estimated costs, and cache state. A group cache retains reusable aggregate state across SQL requests. It is separate from the query-plan cach...")
 
No edit summary
Line 1: Line 1:
<!-- Copyright (C) 2026 Carl-Philip Hänsch -->
<!-- Copyright (C) 2026 Carl-Philip Hänsch -->
= Group Cache Dimensions: Equality, Additive Ranges, and Snapshots =
= Group Caches: Reuse Calculations Across Queries =


Implementation reference: 25 September 2026, commit b22387664, [https://github.com/launix-de/memcp/pull/999 PR #999]. Examples describe query shapes; actual eligibility and plan selection depend on semantic proofs, estimated costs, and cache state.
A '''group cache''' remembers calculations over groups of database records. Once MemCP has worked out a group's total, a later query can reuse that work instead of reading and adding up the same records again.


A group cache retains reusable aggregate state across SQL requests. It is separate from the query-plan cache and from caching complete HTTP responses. Cache eviction or changes to source dependencies can require reconstruction.
For example, a dashboard might repeatedly ask for an order count, revenue over a selected interval, or the total quantity held by a set of accounts. These questions often share most of their work, even when the user changes a filter or moves an interval boundary. Group caches let MemCP reuse the parts that still apply.


The three dimension modes describe '''how aggregate results at different parameter values relate to each other'''. They are not specific to time. Coordinates can represent numeric versions, positions, thresholds, or timestamps.
'''The goal is to touch as few source records as possible to produce the answer.''' Sometimes the answer is already available. Sometimes it can be assembled from stored subtotals. Sometimes only a small set of contributions needs to be recalculated.
 
== What does the cache remember? ==
 
Think of a group cache as an automatically managed table of reusable calculations. Its keys describe which records and parameter values a calculation belongs to; its values hold totals or the information needed to update those totals.
 
The cache survives the SQL request that created it. Compatible calculations in later requests can use it too. It remembers intermediate results, so reuse does not require the entire SQL query to be identical. A query-plan cache, by comparison, remembers ''how to execute'' a query; a group cache remembers ''work already done''.
 
The planner chooses whether to use a group cache. Applications continue to issue SQL and do not have to create or refresh these tables themselves.
 
== Three ways to reuse a calculation ==
 
The three '''dimension types''' describe how a calculation depends on its inputs. A dimension can be a customer ID, a price, a position, a revision number, or a timestamp. The mechanism is not specific to time.


{| class="wikitable"
{| class="wikitable"
! Dimension mode !! Meaning !! Reuse strategy !! Typical query shape
! Dimension !! Question !! How the cache helps
|-
|-
| '''Equality / point domain''' || An exact key or key tuple || Look up the aggregate state for that key || Aggregate over an equality group
| '''Equality / point''' || What is the total for this customer? || Look up that customer's stored result.
|-
|-
| '''Additive range domain''' || An interval of an ordered source dimension || Combine states for disjoint input intervals || Aggregate over selected facts
| '''Additive range''' || What is the total inside this interval? || Combine stored subtotals for disjoint parts of the interval.
|-
|-
| '''Snapshot domain''' || A parameter point at which contributions were evaluated || Read an exact point, or correct an existing state for a new point || Aggregate over parameter-dependent selected states
| '''Snapshot''' || What is the total when evaluated at this parameter value? || Reuse a stored result and correct the contributions that may differ.
|}
|}


The new shape is called '''snapshot aggregation over a parameter domain''', or more precisely, '''a snapshot domain with incremental contribution correction'''. ''As-of state aggregation'' is a useful name for the ordered-state-selection subclass, but the abstraction is not restricted to temporal queries.
=== Equality: calculate a group once ===
 
== 1. Equality domains: One aggregate per exact key ==


<pre>
<pre>
SELECT COUNT(*) FROM orders WHERE customer_id = :customer;
SELECT SUM(amount)
SELECT SUM(amount) FROM orders WHERE customer_id = :customer;
FROM orders
SELECT COUNT(*) FROM orders;
WHERE customer_id = :customer;
</pre>
</pre>


The first two queries can reuse aggregate states indexed by customer ID. An aggregate without a correlated parameter, such as the final COUNT(*), is the limiting case with one group and an empty key tuple. Additional fixed filters belong to the computation and its cache identity.
After calculating the total for customer 42, MemCP can retain it under that key. Another request for customer 42 can read the result without adding up all of that customer's orders again.
 
This is particularly useful for correlated aggregates: many outer rows may request the same group. Once its aggregate state is available, the group need not be aggregated again for each outer row. Several equality keys can form a partition, such as tenant and product class.


'''Work avoided:''' Repeated processing of M records in a group becomes a lookup of existing aggregate state. Whether to prepare groups together or populate them on demand is a physical planning decision.
The same idea applies to counts and other supported aggregates, to several keys together, and to aggregates used inside larger queries. A total over the whole table is simply a calculation with no varying group key.


'''Potential speedup:''' No isolated A/B measurement of this mode is claimed here. If an expensive group aggregate is requested 100 times, its work can conceptually drop from 100 aggregations to one build plus 100 lookups. The speedup of that part approaches at most 100×, while other query work limits the end-to-end gain. Against a cheap index probe or a tiny group, the benefit may be small.
This is useful when many outer rows or repeated requests ask for the same grouped calculation. The group and its filters must describe the same computation; unrelated totals are not interchangeable.


== 2. Additive range domains: Combine disjoint fact intervals ==
=== Additive ranges: assemble a total from smaller totals ===


<pre>
<pre>
SELECT SUM(revenue)
SELECT SUM(revenue)
FROM sales
FROM sales
WHERE coordinate >= :lo AND coordinate < :hi;
WHERE position >= :start AND position < :end;
 
SELECT SUM(revenue)
FROM sales
WHERE tenant_id = :tenant
  AND coordinate >= :lo AND coordinate < :hi;
</pre>
</pre>


Each fact has a contribution that does not depend on the requested interval within this computation. The bounds determine which facts participate. The second query combines an equality partition with a range dimension.
Suppose the cache already knows the subtotals for positions [0, 100) and [100, 200). It can answer [0, 200) by adding those two numbers. It does not need to visit every sale again.


For a sum over disjoint intervals:
For a different interval, MemCP can reuse matching parts and calculate the missing parts. It splits coverage where necessary so overlapping requests do not cause records to be counted twice.


<pre>
This works when a record's contribution is fixed and the interval only decides whether to include it. Examples include revenue over reporting windows, quantities within a position range, and cumulative counts. Each supported aggregate needs a valid way to combine its partial results.
R([a,c)) = R([a,b)) + R([b,c))
</pre>


A cache can reuse and combine matching partial states. Missing coverage and boundary fragments still require work. Overlapping cells must not be counted twice. Inclusive/exclusive boundaries and SQL NULL semantics must be preserved.
=== Snapshots: update the answer when contributions change ===


'''Suitable query families:''' Interval sums, interval counts, cumulative fact aggregates, and repeated overlapping reporting windows. SUM and COUNT are straightforward examples. More generally, each aggregate needs a valid state-combination rule. Conceptually, an average requires a sum and a count, rather than an unweighted average of averages. DISTINCT needs its own state semantics.
Some questions cannot be answered by adding up all records in an interval. Consider an account's history:


'''Work avoided:''' Instead of reading M facts, a query may process K cached partial states and B uncovered facts.
{| class="wikitable"
! Revision !! Recorded quantity
|-
| 10 || 3
|-
| 20 || 5
|}


'''Potential speedup:''' No isolated measured factor for this mode is claimed here. If those operations have comparable costs, M/(K+B) approximates the reduction in aggregation work. For example, replacing 1,000,000 facts with 100 cached cells plus 9,900 remaining facts means roughly 100× fewer processing units. This is an illustrative work estimate, not a measured latency speedup. Cold construction, fragmentation, and little overlap between requests can consume much of the benefit.
At revision 10, the account contributes 3. At revision 20, it contributes 5. Its contribution to the total has increased by '''2'''. Adding both history records would give the wrong answer.


== 3. Snapshot domains: Aggregate state at a parameter point ==
A typical SQL calculation selects one state for each account:
 
=== Why an additive range is insufficient ===


<pre>
<pre>
SELECT SUM((
SELECT SUM((
  SELECT h.quantity
    SELECT h.quantity
  FROM entity_history h
    FROM account_history h
  WHERE h.entity_id = e.id AND h.coordinate <= :x
    WHERE h.account_id = a.id AND h.revision <= :revision
  ORDER BY h.coordinate DESC
    ORDER BY h.revision DESC
  LIMIT 1
    LIMIT 1
))
))
FROM entity e;
FROM accounts a;
</pre>
</pre>


This example requires an unambiguous ordered selection, for example UNIQUE(entity_id, coordinate).
Here, each account has at most one history entry per revision.


It does '''not''' sum all history entries up to x. Each entity contributes only its selected state. If an entity changes from quantity 3 to quantity 5, the aggregate must increase by 2. Simply adding the new quantity 5 would be incorrect.
A snapshot stores the total at a particular parameter value, together with the individual contributions needed to correct it. To answer at another value, MemCP finds the accounts whose contributions might differ and recalculates those accounts:
 
A snapshot retains aggregate state at an anchor point x₀, together with logical keys and contribution values needed for correction. '''The snapshot itself is a point.''' The interval between x₀ and x helps identify possible changes.


<pre>
<pre>
C(k,x) = contribution of entity k at parameter point x
new total = stored total
S(x)  = sum of all C(k,x)
          - previous contributions of affected accounts
A      = a complete superset of potentially changed keys
          + new contributions of affected accounts
 
S(x) = S(x₀) - sum(C(k,x₀), k in A) + sum(C(k,x), k in A)
</pre>
</pre>


A key is corrected once even if several dependency paths identify it. For every key outside A, its contribution must be proven unchanged. Original residual predicates are still evaluated for candidates: a candidate may turn out not to change anything.
If 100 out of 100,000 accounts might differ, the other 99,900 contributions can remain untouched. An exact repeat can reuse the stored total directly.


=== Query structures recognized by the current implementation ===
'''A snapshot belongs to a point.''' The interval between two points helps locate possible changes. This is why a snapshot is a third dimension type, rather than another name for an additive range.


Subject to the corresponding proof conditions, the implementation handles these building blocks:
Snapshot reuse also covers supported conditions on the selected state. An account may start or stop qualifying, or contribute a different amount. A threshold crossing can matter even without a new history entry. MemCP must account for every way the contribution could change before it can safely leave the other accounts untouched.


* '''Ordered state selection per entity:''' Select an unambiguous state using a moving bound. Several scalar values may depend on that selection.
This makes the pattern useful for versioned quantities, qualifying contracts, configuration states, and series of totals at successive parameter values.
* '''Selected state plus threshold predicates:''' A contribution also depends on a selected property being less than, greater than, or equal to a coordinate. Boundary crossings without a new history row must be included in the change cover.
* '''Conditional SUM payloads:''' CASE/IF chooses a contribution using provable conditions. Change coverage includes selection, predicate, and payload dependencies.
* '''Fixed first/last properties:''' A coordinate-independent first or last value can use the same property group cache in both full construction and correction.
* '''Bounded EXISTS predicates:''' Relevant child rows crossing a bound can identify affected contributions through projected parent keys.
* '''Derived outer coordinates:''' A pure outer expression such as x-5 can define a dimension. The selected states for x and x-5 must remain distinct.
* '''Supported clamps and combinations:''' Certain MIN/MAX clamps on selection bounds, including clamps involving selected properties, COALESCE, and Boolean combinations have complete change-cover proofs.
* '''Repeated aggregate series:''' Multiple parameter points of the same formula can use retained anchors, which later SQL requests can reuse as well.


MIN/MAX in the clamp example means a '''scalar bound expression''', not general support for retractable MIN/MAX aggregates.
== The dimensions can be combined ==


Applications include versioned inventory, quantities of qualifying contracts, valid configurations, and other aggregates of “the selected state at coordinate x”. Eligibility follows the relational formula, not the application's business terminology.
These are three dimensions of the '''same group-cache mechanism'''. A single cache table can combine equality, range, and snapshot components and hold several aggregate calculations.


=== What happens on a subsequent request ===
For example, a report might ask:


# '''Same point, matching fixed inputs, unchanged dependencies:''' Read the retained aggregate state.
:''For tenant 7, consider accounts opened within a selected interval, and total their quantities as evaluated at revision 200.''
# '''New point:''' Choose a valid anchor, identify potential changes, retract the old contributions, and add the new ones.
# '''No relevant change between selection bounds:''' Proven equivalence of ordered selection cuts can permit reuse even when the numeric bound changes. A changed parameter value alone need not imply a fresh cache build.
# '''Many changes:''' Full reconstruction may be cheaper and can produce a new anchor.
# '''Source mutation or eviction:''' Reuse must be re-established. Moving an input parameter is distinct from modifying the underlying data.


For monotone interval covers, the current anchor selection compares affected-key work at the nearest valid anchor on each side. Numeric distance alone is not a work estimate. A farther anchor can require fewer corrections.
The tenant is an equality key. The opening interval is an additive range if it only selects which accounts participate. The revision is a snapshot point that determines each account's contribution. The cache can reuse results within that tenant, combine disjoint account intervals, and correct contributions when the revision moves.


=== Current implementation limits ===
The planner checks that these roles really are independent enough to combine. A bound that also changes a record's contribution cannot simply be treated as an additive filter.


* The new retraction path currently targets '''SUM contributions'''. Runtime admission permits NULL and integral numeric values within a conservative exactness budget; the sum of absolute magnitudes must not exceed 2^52. Other values retain the ordinary SQL reducer. Arbitrary floating-point sums are not silently reordered.
== How does the cache become useful? ==
* A count can mathematically be represented as a sum of 0/1 contributions. This does not imply that every COUNT spelling currently selects the snapshot path. Nor does the new path provide general support for COUNT DISTINCT, AVG, MIN/MAX, or arbitrary user-defined aggregates.
* The contribution proof currently requires a suitable single base driver with a single-column primary key. Arbitrarily multiplying joins and composite contribution identities are not generally covered.
* Each contribution proof chooses one moving axis; other inputs partition the cache. This is not yet general incremental correction across freely moving multidimensional surfaces.
* The current snapshot runtime restricts coordinates to suitable integral numeric values. The abstraction is broader than this implementation; support for arbitrary ordered types is not implied.
* Not every algebraically related formula has a proof. For example, a scaled inner selection coordinate is not automatically treated as an identity change domain.
* INSERT/UPDATE/DELETE, changes to classification tables, and table replacement must participate in dependency handling. An invalid snapshot may be discarded and rebuilt. Fully incremental maintenance for every source mutation is not implemented.
* Cost selection and memory budgets may favor direct execution even when a formula is eligible. Small or empty inputs should not pay for unnecessary preparation.


=== Potential speedup ===
A cache has a construction cost. MemCP compares caching with direct execution; it does not need to build an expensive cache for every eligible query.


Let N be the number of contributions in a full build and D the number of potentially changed contributions. A follow-up replaces much of the work on N contributions with change-cover/index work and evaluation of D contributions. With comparable per-contribution costs, N/D is a rough estimate for the reduction in that part of execution, not the complete HTTP latency.
For candidates subject to deferred admission, it initially executes the calculation directly and records the work spent. Once enough work has accumulated to justify materialization, the shared cache path can take over. An already calculated scalar result can be retained without calculating it again. A snapshot needs additional contribution information, so preparing it involves more work than saving a single number.


For example, 100,000 entities with 100 change candidates mean roughly 1,000× fewer contributions to re-evaluate. Expensive candidate discovery, remaining scans elsewhere in the query, planning, and response handling reduce the actual speedup.
After preparation:


Across P requested points, a favorable snapshot plan replaces P full evaluations with one build and P-1 corrections. An exact repeat can be cheaper still. Initial construction remains real work.
* '''A repeated key or snapshot point''' can read retained results.
* '''An overlapping range''' can reuse existing partial results.
* '''A new snapshot point''' can reuse an anchor and correct affected contributions. The current implementation compares neighboring anchors on either side using the number of potentially affected records, rather than numerical distance alone.
* '''A large correction''' can trigger a fresh snapshot instead. Future requests can then reuse that new starting point.


== Performance expectations and reproducible evaluation ==
Changing a query parameter is different from changing the underlying data. Inserts, updates, deletes, and schema changes must be reflected in dependency checks or invalidation. Reuse must also respect transaction visibility. Cached state can be discarded and rebuilt; it is not a durable replacement for the source data. Memory pressure can evict it as well.


There is no universal speedup factor for a dimension mode. The estimates above describe reductions in work under stated assumptions. They are not measured end-to-end latency guarantees.
== How much faster can it be? ==


A reproducible comparison should identify the exact source revisions, hardware, generated fixture, query parameters, and cache preparation. Measure one cold request plus a fixed number of follow-ups, include all requests in the total, and report phase timings separately. Moving-bound tests should cover both empty and nonempty change sets. Repeating an identical query tests exact-point reuse; it does not establish the cost of snapshot correction.
The gain comes from avoiding source-record processing. There is no fixed speedup for a dimension type.


The public test suite provides neutral examples that can be inspected and executed without access to application or customer data:
{| class="wikitable"
 
! Example !! Work avoided after preparation
* ''tests/performance/domain-snapshot.yaml'': generated entities and versioned values; ordered state selection, selected-property boundaries, moving parameters, retained snapshots, anchor selection, and source-change correctness.
|-
* ''tests/performance/group-range-cache.yaml'': generated range-aggregation workloads with performance configuration.
| Repeated total for a large customer group || Replace another aggregation over thousands of orders with a stored-result lookup.
* ''tests/planner/aggregates/group-range-cache.yaml'': correctness and plan regressions for group/range reuse.
| Interval covered by 100 cached subtotals || Combine 100 results instead of reading, for example, a million sales.
 
| 100 affected accounts out of 100,000 || Re-evaluate roughly 1,000 times fewer account contributions, plus the work needed to find them.
Use each test's declared setup and sampling configuration when reproducing it. Small correctness fixtures establish semantics, not production-scale speedups. A comparison intended to isolate one cache mode must control the other planner and runtime changes as well.
| Series of nearby snapshot points || Replace repeated full evaluations with a build followed by corrections where possible.
 
|}
''This page does not publish latency factors from private application workloads. It makes no independently measured speedup claim for an individual dimension mode.''
 
== Combining dimensions and choosing a plan ==


One report can use all three modes: equality partitions for tenants or categories, additive ranges for fact revenue, and snapshot domains for state-dependent quantities. Different metrics need not use the same mode. For example, the current implementation can keep categories as fixed partitions without computing every category together in one contribution vector.
These examples describe reductions in work, not guaranteed latency factors. Finding affected records, preparing plans, executing other query parts, and returning results still take time. An exact repeat can be very cheap, while a moving parameter usually requires some additional work. Small inputs or constantly changing data may offer little benefit.


Logical IR exposes parameter dependencies, unique selection, contribution identity, and a complete superset of potentially changed keys. Physical planning then decides whether direct execution or cache reuse is cheaper. The logical IR does not introduce physical scans or cache tables prematurely.
A useful performance comparison includes '''one cold request and a fixed number of follow-up requests'''. This includes construction costs and shows whether reuse pays off. Testing moving parameters is essential: repeating an identical request alone does not measure snapshot correction.


EXPLAIN IR and EXPLAIN REORDER help inspect the structure. EXPLAIN PHYSICAL exposes decisions such as ''contribution_domain'', with ''domain_snapshot'' and ''direct_aggregate'' alternatives. EXPLAIN shows the generated Scheme plan. Actual latency and records visited still require execution measurements.
== Which queries are covered today? ==


A snapshot cache is distinct from RecMap. RecMap accelerates physical row mappings within a query. Retained snapshots use logical keys and values; query-local physical row identities must not survive arbitrary shard rebuilds.
Equality grouping and supported additive range aggregates cover conventional grouped totals and repeated interval calculations. Snapshot recognition adds supported sums over uniquely identified records whose contribution depends on an ordered parameter, including ordered state selection and certain combinations of filters, conditional values, and nested lookups.


== Implementation and test references ==
The snapshot correction path currently focuses on exact integer-valued sums within its numerical safety limits and a supported record identity. It is not a general incremental engine for every SQL aggregate or join. Arbitrary floating-point sums, distinct counts, and arbitrary combinations of moving dimensions are not automatically eligible. If safe reuse cannot be established, the query retains an ordinary execution plan.


* [https://github.com/launix-de/memcp/pull/999 PR #999]: implementation changes.
For executable examples using generated data, see [https://github.com/launix-de/memcp/blob/perf/kpi-moving-snapshot-domains/tests/performance/domain-snapshot.yaml snapshot workloads] and [https://github.com/launix-de/memcp/blob/perf/kpi-moving-snapshot-domains/tests/planner/aggregates/group-range-cache.yaml combined dimension tests]. The unified implementation is described in [https://github.com/launix-de/memcp/pull/999 PR #999].
* [https://github.com/launix-de/memcp/blob/b2238766400c2c5b20d155a328d0ed380a7414ff/INVARIANTS.md INVARIANTS.md]: planner and group-cache contracts.
* [https://github.com/launix-de/memcp/blob/b2238766400c2c5b20d155a328d0ed380a7414ff/lib/queryplan-optimize.scm queryplan-optimize.scm]: logical contribution and dependency proofs.
* [https://github.com/launix-de/memcp/blob/b2238766400c2c5b20d155a328d0ed380a7414ff/lib/queryplan-physical-expr.scm queryplan-physical-expr.scm]: physical selection, snapshot admission, and correction.
* [https://github.com/launix-de/memcp/blob/b2238766400c2c5b20d155a328d0ed380a7414ff/tests/performance/domain-snapshot.yaml domain-snapshot.yaml]: neutral correctness and performance cases.
* [https://github.com/launix-de/memcp/blob/b2238766400c2c5b20d155a328d0ed380a7414ff/tests/planner/aggregates/group-range-cache.yaml group-range-cache.yaml]: group/range regression cases.

Revision as of 19:31, 25 September 2026

Group Caches: Reuse Calculations Across Queries

A group cache remembers calculations over groups of database records. Once MemCP has worked out a group's total, a later query can reuse that work instead of reading and adding up the same records again.

For example, a dashboard might repeatedly ask for an order count, revenue over a selected interval, or the total quantity held by a set of accounts. These questions often share most of their work, even when the user changes a filter or moves an interval boundary. Group caches let MemCP reuse the parts that still apply.

The goal is to touch as few source records as possible to produce the answer. Sometimes the answer is already available. Sometimes it can be assembled from stored subtotals. Sometimes only a small set of contributions needs to be recalculated.

What does the cache remember?

Think of a group cache as an automatically managed table of reusable calculations. Its keys describe which records and parameter values a calculation belongs to; its values hold totals or the information needed to update those totals.

The cache survives the SQL request that created it. Compatible calculations in later requests can use it too. It remembers intermediate results, so reuse does not require the entire SQL query to be identical. A query-plan cache, by comparison, remembers how to execute a query; a group cache remembers work already done.

The planner chooses whether to use a group cache. Applications continue to issue SQL and do not have to create or refresh these tables themselves.

Three ways to reuse a calculation

The three dimension types describe how a calculation depends on its inputs. A dimension can be a customer ID, a price, a position, a revision number, or a timestamp. The mechanism is not specific to time.

Dimension Question How the cache helps
Equality / point What is the total for this customer? Look up that customer's stored result.
Additive range What is the total inside this interval? Combine stored subtotals for disjoint parts of the interval.
Snapshot What is the total when evaluated at this parameter value? Reuse a stored result and correct the contributions that may differ.

Equality: calculate a group once

SELECT SUM(amount)
FROM orders
WHERE customer_id = :customer;

After calculating the total for customer 42, MemCP can retain it under that key. Another request for customer 42 can read the result without adding up all of that customer's orders again.

The same idea applies to counts and other supported aggregates, to several keys together, and to aggregates used inside larger queries. A total over the whole table is simply a calculation with no varying group key.

This is useful when many outer rows or repeated requests ask for the same grouped calculation. The group and its filters must describe the same computation; unrelated totals are not interchangeable.

Additive ranges: assemble a total from smaller totals

SELECT SUM(revenue)
FROM sales
WHERE position >= :start AND position < :end;

Suppose the cache already knows the subtotals for positions [0, 100) and [100, 200). It can answer [0, 200) by adding those two numbers. It does not need to visit every sale again.

For a different interval, MemCP can reuse matching parts and calculate the missing parts. It splits coverage where necessary so overlapping requests do not cause records to be counted twice.

This works when a record's contribution is fixed and the interval only decides whether to include it. Examples include revenue over reporting windows, quantities within a position range, and cumulative counts. Each supported aggregate needs a valid way to combine its partial results.

Snapshots: update the answer when contributions change

Some questions cannot be answered by adding up all records in an interval. Consider an account's history:

Revision Recorded quantity
10 3
20 5

At revision 10, the account contributes 3. At revision 20, it contributes 5. Its contribution to the total has increased by 2. Adding both history records would give the wrong answer.

A typical SQL calculation selects one state for each account:

SELECT SUM((
    SELECT h.quantity
    FROM account_history h
    WHERE h.account_id = a.id AND h.revision <= :revision
    ORDER BY h.revision DESC
    LIMIT 1
))
FROM accounts a;

Here, each account has at most one history entry per revision.

A snapshot stores the total at a particular parameter value, together with the individual contributions needed to correct it. To answer at another value, MemCP finds the accounts whose contributions might differ and recalculates those accounts:

new total = stored total
          - previous contributions of affected accounts
          + new contributions of affected accounts

If 100 out of 100,000 accounts might differ, the other 99,900 contributions can remain untouched. An exact repeat can reuse the stored total directly.

A snapshot belongs to a point. The interval between two points helps locate possible changes. This is why a snapshot is a third dimension type, rather than another name for an additive range.

Snapshot reuse also covers supported conditions on the selected state. An account may start or stop qualifying, or contribute a different amount. A threshold crossing can matter even without a new history entry. MemCP must account for every way the contribution could change before it can safely leave the other accounts untouched.

This makes the pattern useful for versioned quantities, qualifying contracts, configuration states, and series of totals at successive parameter values.

The dimensions can be combined

These are three dimensions of the same group-cache mechanism. A single cache table can combine equality, range, and snapshot components and hold several aggregate calculations.

For example, a report might ask:

For tenant 7, consider accounts opened within a selected interval, and total their quantities as evaluated at revision 200.

The tenant is an equality key. The opening interval is an additive range if it only selects which accounts participate. The revision is a snapshot point that determines each account's contribution. The cache can reuse results within that tenant, combine disjoint account intervals, and correct contributions when the revision moves.

The planner checks that these roles really are independent enough to combine. A bound that also changes a record's contribution cannot simply be treated as an additive filter.

How does the cache become useful?

A cache has a construction cost. MemCP compares caching with direct execution; it does not need to build an expensive cache for every eligible query.

For candidates subject to deferred admission, it initially executes the calculation directly and records the work spent. Once enough work has accumulated to justify materialization, the shared cache path can take over. An already calculated scalar result can be retained without calculating it again. A snapshot needs additional contribution information, so preparing it involves more work than saving a single number.

After preparation:

  • A repeated key or snapshot point can read retained results.
  • An overlapping range can reuse existing partial results.
  • A new snapshot point can reuse an anchor and correct affected contributions. The current implementation compares neighboring anchors on either side using the number of potentially affected records, rather than numerical distance alone.
  • A large correction can trigger a fresh snapshot instead. Future requests can then reuse that new starting point.

Changing a query parameter is different from changing the underlying data. Inserts, updates, deletes, and schema changes must be reflected in dependency checks or invalidation. Reuse must also respect transaction visibility. Cached state can be discarded and rebuilt; it is not a durable replacement for the source data. Memory pressure can evict it as well.

How much faster can it be?

The gain comes from avoiding source-record processing. There is no fixed speedup for a dimension type.

Example Work avoided after preparation
Repeated total for a large customer group Replace another aggregation over thousands of orders with a stored-result lookup. Interval covered by 100 cached subtotals Combine 100 results instead of reading, for example, a million sales. 100 affected accounts out of 100,000 Re-evaluate roughly 1,000 times fewer account contributions, plus the work needed to find them. Series of nearby snapshot points Replace repeated full evaluations with a build followed by corrections where possible.

These examples describe reductions in work, not guaranteed latency factors. Finding affected records, preparing plans, executing other query parts, and returning results still take time. An exact repeat can be very cheap, while a moving parameter usually requires some additional work. Small inputs or constantly changing data may offer little benefit.

A useful performance comparison includes one cold request and a fixed number of follow-up requests. This includes construction costs and shows whether reuse pays off. Testing moving parameters is essential: repeating an identical request alone does not measure snapshot correction.

Which queries are covered today?

Equality grouping and supported additive range aggregates cover conventional grouped totals and repeated interval calculations. Snapshot recognition adds supported sums over uniquely identified records whose contribution depends on an ordered parameter, including ordered state selection and certain combinations of filters, conditional values, and nested lookups.

The snapshot correction path currently focuses on exact integer-valued sums within its numerical safety limits and a supported record identity. It is not a general incremental engine for every SQL aggregate or join. Arbitrary floating-point sums, distinct counts, and arbitrary combinations of moving dimensions are not automatically eligible. If safe reuse cannot be established, the query retains an ordinary execution plan.

For executable examples using generated data, see snapshot workloads and combined dimension tests. The unified implementation is described in PR #999.