Skip to content

Aggregate entity and relation usage counts in ontology views to avoid correlated table scans - #1088

Merged
WaylandYang merged 1 commit into
deeplethe:devfrom
plpycoin:perf/ontology-views-usage-subqueries
Oct 5, 2026
Merged

WaylandYang merged 1 commit into
deeplethe:devfrom
plpycoin:perf/ontology-views-usage-subqueries

Conversation

@plpycoin

@plpycoin plpycoin commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Why

In utopia-store::ontology::entity_type_views and relation_type_views, usage counts for each entity type and relation type were computed via correlated subqueries in the SELECT list:

  • (SELECT count(*) FROM entities e WHERE e.type_id = t.id AND e.merged_into IS NULL) AS usage
  • (SELECT count(*) FROM facts f WHERE f.predicate_id = r.id AND f.invalidated_at IS NULL) AS usage

On knowledge bases with large ontologies (e.g. imported vocabularies with thousands of classes) and tens/hundreds of thousands of entities and facts:

  1. entities has no single-column index on type_id, and omitting e.kb_id = t.kb_id prevents Postgres from utilizing entities_kb_type_name_idx. For every entity type in the KB, Postgres executes a sequential scan of the entire entities table. For a KB with ~9,700 classes and 89,000 entities, this leads to ~8.7x10^8 row inspections and takes ~70 seconds.
  2. Similarly, facts has no index with predicate_id as the leading column, leading to sequential scans of facts for each relation type (~10 seconds).
  3. In ontology_routes::get, these queries were executed sequentially, causing GET /api/v1/kbs/:kb_id/ontology to take 80+ seconds and easily hit reverse proxy timeouts (such as nginx 60s gateway timeouts).

What changes

  • Rewrite entity_type_views and relation_type_views in crates/utopia-store/src/ontology.rs to aggregate usage counts per knowledge base once (GROUP BY type_id / GROUP BY predicate_id) and LEFT JOIN the result onto entity_types and relation_types.
  • In crates/utopia-server/src/api/ontology_routes.rs, fetch entity types, relation types, and misses concurrently using tokio::try_join!.

Benchmarks (measured on KB with 9,740 classes and 372 relations):

  • entity_type_views query: ~70,139 ms -> 125 ms (~560x speedup)
  • relation_type_views query: ~10,170 ms -> 5.4 ms (~1880x speedup)
  • Overall GET /api/v1/kbs/:kb_id/ontology response: ~78 s -> 0.62 s (~125x speedup)

How it was checked

  • cargo fmt --all --check
  • cargo clippy -p utopia-store -p utopia-server --all-targets -- -D warnings
  • cargo test -p utopia-store (unit and store integration tests pass)
  • cargo test -p utopia-server ontology (18 ontology route tests pass)
  • Verified with live requests that payload format (entity_types, relation_types, usage counts) remains identical.

Before review

  • Every commit is signed off (git commit -s)
  • cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings and cargo test --workspace pass
  • SQL under crates/utopia-store/ was tested with UTOPIA_DATABASE_URL set (those tests skip without it)

…eries with LEFT JOIN aggregations and concurrent fetch

(cherry picked from commit eae3d45b46671e96456060fc92a07e1e09b9bee9)
Signed-off-by: Jun Du <dujun@tib.cas.cn>

@WaylandYang WaylandYang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @plpycoin. Measured on a database with the dev schema: one base with 3,000 classes, 90,000 entities, 300 properties and 200,000 facts, beside another base's 60,000 entities.

Usage counts Before After
Classes 7.9 s 9 ms
Properties 1.4 s 14 ms

Same counts on both sides. The correlated count had no index to seek with (entities and facts both lead their indexes with kb_id), so it scanned once per class; one grouped pass over the base's own rows is the right shape. Running the four reads of the ontology page together is fine too. Landing it.

@WaylandYang
WaylandYang merged commit 306e333 into deeplethe:dev Oct 5, 2026
6 checks passed
@WaylandYang WaylandYang mentioned this pull request Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants