Neo4j Composite Indexes: Fixing Multi-Property Filters Without Intersection Overhead
Composite property indexes can collapse multi-attribute graph queries into a single index seek, but only when property order matches your query patterns. Here is how to decide when they help and what they cost.
06 Dec 2025, 09:07 UTC

The Problem: Three Filters, One Slow Scan
You have a graph of products, and your application needs to find items in a category, above a price floor, and marked in stock. The query looks straightforward, but without the right index Neo4j examines every Product node and filters them one by one. If you already maintain single-property indexes on category, price, and inStock, the planner may intersect them: seek each index, merge the results, and return the survivors. Intersection works, but it is not free. On high-cardinality properties—where values are mostly unique, like price—the intermediate result sets can be large, and the merge step becomes the bottleneck.
Why Intersecting Single-Property Indexes Isn't Free
A single-property index is an ordered map from a value to the nodes that carry it. When a query filters on three properties, the engine can look up three separate indexes and combine the node sets. That combination, often called an intersection, requires the engine to fetch candidate nodes from each index and compare them. The cost grows with the cardinality of each property. A low-cardinality property like a boolean flag returns huge sets; intersecting them wastes I/O and memory. High-cardinality properties return smaller sets, but the engine still pays for multiple seeks and a merge step before it can return rows.
How Composite Indexes Work (and Why Order Matters)
A composite property index stores multiple properties for the same label in one ordered structure. Instead of intersecting three lookups, the planner can perform a single seek that narrows directly to matching nodes. The catch is ordering: the index is only useful when your query includes the leading properties in the order they were defined. This is prefix matching.
Think of it like a phone book sorted by last name, then first name. If you know both names, you find the entry immediately. If you only know the first name, the book is useless because the primary sort key is missing. Neo4j behaves the same way: omit the first property in the composite definition, and the planner will ignore the index entirely.
Order also determines which predicates can use the index efficiently. Equality checks on leading properties are ideal. Range predicates are generally only efficient on the last property in the sequence, because the index is sorted by the full property tuple. A range on an earlier property would force the engine to scan large portions of the index anyway.
A Worked Example: From FilterFilter to IndexSeek
Suppose your access pattern is always category first, then a price range. Create the composite index with equality properties before range properties:
CREATE COMPOSITE INDEX product_category_price IF NOT EXISTS
FOR (p:Product) ON (p.category, p.price)
Run this in the Neo4j Browser, Cypher Shell, or any client connected to the database. You need privileges to create indexes—typically the CREATE INDEX privilege or an administrative role, depending on your edition and security configuration. In recent Neo4j versions, IF NOT EXISTS makes the command idempotent, which is useful in migration scripts.
Before running production queries, verify the index is ready:
SHOW INDEX product_category_price
Check that the state is ONLINE and that the property order matches your intention. If the index is still POPULATING, queries will not use it yet.
Now prefix your query with EXPLAIN to inspect the plan without executing it:
EXPLAIN MATCH (p:Product)
WHERE p.category = $category
AND p.price >= $minPrice
RETURN p
You are looking for an IndexSeek operator in the plan. If you instead see NodeByLabelScan followed by Filter, or multiple separate index operators feeding an intersection, the composite index is not being used. The most common cause is a missing leading property: if the query filters only on price and inStock, the planner cannot use an index whose first property is category.
To measure the actual difference, run the same query with PROFILE and compare the reported execution time and rows fetched from the index against your previous single-index approach. Do not assume the composite index wins in every case; measure it against your data distribution.
The Trade-Off: Write Cost, Memory, and Query Shape
Composite indexes are not free. Every time a write updates any property covered by the index, Neo4j must update the composite index tree. If you index five properties, a write to any one of those five triggers an index update. This write amplification increases ingestion latency and consumes more memory and disk than a single-property index.
Over-indexing compounds the problem. Creating a composite index for every possible combination of filters might look defensive, but each additional index adds write overhead and memory pressure. During heavy batch ingestion, too many indexes can slow writes more than they speed up reads.
There is also a query-shape limitation. Composite indexes handle equality predicates on leading properties well, but range predicates are generally only efficient on the last property in the sequence. If you need range queries on both price and weight, a single composite index will not serve both efficiently. In that case, separate indexes or a different access pattern may be necessary.
Actionable Closing
Use a composite index when your queries reliably filter on the same set of properties, the leading properties have meaningful selectivity, and your workload is read-heavy enough to justify the write cost. Skip it when filters are ad hoc, when the first property has low cardinality, or when ingestion throughput is your primary constraint.
- Identify your most frequent multi-property query pattern and note which properties are equality checks versus range checks.
- Order the index so equality properties come first and the range property comes last.
- Create the index with
CREATE COMPOSITE INDEX, then confirm it isONLINEviaSHOW INDEX. - Run
EXPLAINon your target query and confirm anIndexSeekoperator appears. - Benchmark against your previous plan using
PROFILEbefore and after. - Monitor write latency after deployment; if ingestion slows noticeably, reconsider whether every indexed property is necessary.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.