Neo4j Composite Indexes: Multi-Property Equality Lookups in 4.x and 5.x
Composite indexes in Neo4j speed up multi-property equality lookups by storing concatenated keys in one B+Tree. They work for the leading property alone or both properties together, but not trailing properties alone. Neo4j 5.x adds limited ORDER BY support.
28 Jul 2025, 15:34 UTC

When to Use a Composite Index
Use a composite index when you frequently query a label with equality predicates on two or more properties together, or on the leading property alone. A single composite index replaces multiple single-property indexes for joint lookups, avoids index intersection overhead, and gives the planner better cardinality estimates in Neo4j 5.x.
Creating the Index
Run the following Cypher against the database (requires DB_ADMIN or INDEX_MANAGEMENT privilege):
CREATE INDEX idx_person_name_age FOR (p:Person) ON (p.name, p.age);This builds a single B+Tree where each key concatenates the encoded name value, a 1-byte separator, and the encoded age value. Population runs asynchronously; in scripts, wait for completion with:
CALL db.awaitIndexes(300);Planner Behavior: What Gets Indexed and What Doesn't
The index supports:
MATCH (p:Person {name: 'Alice', age: 30}) RETURN p— full prefix match,IndexSeekon both properties.MATCH (p:Person {name: 'Alice'}) RETURN p— leading property only,IndexSeekonnameprefix.
It does not support:
MATCH (p:Person {age: 30}) RETURN p— missing leading column; falls back toNodeByLabelScan.MATCH (p:Person) WHERE p.age > 30 RETURN p— range on non-leading property without leading equality.
Verifying Index Usage
Run EXPLAIN on the target queries and inspect the plan:
EXPLAIN MATCH (p:Person {name: 'Alice', age: 30}) RETURN p;Look for IndexSeek with index: 'idx_person_name_age'. For the negative case:
EXPLAIN MATCH (p:Person {age: 30}) RETURN p;Should show NodeByLabelScan.
Inspecting Index Metadata
Confirm the index exists and its structure:
CALL db.indexes() YIELD name, type, properties, labels WHERE name = 'idx_person_name_age' RETURN *;Expected output: type: 'BTREE', properties: ['name', 'age'], labels: ['Person'].
Check population progress if needed:
CALL db.index.progress() YIELD indexName, completed, total WHERE indexName = 'idx_person_name_age';Storage Overhead
Each entry stores the concatenated key: encoded(name) + 0x00 + encoded(age). The 1-byte separator per property means a two-property index adds roughly len(name) + len(age) + 1 bytes per node versus separate single-property indexes (which each store full keys plus node references). For high-cardinality leading properties, the composite index is often smaller than two separate indexes because it avoids duplicate node references.
Version-Specific Limitations
Neo4j 4.x
- Composite indexes cannot be used for
ORDER BYon the second property, even with leading equality. - Index intersection (combining two single-property indexes) is the only alternative for joint lookups, but it has higher overhead and poorer cardinality estimates.
Neo4j 5.x
- Limited
ORDER BYsupport on the second property when the leading property is equality-constrained (e.g.,WHERE p.name = 'Alice' ORDER BY p.age). - Composite indexes are explicitly preferred over multiple single-property indexes for joint equality lookups.
Common Mistakes
Assuming Trailing Property Alone Works
Queries filtering only on age will not use the index. If you need both access patterns, create a separate single-property index on age or reverse the property order in a new composite index.
Trying to Add a Property to an Existing Composite Index
Not supported. You must drop and recreate:
DROP INDEX idx_person_name_age;CREATE INDEX idx_person_name_age_city FOR (p:Person) ON (p.name, p.age, p.city);Dropping locks the Person label during population of the new index. Plan for downtime or use a maintenance window.
Mixing Unique Constraints and Composite Indexes on Overlapping Properties
CREATE CONSTRAINT ON (p:Person) ASSERT p.name, p.age IS UNIQUE creates a backing composite index that also enforces uniqueness. Adding a separate non-unique composite index on the same properties duplicates the index build. Choose one: constraint for uniqueness enforcement, plain index for lookup performance without uniqueness.
Forgetting to Await Index Population
In automated deployments, queries issued immediately after CREATE INDEX may fall back to scans. Always call CALL db.awaitIndexes(timeoutSeconds) in scripts.
Dropping the Index
Explicit drop required; dropping a constituent property constraint does not cascade:
DROP INDEX idx_person_name_age;Quick Reference Table
| Query Pattern | Index Used? | Plan Operator |
|---|---|---|
{name: 'Alice', age: 30} | Yes | IndexSeek (full key) |
{name: 'Alice'} | Yes | IndexSeek (prefix) |
{age: 30} | No | NodeByLabelScan |
WHERE age > 30 | No | NodeByLabelScan |
WHERE name = 'Alice' ORDER BY age | 5.x only | IndexSeek + Order (5.x) |
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.