Skip to content

Reduce mutations-over-time query fan-out using unionAll #1835

Description

@fhennig

Background

The LAPIS /component/nucleotideMutationsOverTime (and aminoAcidMutationsOverTime) endpointscurrently fan out 2M queries to RhyDB for each request, where M is the number of mutations being tracked:

  • one query per mutation for count (sequences carrying that mutation, grouped by date)
  • one query per mutation for coverage (sequences with a non-N symbol at that position, grouped by date)

These are parallelised via a thread pool in LAPIS, but the O(M) request count means network and per-request overhead scales linearly with the number of mutations. For a typical request tracking 50 mutations this is 100 round-trips between LAPIS and RhyDB.

Proposed approach

Both query types can be collapsed into 2 RhyDB requests total (one for counts, one for coverage) regardless of M, using unionAll with nucleotideEquals filters:

Count query:

unionAll(
  default
    .filter(baseFilter && nucleotideEquals(position:=44, symbol:='T'))
    .map({mutation := '44T'})
    .groupBy({count:=count()}, {date, mutation}),
  default
    .filter(baseFilter && nucleotideEquals(position:=774, symbol:='T'))
    .map({mutation := '774T'})
    .groupBy({count:=count()}, {date, mutation}),
  ...
)

Coverage query (same shape, using hasMutation or equivalent non-N filter per position).

Each branch still uses the per-position roaring bitmap index via nucleotideEquals, so the per-mutation cost is unchanged. Crucially, RhyDB executes unionAll branches in parallel through Arrow Acero's ExecPlan (both children are added to the same plan via MakeExecNode("union", ...)) <80><94> so the server-side parallelism is preserved without LAPIS having to manage a thread pool for it.

Expected benefits

  • O(1) round-trips instead of O(M): 2 requests regardless of how many mutations are tracked
  • Same server-side parallelism: Arrow Acero parallelises unionAll branches within a single execution plan
  • Simpler LAPIS code: no thread pool management, no data-version consistency retries across many concurrent requests
  • Better fit for direct RhyDB access: if clients ever query RhyDB directly (e.g. via the WASM build), they can issue 2 requests rather than needing to replicate LAPIS's fan-out logic

Notes

  • There is currently no query text size limit in RhyDB (query_handler.cpp line 52, see TODO #1244), so large unionAll trees are accepted. If a size limit is introduced, batching into groups of N mutations per request remains an option while still giving O(M/N) instead of O(M).
  • The coverage predicate needs clarification: hasMutation excludes both N and the reference symbol, so it undercounts coverage. The right predicate is "symbol is not N", which may need a dedicated filter or a nucleotideEquals(symbol:='N') negation <80><94> worth verifying against the current coverage definition in LAPIS before implementing.
  • Amino acid mutations follow the same pattern with aminoAcidEquals / hasAAMutation.

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions