8000 Recursive rule query 50–10,000× slower since #700: delta run hash-joins a whole attribute (v3) · Issue #721 · project-minigraf/minigraf · GitHub
[go: up one dir, main page]

Skip to content
7FFF

Recursive rule query 50–10,000× slower since #700: delta run hash-joins a whole attribute (v3) #721

Closed

Description

@adityamukho

Found while measuring #551 PR 3 (#394 envelope, bench machine). v3 only: main has no streaming engine.

Regression

recursive_ancestors, (boss :p<leaf> ?b) over the envelope's :manager tree (5–7 rows through the magic-set rewrite), with boss the usual two-rule closure:

100K 1M 10M
#548 PR 3 (2ee149a2), docs/BENCHMARKS.md 1.45 ms 2.14 ms 3.04 ms
v3 today (1096e6a2 and after) 101 ms 1.8 s 35 s

Bisected with a probe that opens the 100K fixture read-only and times the query (p50 of 12, one process per commit, each commit built in its own target directory): 05352121 2.2 ms, 4413d711 (#700, the #694 fix) 101 ms. Every commit from 2ee149a2 to 05352121 is 1.9–3.0 ms.

Cause

After the magic-set rewrite the recursive rule's body is (__magic_boss_bf ?x) [?x :manager ?y] (boss ?y ?z). The run from the boss delta binds ?y ?z first. optimizer::connect (#700) then takes the next 8000 pattern that shares a variable with them, [?x :manager ?y], which shares only its value. That join is a HashJoin { build_inner: true } holding every :manager fact, rebuilt in every round: about 11 ms per round at 100K, whether or not the delta has rows. The magic relation (one row per round here) is probed after it. Before #700 the run joined the magic relation first and probed :manager by entity.

Instrumented plan of that run (round 2):

Join { inner: Scan [?x :manager ?y], kind: Hash { build_inner: true }, keys: [v = ?y] }
Join { inner: Scan [?x :__magic_boss_bf _] (derived, Old), kind: Probe(?x) }

#694's own case (recursion/fanout, (reach :n0 ?to) with body (magic ?from) [?from :next ?mid] (reach ?mid ?to)) is the mirror image: there the shared variable is the pattern's entity side of the next join, so connect makes it a probe. In the ancestors shape it is the value side, which the engine cannot probe (no AVET over pending facts, #545 plan decision 3), so "shares a variable" is not "joins cheaply".

Fix direction (to settle)

connect should prefer a next pattern whose shared variable lets the engine probe (entity bound) over one that only shares a value. When neither is available, a keyless join on a derived relation (a magic set, usually tiny) may still beat a hash join over a whole stored attribute. A Criterion case or envelope check for the ancestors shape, next to recursion/fanout, keeps both directions covered.

Activity

  1. added this to the v3.0.0 milestone on Oct 23, 2026
  2. added
    bugSomething isn't working
    performanceBenchmark performance regressions
    on Oct 23, 2026
  3. adityamukho commented on Oct 23, 2026

    @adityamukho
    CollaboratorAuthor

    Fixed on v3 by #722.

  4. added a commit that references this issue on Oct 23, 2026
  5. added a commit that references this issue on Oct 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingperformanceBenchmark performance regressions

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      0