Use when diagnosing and optimizing slow Neo4j queries in Chariot - systematic methodology for profiling, analyzing execution plans, creating indexes, and measuring performance improvements
Systematic methodology for diagnosing and optimizing Neo4j Cypher query performance in the Chariot platform.
Use this skill when:
You MUST use TodoWrite before starting to track all optimization phases.
| Phase | Purpose | Key Actions |
|---|---|---|
| 1. Baseline | Establish current performance | Profile query, measure time |
| 2. Analyze | Understand execution plan | Identify bottlenecks |
| 3. Diagnose |
| Root cause analysis |
| Cardinality, indexes, scans |
| 4. Optimize | Apply targeted optimizations | Rewrite, indexes, hints |
| 5. Verify | Measure improvement | Compare before/after |
| 6. Platform Integration | Integrate with Chariot query builder if needed | Update Go code |
NEVER optimize without profiling. Guessing leads to incorrect optimizations.
// Step 1: Always profile the current query
PROFILE
MATCH (a:Asset)-[:HAS_ATTRIBUTE]->(attr:Attribute)
WHERE attr.name = 'critical' AND a.status = 'A'
RETURN a
Look for: NodeByLabelScan, Expand(All), high db hits, large rows counts
All Chariot queries MUST include username filtering for tenant isolation:
WHERE a.username = $username
Reference: modules/chariot/backend/pkg/cloud/service/services/neo4j/neo4j.go line 24
Before using any field in a query filter, verify it exists in:
Source of truth: modules/chariot/backend/pkg/query/allowed_columns.go (200+ fields)
# Quick verification
grep -w "fieldname" modules/chariot/backend/pkg/query/allowed_columns.go
Invalid fields cause: "invalid filter column: {field}" errors
After optimization, verify identical results:
// Compare counts
WITH "MATCH ..." AS originalQuery, "MATCH ..." AS optimizedQuery
CALL { WITH originalQuery CYPHER runtime=interpreted originalQuery RETURN count(*) AS origCount }
CALL { WITH optimizedQuery CYPHER runtime=slotted optimizedQuery RETURN count(*) AS optCount }
RETURN origCount = optCount AS resultsMatch
1.1 Capture Current Performance
PROFILE
[your slow query here]
Document:
db hits (lower is better)rows processed at each step1.2 Understand Query Intent
See: references/profiling-queries.md for detailed PROFILE interpretation
2.1 Identify Expensive Operations
Common performance killers:
NodeByLabelScan on high-cardinality labels (Assets, Attributes)Expand(All) without relationship filtering2.2 Calculate Cardinality
// Asset count
MATCH (a:Asset) RETURN count(a)
// Attribute count
MATCH (attr:Attribute) RETURN count(attr)
// Filter selectivity
MATCH (attr:Attribute) WHERE attr.name = 'critical' RETURN count(attr)
MATCH (a:Asset) WHERE a.status = 'A' RETURN count(a)
3.1 Check Index Usage
SHOW INDEXES
Look for:
3.2 Verify Index Coverage
// Check if query uses index (should see NodeIndexSeek)
EXPLAIN
MATCH (attr:Attribute) WHERE attr.name = 'critical' RETURN attr
If no index: Proceed to Phase 4.1 (Create Indexes) If index not used: Proceed to Phase 4.2 (Add Index Hints)
Priority order: Indexes → Query rewrite → Index hints → Advanced patterns
4.1 Create Missing Indexes
// Selective property indexes
CREATE INDEX attribute_name IF NOT EXISTS FOR (attr:Attribute) ON (attr.name);
CREATE INDEX asset_status IF NOT EXISTS FOR (a:Asset) ON (a.status);
// Multi-tenant composite indexes (CRITICAL for Chariot)
CREATE INDEX asset_username_status IF NOT EXISTS FOR (a:Asset) ON (a.username, a.status);
Wait for indexes to populate:
SHOW INDEXES YIELD name, state WHERE state = 'POPULATING'
4.2 Optimize Query Structure
Strategy A: Start from Selective Node
// BEFORE: Start from high-cardinality Assets
MATCH (a:Asset)-[:HAS_ATTRIBUTE]->(attr:Attribute)
WHERE attr.name = 'critical' AND a.status = 'A'
RETURN a
// AFTER: Start from low-cardinality Attributes
MATCH (attr:Attribute)-[:HAS_ATTRIBUTE]-(a:Asset)
WHERE attr.name = 'critical' AND a.status = 'A'
RETURN a
Strategy B: Use Index Hints
MATCH (attr:Attribute)
USING INDEX attr:Attribute(name)
WHERE attr.name = 'critical'
MATCH (attr)<-[:HAS_ATTRIBUTE]-(a:Asset)
WHERE a.status = 'A'
RETURN a
Strategy C: Filter Early with EXISTS
MATCH (a:Asset)
WHERE a.status = 'A'
AND EXISTS {
MATCH (a)-[:HAS_ATTRIBUTE]->(attr:Attribute)
WHERE attr.name = 'critical'
}
RETURN a
See: references/optimization-patterns.md for advanced strategies
5.1 Profile Optimized Query
PROFILE
[optimized query]
5.2 Compare Metrics
| Metric | Before | After | Improvement |
|---|---|---|---|
| Execution time | ?ms | ?ms | ?x faster |
| db hits | ? | ? | ?x fewer |
| Index usage | No | Yes | ✅ |
5.3 Validate Results
// Ensure same results
WITH originalResults, optimizedResults
RETURN size(originalResults) = size(optimizedResults) AS countsMatch
Success criteria:
NodeIndexSeek in PROFILE)When needed: Query is generated by Chariot's query builder (pkg/query/read.go)
6.1 Update Query Builder
If optimization requires query structure changes, update the query builder:
// modules/chariot/backend/pkg/query/read.go
// Add optimization logic to generate efficient query patterns
6.2 Test Integration
cd modules/chariot/backend
go test ./pkg/query/... -v -run TestReadQuery
// Chariot-specific: Always filter by username first
MATCH (a:Asset)
USING INDEX a:Asset(username)
WHERE a.username = $username AND a.status = 'A'
MATCH (a)-[:HAS_ATTRIBUTE]->(attr:Attribute)
WHERE attr.name = 'critical'
RETURN a
Why: Username provides highest selectivity in multi-tenant systems
// If Attribute has lower cardinality than Asset
MATCH (attr:Attribute)<-[:HAS_ATTRIBUTE]-(a:Asset)
WHERE attr.name = 'critical' AND a.status = 'A'
RETURN a
Why: Start from smaller node set, traverse to larger
// Filter relationships during traversal
MATCH (a:Asset)-[r:HAS_ATTRIBUTE WHERE r.validated = true]->(attr:Attribute)
WHERE attr.name = 'critical'
RETURN a
Why: Reduces relationship expansion before filtering
See: references/optimization-patterns.md for complete pattern catalog
Check:
EXPLAIN should show NodeIndexSeek)SHOW INDEXES state should be ONLINE)CALL db.stats.retrieve('GRAPH COUNTS'))rows in PROFILESolutions:
USING INDEX hintLikely causes:
Solutions:
runtime=slotted or runtime=pipelined (Neo4j 5.x)CALL db.stats.retrieve('GRAPH COUNTS')Causes:
Solution: Revert optimization, verify query semantics step by step
backend-developer agent when implementing slow query fixesbackend-reviewer agent when reviewing query performance/chariot-api command when diagnosing API slowness| Skill | When | Purpose |
|---|---|---|
querying-neo4j-with-cypher | Start | Modern syntax patterns for query rewrite |
using-skills | Start | Skill discovery and invocation patterns |
| Skill | Phase/Step | Purpose |
|---|---|---|
querying-neo4j-with-cypher | Phase 4.2 | Query rewrite patterns |
verifying-before-completion | Phase 5.3 | Validate optimization success |
| Skill | Trigger | Purpose |
|---|---|---|
debugging-systematically | Complex performance issues | Root cause analysis |
orchestrating-research | Unknown Neo4j optimization | Research best practices |
querying-neo4j-with-cypher - Modern Cypher syntax and query patternsdebugging-systematically - Systematic debugging methodologyverifying-before-completion - Final validation checklistDetailed guidance organized by topic: