Skip to content

See how a query ran

View Markdown

A slow query has a cause. It reads too many blocks, or the planner joins its patterns in a poor order, or it waits for the snapshot. A query profile shows which one. The examples use the support graph from Query with SPARQL & validate.

Set profile: true on POST /v1/query/sparql-text or POST /v1/query/sparql. The answer gains a profile object. It describes this one request.

const query = `
PREFIX dclass: <https://littlebigbrain.com/class/>
PREFIX dr: <https://littlebigbrain.com/r/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?ticket ?release WHERE {
?t a dclass:ticket ; rdfs:label ?ticket ; dr:reported_as ?b .
?b dr:fixed_in ?r .
?r rdfs:label ?release .
}`;
const { rows, profile } = await lbb.query.sparql({ query }, { profile: true });
console.log(rows);
console.log(profile?.total_ms, profile?.stages);
for (const order of profile?.join_orders ?? []) {
for (const step of order.steps) console.log(step.estimate, step.pattern);
}

A profiled request does not use the result cache. Its profile always describes an execution, and result_cache reads bypassed. Without profile, the answer is the same as before, byte for byte.

Times are milliseconds.

Field What it tells you
total_ms The whole request inside the server, snapshot included.
execution_ms The query engine alone.
stages Time per stage, in order: resolve_snapshot, parse_rewrite, execute, serialize.
rows, rows_total Rows in this answer, and rows before offset and limit.
reads Blocks and bytes the plan read, and the hits, misses and bytes of the request’s block cache. A miss reads from the node’s caches or from storage.
join_orders The order the planner chose for each group of patterns, with the estimate it compared for each pattern.
operators Rows in and out per operator, such as filter and distinct.
plans The specialized plans that ran, such as answers from statistics.
trigram For a text FILTER: used, or the reason the text index was not used.

A large resolve_snapshot time means the server waited for the graph’s snapshot, before any query work started. A large execute time with many reads.blocks means the plan read a lot of data. In join_orders, the pattern with the smallest estimate runs first, and each later estimate counts the variables already bound. The estimates come from the planner statistics below. Compare them with what you know about the data.

The join-order record keeps at most 64 orders of 64 patterns each. Literals are cut at 64 characters, and each pattern at 512.

For a large set of linked entities, select a small membership page in a subquery before joining labels and types. An outer LIMIT after grouping or sorting limits the answer size, but can still require reading every member. The engine can stop early for a limited, explicitly bounded set joined to one triple pattern with a known predicate and endpoint. Ordering, grouping, DISTINCT and OPTIONAL keep their normal SPARQL semantics.

The console’s linked-group view uses this approach. It loads six members, then 25 more per click, with continuations pinned to the first page’s snapshot. Its relationship preview samples a bounded number of members and links, shows the sample size, and marks truncated counts as lower bounds. A failed preview can be retried while already loaded members remain visible. Member pages are sorted within each page; they are not a global alphabetical list.

GET /v1/graph/planner-stats returns the statistics the planner uses for the graph’s published data. It reads stored metadata and does not scan the graph.

const stats = await lbb.graph("support").plannerStats({ limit: 50 });
console.log(stats.triple_count, stats.served_at_seq);
for (const p of stats.predicates) {
console.log(p.iri, p.triples, p.distinct_subjects, p.distinct_objects);
}

predicates lists each link and field with its triple count, largest first. Pass next_cursor as cursor to read the next page. limit is 1 to 500, 200 by default. A cursor belongs to one published version of the data. After a new version is published, it returns 400 planner-stats cursor expired. Start again without a cursor. The answer also reports the count histograms, the pair-count statistics, the text index for labels, and the value-order index. A graph with no published data answers served_at_seq: null and empty lists.

NOW() returns the time of the request, one value for the whole query. RAND(), UUID() and STRUUID() draw new values on each request. The server never caches an answer to a query that uses them.