Skip to content

perf(profiles): ⚡️ speed up SPARQL target queries on medium/large crates - #191

Open
kikkomep wants to merge 2 commits into
crs4:developfrom
kikkomep:perf/optimize-sparql-queries
Open

perf(profiles): ⚡️ speed up SPARQL target queries on medium/large crates#191
kikkomep wants to merge 2 commits into
crs4:developfrom
kikkomep:perf/optimize-sparql-queries

Conversation

@kikkomep

@kikkomep kikkomep commented Aug 6, 2026

Copy link
Copy Markdown
Member

On medium and large RO-Crates — anything with a few hundred data entities or more — validation time grew far faster than the size of the crate. The cost was concentrated in a handful of sh:select queries in the RO-Crate profile shapes, whose cost is quadratic in the number of entities.

Two distinct pathologies, both of which only become visible once a crate is large enough:

  1. Cartesian products in target queries. Several sh:SPARQLTarget shapes combined two high-cardinality patterns sharing no variables — ?this a <Class> and ?metadatafile schema:about ?root — related only by the FILTERs applied afterwards. rdflib materialises the full product before filtering, so the number of intermediate rows grows as (entities of that class) × (schema:about triples).

  2. Filters applied per triple instead of per entity. The ro-crate:ContextualEntityDefinition target matched every triple with ?this ?p ?o, then ran 6 FILTER NOT EXISTS and 17 STRSTARTS exclusions on each match. SELECT DISTINCT collapsed the duplicates only afterwards — too late to save any work, so the filters ran once per triple rather than once per entity.

Both scale with crate size, so small crates and the test fixtures were never slow enough for this to surface.

What changes

Both are fixed with the same idea: materialise the small side first in a SELECT DISTINCT subquery, which SPARQL evaluates before the rest of the query.

  • 14 queries across 8 files pre-compute ?root (and ?metadatafile where the outer FILTERs need it in scope) instead of joining against every schema:about triple.
  • The contextual entity target materialises the distinct subjects first, so the exclusion filters run once per entity. The exclusion filters themselves are untouched.

No changes to the validator code, the ontologies, or the inference settings — this is entirely in the profile shapes.

The `sh:select` queries of several `sh:SPARQLTarget` shapes
combined two high-cardinality patterns sharing no variables — `?this a <Class>` and `?metadatafile schema:about ?root` — related only by the FILTERs applied afterwards. rdflib materialises the full cartesian product before filtering.

Pre-compute the metadata descriptor in a `SELECT DISTINCT` subquery, which SPARQL evaluates and materialises first, so the outer pattern joins against an already-known `?root` instead of building the product.
The `ro-crate:ContextualEntityDefinition` target matched every
triple with `?this ?p ?o` and then applied 6 `FILTER NOT EXISTS`
plus 17 `STRSTARTS` exclusions to each match. The `SELECT DISTINCT`
collapsed the duplicates only afterwards, so the filters ran once
per triple rather than once per entity.
@fbacall

fbacall commented Aug 6, 2026

Copy link
Copy Markdown

Can confirm it is much faster at validating an RO-Crate with 3000+ entities following this change.

Before:

real	3m4.916s
user	3m4.608s
sys 	0m0.297s

After:

real	0m11.610s
user	0m11.473s
sys 	0m0.137s

@elichad elichad 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 @kikkomep !

For info, another way we have approached this (no idea if it's better or worse performance-wise) is to make use of sh:order to enable classes like ro-crate:RootDataEntity to be used in SPARQL targets directly. Example: https://github.com/eScienceLab/rocrate-validator/blob/ddf10285cb1df2effde1032be0939eb11870db5d/rocrate_validator/profiles/five-safes-crate/0_workflow_run_inference.ttl#L22-L47

Comment on lines +29 to +32
{ SELECT DISTINCT ?root WHERE {
?metadatafile schema:about ?root .
FILTER(contains(str(?metadatafile), "ro-crate-metadata.json"))
} }

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.

I don't see ?metadatafile or ?root referenced elsewhere in this target - is this part necessary?

Comment on lines +53 to +56
{ SELECT DISTINCT ?root WHERE {
?metadatafile schema:about ?root .
FILTER(contains(str(?metadatafile), "ro-crate-metadata.json"))
} }

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.

same as above - I don't see ?metadatafile or ?root referenced elsewhere in this target - is this part necessary?

Comment on lines +85 to +88
{ SELECT DISTINCT ?root WHERE {
?metadatafile schema:about ?root .
FILTER(contains(str(?metadatafile), "ro-crate-metadata.json"))
} }

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.

same as above - I don't see ?metadatafile or ?root referenced elsewhere in this target - is this part necessary?

Comment on lines +50 to +53
{ SELECT DISTINCT ?root WHERE {
?metadatafile schema:about ?root .
FILTER(contains(str(?metadatafile), "ro-crate-metadata.json"))
} }

@elichad elichad Aug 7, 2026

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.

It would be really useful to document this as a recommended pattern for profile developers when they need to fetch the root in a query

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.

Also: the wording of this PR is very jargon-heavy, and I found it difficult to parse until I read the code myself ("Cartesian products in target queries" in particular meant nothing to me). It would help in future if you could describe the solution in simpler language, to make it easier for others to review the code, understand why it works, and (in my case) understand what further changes might be needed downstream.

@elichad

elichad commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Another thought - it'd be good to add a regression test for this fix, e.g. validate a large crate and ensure it doesn't take too long

@floWetzels

Copy link
Copy Markdown
Contributor

In the ISA profile, we tried to avoid SPARQL targets completely, here's an example how we did this: https://github.com/crs4/rocrate-validator/blob/develop/rocrate_validator/profiles/isa-ro-crate/1_study.ttl#L26
I have not tested this regarding performance though, @elichad said that I should mention this here. Maybe that's an alternative approach.

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.

4 participants