How it works

A SQL-to-SQL compiler for incremental computation.

OpenIVM does not replace your engine. It reads your view definition and emits the SQL that keeps it up to date, so the same approach carries from DuckDB to Spark and beyond.

  1. 01 · Define

    Write a normal view

    CREATE MATERIALIZED VIEW with standard SQL: joins, aggregates, CTEs, subqueries.

  2. 02 · Compile

    SQL → incremental SQL

    The logical plan is classified operator by operator and rewritten into delta propagation rules. The output is plain SQL.

  3. 03 · Capture

    Track changes as deltas

    An optimizer rule intercepts DML on base tables and writes signed rows to openivm_delta_<table>, or reads DuckLake snapshots.

  4. 04 · Refresh

    Merge only the change

    On demand or on a schedule, consolidated deltas are pushed through the compiled SQL and merged into the view. Pipelines cascade.

The model

IVM in one equation.

Given a table T and a view V = Q(T), find a functionf so that f(ΔT) = ΔV: compute the change to the view from the change to the table alone, instead of re-running Q.

OpenIVM follows DBSP, which gives such an f for arbitrary relational queries using two operators: differentiation D (ΔT = T′ −T) and integration I (T + ΔT = T′). Each operator is replaced by its incremental form, and the plan runs over deltas.

V = π(σ(T))⟶ΔV = π*(σ*(ΔT))

Selection and projection are their own incremental form; a join becomes three joins.

Z-sets: every row carries an integer weight, and Rnew = Rold + ΔR

V{ apple → 5, banana → 2 }
ΔV{ apple → −3, banana → +1 }
V + ΔV{ apple → 2, banana → 3 }

…and every algebraic step is plain SQL

additionUNION ALL+ grouped weight sums
multiplicationJOINweights multiply
integrationMERGE/ INSERT, DELETE

Delta tables

Every change is a weighted row.

A delta table has the same columns as its source, plus a signed weight. An INSERT writes+1, a DELETE writes −1, and anUPDATE writes both (old row −1, new row +1) in one atomic insert. Operators then work on these weights algebraically: aggregates sum weight × value, joins multiply weights.

Several views can share one delta table; each keeps its own cursor and reads only what it has not yet processed. Over DuckLake, there is no delta table at all: changes are derived from table snapshots, and the pre-refresh state is read through time travel.

SELECT * FROM openivm_delta_sales;

regionproductamountmultiplicity
USBolt50+1
JPGear300+1
EUGadget200−1

After two inserts and one delete. Timestamps omitted. Captured from the DuckDB extension.

Architecture

A real engine underneath.

Most IVM compilers are detached prototypes that talk to a database through drivers. OpenIVM lives inside one: DuckDB parses, binds and plans the view, so the compiler gets schema validation, cardinality estimates and a cost model for free, and physical choices such as indexes or what to materialize stay tunable.

OpenIVM inside DuckDBA CREATE MATERIALIZED VIEW statement enters DuckDB's parser. OpenIVM's fallback parser strips the materialized-view options and stores metadata. After planning, OpenIVM's optimizer rules rebind scans to delta tables and apply delta rules. The incremental plan either runs natively in DuckDB or is compiled back to SQL for other engines such as PostgreSQL or Spark.DuckDBOpenIVM extensionCREATE MATERIALIZEDVIEW v AS …parserplanneroptimizerexecutorfallback parserstrips MV options,stores view metadataIVM optimizer rulesdelta model, lineage,operator rewritingLPTSplan → AST → CTEs→ target dialectrefresh SQLDuckDB · PostgreSQLSpark (soon) · …MV keywordsclean SQLlogical planincremental plannativeplan
OpenIVM is a DuckDB extension: it plugs into the parser and the optimizer without modifying the core engine, then compiles the incremental plan back to SQL through LPTS. Adapted from the SIGMOD 2024 demo paper.
  1. Normalize. Intercept CREATE MATERIALIZED VIEW, take the bound logical plan, and rewrite equivalent shapes into forms the delta rules handle.
  2. Model. Build a delta model: each operator's rule kind, the state it needs, and its lineage, a mapping from an affected output key, group or window partition back to the source columns that can change it.
  3. Rewrite. Turn source scans into deltas and propagate them operator by operator, restricting any recomputation to the exact domain lineage says was touched.
  4. Emit. LPTS (Logical Plan to SQL) turns the rewritten plan into a dialect-independent AST, flattens it into an ordered CTE program, and renders it for the target engine. OpenIVM decides what to compute; LPTS decides how to say it.

Generated SQL

What a refresh actually runs.

One view definition becomes several SQL statements: create the deltas, compute ΔV from ΔT, fold ΔV into V and drop rows whose count reached zero, then clear the consumed deltas. Below is what OpenIVM compiles for regional_totals, captured withSET openivm_files_path and lightly reformatted (catalog prefixes and literal timestamps elided). It is ordinary SQL, which is what makes the approach portable.

A grouped aggregate over one table. This is all you write.

CREATE MATERIALIZED VIEW regional_totals AS
    SELECT region, SUM(amount) AS total, COUNT(*) AS cnt
    FROM sales
    GROUP BY region;

Delta rules

Classified by linearity, not pattern-matched.

Each node of the plan carries a rule kind that determines the shape of its delta and what state it needs. It is the same taxonomy DBSP uses (Budiu et al., VLDB 2023), and it tells you, before the first refresh, what a view will cost to maintain.

LINEARΔ(Q(R)) = Q(ΔR)

scan, projection, filter, UNION ALL, SUM, COUNT

proportional to |Δ|, no extra state

BILINEARΔ(R ⋈ S) = ΔR ⋈ S + R ⋈ ΔS − ΔR ⋈ ΔS

