# See how a query ran

Source: https://docs.littlebigbrain.com/guides/query-profiles/

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](https://docs.littlebigbrain.com/guides/sparql-and-shacl/).

## Profile a query

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.

### TypeScript

```ts
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);
}
```

### Python

```python
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 .
}"""

results = lbb.sparql(query, profile=True)
print(results.rows())
print(results.profile["total_ms"], results.profile["stages"])
for order in results.profile["join_orders"]:
    for step in order["steps"]:
        print(step["estimate"], step["pattern"])
```

### cURL

```sh
curl --fail-with-body -sS -X POST "$LBB_URL/v1/query/sparql-text?graph=support" \
  -H "Authorization: Bearer $LBB_API_KEY" -H "Content-Type: application/json" \
  -d '{"query": "SELECT ?r WHERE { ?t <https://littlebigbrain.com/r/reported_as> ?b . ?b <https://littlebigbrain.com/r/fixed_in> ?r }", "profile": true}' \
  | jq '.profile | {total_ms, stages, reads, join_orders}'
```

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.

## Read a profile

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.

## Bound previews before adding details

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.

## Read the planner statistics

`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.

### TypeScript

```ts
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);
}
```

### Python

```python
stats = lbb.planner_stats(limit=50)
print(stats["triple_count"], stats["served_at_seq"])
for p in stats["predicates"]:
    print(p["iri"], p["triples"], p["distinct_subjects"], p["distinct_objects"])
```

### cURL

```sh
curl --fail-with-body -sS "$LBB_URL/v1/graph/planner-stats?graph=support&limit=50" \
  -H "Authorization: Bearer $LBB_API_KEY" | jq '{triple_count, predicates}'
```

`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.

## Time and random functions

`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.
