Skip to main content

Query Plans

Understand how Grafio executes your Cypher queries.

Setup

import { InMemoryGraphFactory } from 'grafio';
import { CypherEngine } from 'grafio/cypher';

const factory = new InMemoryGraphFactory();
const graph = factory.forGraph('default');
const engine = new CypherEngine(graph);

Query Plan vs Execution Plan

Plan TypePurposeWhen to Use
Query PlanLogical execution strategyDebugging query performance
Execution PlanRuntime statisticsPerformance tuning

Query Plans

Get the logical query plan without executing.

Text Tree Format

const plan = await engine.getQueryPlan(
'MATCH (p:Person)-[:KNOWS]->(b) RETURN p.name, b.name',
undefined,
'text'
);
console.log(plan);

Output:

├— ProjectStep [p.name, b.name]
├— EdgeExpandStep (→) r:KNOWS → b:Person
└— NodeScanStep (p:Person)

JSON Format

const plan = await engine.getQueryPlan(
'MATCH (p:Person) RETURN p.name',
undefined,
'json'
);
console.log(JSON.stringify(plan, null, 2));

Mermaid Flowchart

const plan = await engine.getQueryPlan(
'MATCH (p:Person)-[:KNOWS]->(b) RETURN p.name, b.name',
undefined,
'mermaid'
);
console.log(plan);

Execution Plans

Get runtime statistics with timing and row counts.

const result = await engine.execute(
'MATCH (p:Person) RETURN p.name AS name',
{},
{ executionPlan: { format: 'text' } }
);
console.log(result.executionPlan);

Output:

├— ProjectStep [name] (1ms, 3.7%, 100 rows)
└— NodeScanStep (p:Person) (25ms, 96.3%, 100 rows)

Available Formats

// Text tree with statistics
{ executionPlan: { format: 'text' } }

// JSON with full statistics
{ executionPlan: { format: 'json' } }

// Mermaid flowchart
{ executionPlan: { format: 'mermaid' } }

Example: Complex Query

For a query like:

MATCH (p:Person)-[r1:BOUGHT]->(t:Product)-[r2:IN_CATEGORY]->(c:Category)
WHERE r1.weight > 5 AND r1.weight < r2.weight AND p.score > 50
RETURN p.label AS personLabel,
t.label AS productLabel,
c.label AS categoryLabel,
p.score AS personScore,
avg(t.score) AS avgTargetScore,
sum(r1.weight) AS totalWeight,
count(r1) AS relationshipCount
ORDER BY personScore DESC

Query Plan

├— NodeScanStep (p:Person { score > 50 })
├— EdgeExpandStep (→) r1:BOUGHT { weight > 5 } → t:Product
├— EdgeExpandStep (→) r2:IN_CATEGORY → c:Category
├— FilterStep r1.weight < r2.weight
├— AggregateStep [AVG(avgTargetScore), SUM(totalWeight), COUNT(relationshipCount)]
├— SortStep [personScore DESC]
└— ProjectStep [personLabel, productLabel, categoryLabel, personScore, avgTargetScore, totalWeight, relationshipCount]

Execution Plan

├— NodeScanStep (p:Person { score > 50 }) (3ms, 4.3%, 1003 rows)
├— EdgeExpandStep (→) r1:BOUGHT { weight > 5 } → t:Product (57ms, 82.6%, 122 rows)
├— EdgeExpandStep (→) r2:IN_CATEGORY → c:Category (7ms, 10.1%, 14 rows)
├— FilterStep r1.weight < r2.weight (1ms, 1.4%, 7 rows)
├— AggregateStep [AVG(avgTargetScore), SUM(totalWeight), COUNT(relationshipCount)] (0ms, 0.0%, 7 rows)
├— SortStep [personScore DESC] (0ms, 0.0%, 7 rows)
└— ProjectStep [personLabel, productLabel, categoryLabel, personScore, avgTargetScore, totalWeight, relationshipCount] (1ms, 1.4%, 7 rows)

Performance Tips

Use Indexes

-- Without index: Full scan
MATCH (p:Person {name: 'Alice'}) RETURN p

-- With index: Fast lookup
CREATE INDEX name_idx FOR (n:Person) ON (n.name)
MATCH (p:Person {name: 'Alice'}) RETURN p

Limit Results Early

-- Limit at the end
MATCH (p:Person) RETURN p.name LIMIT 10

-- Combine with filter
MATCH (p:Person {city: 'NYC'}) RETURN p.name LIMIT 10

Use CypherEngineOptions.maxDegreeOfParallelism

const engine = new CypherEngine(graph, { maxDegreeOfParallelism: 8 })

Next Steps