INNER, CROSS, LEFT, RIGHT, FULL OUTER JOIN

2ᴺ − 1 terms for N tables (or N with telescoping)

NON_LINEARneeds accumulated state

DISTINCT, SEMI / ANTI JOIN, MIN / MAX with deletes, window functions

aux state, or recompute of affected groups / partitions

FULL_ONLYno delta rule

anything else

detected at CREATE time; full refresh

Two ways to differentiate a join

Inclusion–exclusion, over current state

Δ(R ⋈ S) = ΔR ⋈ Snew + Rnew ⋈ ΔS − ΔR ⋈ ΔS

By refresh time the base tables already contain their deltas. Joining each delta against the current state counts the cross-term twice, so it is subtracted once: a Möbius sign(−1)^(k−1) × ∏ wᵢ for a term using k deltas, giving 2ᴺ − 1 terms for Ntables.

Telescoping, with time travel

Δ(⋈ᵢ Rᵢ) = Σᵢ (⋈j<i Rjnew) ⋈ ΔRᵢ ⋈ (⋈j>i Rjold)

When the old state is readable, for example through DuckLake snapshots, each combination of changes lands in exactly one term: N joins instead of 2ᴺ − 1. The same rule covers inner, cross and theta joins.

Outer joins and negation use match counts: a NULL-extended row is retracted when its match count goes from zero to non-zero, and reinstated when it drops back to zero. SEMI,ANTI, EXISTS and NOT EXISTS use the same zero-crossing transition.

Beyond the textbook

Data-dependent optimizations.

Correct delta rules are the floor. Because OpenIVM runs inside an engine, it can also look at the data and the change itself to skip work entirely.

Empty-delta skipping

πk(ΔR) ∩ πk(S) = ∅ ⇒ skip

No source changed? Skip the refresh. A filter rejects every delta row? Every downstream term is zero. New orders for customers the join never sees? That term is omitted even though the delta is non-empty.

Delta consolidation

ΔR̄(t) = Σu = t ΔR(u), keep ΔR̄(t) ≠ 0

Equal tuples are summed before anything else runs, so an insert and a delete of the same row, or an update and its revert, cancel out before they cost anything.

Insert-only fast paths

ΔR ≥ 0 ∧ Δ-rule ≥ 0 ⇒ append

When both the source change and the chosen delta rule are provably non-negative, projections append directly, zero-group deletion is skipped, and MIN/MAX useLEAST/GREATEST instead of rescanning groups.

Affected-domain recompute

A = πk(ΔT), N = σk ∈ A(V(Tnew))

For operators without a closed-form delta (MIN/MAX with deletes, windows), only the affected keys A are recomputed. A and N are materialized once and reused to delete, replace, and emit the downstream delta.

SQL-exact NULLs

countnon-NULL = 0 ⇒ SUM = NULL

Nullable aggregate inputs carry a hidden non-NULL count, so a group that loses its last value returnsNULL, not 0, and lookups use NULL-safe equality, exactly as SQL grouping does.

Window suffixes

min ord(ΔP) > max ord(Pold)

A cumulative window whose new rows all come after the old ones is extended from its last stored aggregate instead of recomputing the partition; late rows fall back to partition recompute.

Cross-system

Maintain views across engines.

Modern pipelines span several systems: transactions in one, analytics in another. Because the maintenance steps are SQL, rendered by LPTS in each engine's dialect, they can be shipped to wherever the data lives instead of copying the data to wherever the IVM engine lives.

Cross-system IVM between DuckDB and PostgreSQLA materialized view in DuckDB is defined over a table in an attached PostgreSQL instance. Updates to the PostgreSQL table are captured in a delta table there. OpenIVM sends compiled queries to PostgreSQL, the incremental result flows back into the view's delta in DuckDB, and is merged into the view.CREATE MATERIALIZEDVIEW my_view ASSELECT COUNT(*)FROM p.my_table;DuckDBanalytical enginemy_viewΔ my_viewPostgreSQL pattached transactional enginemy_tableΔ my_tablecompiled queriesincremental resultmergecaptureupdates
Because the output is SQL, the maintenance steps can run where the data lives. Here PostgreSQL takes the transactional writes and DuckDB keeps the analytical view fresh. Adapted from the SIGMOD 2024 demo paper.

Operator coverage

Any SQL in, the cheapest correct refresh out.

Views that cannot be maintained incrementally are detected at creation time. Withopenivm_refresh_mode = 'auto' they fall back to a full refresh.

OperatorStrategy
Projection, filter, expressionsincremental
GROUP BY · SUM, COUNT, AVG, STDDEV, VARIANCEincremental
MIN / MAXincremental (insert-only) · group recompute
HAVING, ungrouped aggregates, LISTincremental
INNER, CROSS, LEFT, RIGHT JOINincremental
FULL OUTER JOINincremental (merge + recompute)
SEMI / ANTI JOIN, EXISTS, NOT EXISTSaux-state incremental
UNION ALL, DISTINCTincremental
Window functionspartition-level recompute
CTEs, decorrelated & scalar subqueriesincremental when the lowered plan is
Anything elsefull refresh

Per-operator derivations live in the operator docs.

Try it

A view that only does the work that changed.

The view below is SELECT region, SUM(amount), COUNT(*) FROM sales GROUP BY region. Changes collect as signed deltas; a refresh folds them in. Insert then delete the same row and the deltas cancel out before any work is done.

Insert or delete a few rows, then refresh.

sales base table

regionamount

Δ sales pending deltas

±regionamount

No pending changes.

regional_totals materialized view

regiontotalcnt

Read the paper

OpenIVM: a SQL-to-SQL Compiler for Incremental Computations

Ilaria Battiston, Kriti Kathuria, Peter Boncz · SIGMOD Companion 2024