When to Index in Neo4j: Using Range Indexes to Speed Up Queries Without Slowing Down Writes
A range index on node properties can speed up MATCH queries, but only if the planner actually uses it. Learn to verify index usage with EXPLAIN/PROFILE and balance read gains against write overhead.
05 Aug 2026, 03:35 UTC

The Problem: Slow MATCH Queries in a Growing Graph
Imagine a recommendation engine that matches users based on age ranges. As the user graph grows past a million nodes, queries like MATCH (u:User) WHERE u.age >= 25 AND u.age <= 35 start taking seconds. The planner defaults to scanning every User node, which becomes prohibitively expensive at scale.
Thesis: Strategic Range Indexes + Planner Verification
A range index on :User(age) lets Neo4j use a NodeIndexSeek instead of a full label scan. But indexes aren’t free—they add write overhead and storage. The key is confirming the planner actually uses the index and measuring the trade-off for your workload.
Creating and Verifying a Range Index
Range indexes in Neo4j 5.x are created with:
CREATE INDEX user_age_index FOR (u:User) ON (u.age)
Run this in the neo4j shell or Browser. Check status:
SHOW INDEXES YIELD name, state, type WHERE name = 'user_age_index'
When state = 'ONLINE', the index is ready. The type will be RANGE.
Confirming Index Usage with EXPLAIN and PROFILE
Before the index exists, this query:
EXPLAIN MATCH (u:User) WHERE u.age >= 25 AND u.age <= 35 RETURN u
shows NodeByLabelScan in the plan. After creating the index, the same EXPLAIN shows NodeIndexSeek with an index: user_age_index annotation. Use PROFILE to see actual database hits—expect a dramatic drop from scanning all User nodes to touching only matching index entries.
Worked Example: Age-Based User Filtering
Consider a social app where users search for peers in specific age brackets. Without an index, the query planner has no choice but to iterate through every User node. By adding a range index on the age property, we enable the planner to seek directly into the index structure, dramatically reducing the number of nodes that need to be examined.
After creating the index, verify its usage:
EXPLAIN MATCH (u:User) WHERE u.age >= 25 AND u.age <= 35 RETURN u
Look for NodeIndexSeek in the output. If you see AllNodesScan or NodeByLabelScan, the index isn’t being used—check that the index is online and that the query predicates match the indexed property exactly.
Trade-offs: Read Speed vs. Write Overhead
Every write operation—CREATE, SET, DELETE—must now update the index. On high-ingestion workloads, this can add milliseconds per transaction. Storage also grows: each indexed property requires additional B-tree structures on disk and memory for caching.
Composite indexes help when queries filter on multiple leading properties. For example, CREATE INDEX user_loc_age FOR (u:User) ON (u.location, u.age) serves queries filtering by location first, then age. However, the planner won’t use this index for queries that only filter on age alone.
Actionable Closing: Measure Before You Index
Don’t index blindly. Start with EXPLAIN on slow queries to identify scans. Create a range index on the hot property, then re-run PROFILE to compare database hits and execution time. Finally, test write throughput on a staging copy—if ingestion latency increases unacceptably, consider dropping the index or redesigning the schema (e.g., denormalizing into relationship properties or using a different access pattern).
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.