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_children — hasChildren ⟺ 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_children — roots(: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.
Summary
GET /ontologies/{ontology}/classes/{cls}/treeissues a separate SPARQL round-trip per node to computehasChildren. For large or flat ontologies this is hundreds to thousands ofLIMIT 1queries per request, dominating response time.Root cause
hasChildrenis resolved one node at a time viaClass#load_has_children, which fires aLIMIT 1has_children_queryper node (lib/ontologies_linked_data/models/class.rb). It's invoked across the whole visible tree from two places:OntologySubmission#roots— computeshasChildrenper root in thedelete_ifloop. For flat ontologiesrootsreturns up toFLAT_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— callsload_computed_attributes(→load_has_children) for every child of every node on the path-to-root.hasChildrenis exactlychildrenCount > 0(#children,#parents, and thehasChildrenquery all use the sametree_view_property), and thechildrenCountaggregate is already computed in batch bypartially_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 (childrenCountaggregate or loadedchildren) and falling back to a single grouped count query for the rest. The existing per-nodeload_has_childrencalls then become no-ops (they short-circuit when@intlHasChildrenis 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) androots(gated on:hasChildren).Impact (measured, BRO, 29-node tree)
LIMIT 1has-children queries−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_children—hasChildren ⟺ childrenCounton every OWL tree nodetest_skos_class_tree— first end-to-endClass#treecoverage of the SKOS branchtest_submission_root_classes_has_children—roots(:hasChildren)keepsprefLabelintact and stays consistentFollow-ups (separate)
treeto skip the bulk roots load.traverse_path_to_rootto replace the per-ancestorbring(parents:)query.