Skip to content

Optimize class-tree endpoint: eliminate per-node hasChildren N+1 queries #296

Description

@alexskr

Summary

GET /ontologies/{ontology}/classes/{cls}/tree issues a separate SPARQL round-trip per node to compute hasChildren. For large or flat ontologies this is hundreds to thousands of LIMIT 1 queries per request, dominating response time.

Root cause

hasChildren is resolved one node at a time via Class#load_has_children, which fires a LIMIT 1 has_children_query per node (lib/ontologies_linked_data/models/class.rb). It's invoked across the whole visible tree from two places:

  • OntologySubmission#roots — computes hasChildren per root in the delete_if loop. For flat ontologies roots returns up to FLAT_ROOTS_LIMIT (1000) classes → up to ~1000 queries just to set boolean flags, even when requesting a single class's tree.
  • Concerns::Concept::Tree#tree — calls load_computed_attributes (→ load_has_children) for every child of every node on the path-to-root.

hasChildren is exactly childrenCount > 0 (#children, #parents, and the hasChildren query all use the same tree_view_property), and the childrenCount aggregate is already computed in batch by partially_load_children. So the per-node boolean is redundant work.

Proposed fix

Add a batched Class.load_has_children_batch(models, submission) that pre-warms each instance's @intlHasChildren — deriving it from already-loaded data (childrenCount aggregate or loaded children) and falling back to a single grouped count query for the rest. The existing per-node load_has_children calls then become no-ops (they short-circuit when @intlHasChildren is set). Nodes the batch can't resolve fall through to the original per-node query, preserving behavior.

Wire it into tree (path nodes + their children) and roots (gated on :hasChildren).

Impact (measured, BRO, 29-node tree)

Total SPARQL LIMIT 1 has-children queries
Before 126 43
After 87 0

−31% queries on a small tree; the saving scales with tree size and especially with flat ontologies (eliminates the up-to-1000-query root storm).

Tests

  • test_bro_tree_has_childrenhasChildren ⟺ childrenCount on every OWL tree node
  • test_skos_class_tree — first end-to-end Class#tree coverage of the SKOS branch
  • test_submission_root_classes_has_childrenroots(:hasChildren) keeps prefLabel intact and stays consistent

Follow-ups (separate)

  • Flat-ontology fast path in tree to skip the bulk roots load.
  • Batched traverse_path_to_root to replace the per-ancestor bring(parents:) query.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions