Creating Property Indexes
Introduction
Welcome back to Indexes and Performance! In this second lesson, we'll explore one of the most important performance-optimization techniques: creating and managing indexes. As your graph database grows, the ability to quickly locate specific nodes and relationships becomes critical. Without proper indexing, queries can become slow as Neo4j must scan through every node or relationship to find matches. Indexes provide a fast lookup mechanism that can dramatically improve query performance, often reducing execution time from seconds to milliseconds. In this lesson, we'll learn how to create indexes, view them, measure their impact, and understand when they're beneficial.
Why Indexes Matter in Graph Databases
Indexes in graph databases work similarly to indexes in traditional databases: they create an optimized data structure that allows the database to quickly find nodes or relationships based on property values. Imagine searching for a specific person in a social network with millions of users. Without an index, Neo4j would need to examine every single User node to find the one you're looking for. With an index on the name property, Neo4j can jump directly to the relevant nodes in just a few operations. This becomes especially important in graph queries where we typically start by finding specific anchor nodes and then traverse relationships from there. The faster we can locate those starting points, the faster our entire query executes.
Understanding Index Mechanics
When you create an index in Neo4j, the database builds a separate data structure that maps property values to the nodes or relationships that contain them. This structure is maintained automatically as you add, update, or delete data. Neo4j uses range indexes by default, which are optimized for equality checks, range queries, and sorting operations. These indexes support common query patterns like finding exact matches (WHERE u.name = 'Alice'), range conditions (WHERE u.age > 25), and prefix matching (WHERE u.name STARTS WITH 'Al'). The query planner automatically decides whether to use an available index based on the query structure and estimated performance benefits.
Creating Your First Index
Let's create our first index on the name property of User nodes. This will speed up any queries that filter or search by user names:
This statement creates a range index named user_name that applies to all nodes with the User label. The syntax follows this pattern:
CREATE INDEXfollowed by the index nameFOR (u:User)specifies the node label patternON (u.name)indicates which property to index
Once created, Neo4j will populate this index with all existing User nodes and automatically maintain it as data changes.
Adding Another Index
Email addresses are another common property used in queries, especially for user lookups and authentication flows. Let's create an index for the email property:
This follows the same pattern as our name index but targets the email property. In practice, if email serves as a unique identifier for users, you might consider using a uniqueness constraint instead, which automatically creates an index while also enforcing data integrity. We'll explore uniqueness constraints in detail in a later lesson. However, for learning purposes, a standard index demonstrates the core concepts clearly.
Viewing All Indexes
After creating indexes, we need a way to verify they exist and check their status. Neo4j provides the SHOW INDEXES command for this purpose:
Output:
| id | name | state | populationPercent | type | entityType | labelsOrTypes | properties | indexProvider | owningConstraint | lastRead | readCount |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 2 | index_1b9dcc97 | ONLINE | 100.0 | LOOKUP | RELATIONSHIP | NULL | NULL | token-lookup-1.0 | NULL | NULL | 0 |
| 1 | index_460996c0 | ONLINE | 100.0 | LOOKUP | NODE | NULL | NULL | token-lookup-1.0 | NULL | NULL | 0 |
| 4 | user_email | ONLINE | 100.0 | RANGE | NODE | ["User"] | ["email"] | range-1.0 | NULL | NULL | NULL |
| 3 | user_name | ONLINE | 100.0 | RANGE | NODE | ["User"] | ["name"] | range-1.0 | NULL | NULL | NULL |
Notice that the output includes not just our user_name and user_email indexes, but also two LOOKUP indexes. These are system indexes that Neo4j creates automatically to optimize label and relationship type lookups. You'll always see these in your database.
This command returns a table with comprehensive information about all indexes in the database:
name: the index name we specified (or auto-generated for system indexes)type: the kind of index (RANGE,LOOKUP,TEXT,FULLTEXT, etc.)state: whether the index isONLINE,POPULATING, orFAILEDpopulationPercent: the completion percentage for newly created indexesentityType: whether this indexes nodes or relationshipslabelsOrTypes: which labels or relationship types the index coversproperties: which properties are indexedindexProvider: the underlying index implementation usedowningConstraint: the name of the constraint that created this index (NULL if created independently)lastRead: timestamp of when this index was last used by a query (NULL if never used)readCount: the total number of times this index has been accessed by queries
When Indexes Help Performance
Indexes provide the greatest benefit when they help Neo4j narrow down large datasets to small, relevant subsets. They're most effective for properties with high selectivity, meaning many distinct values. For example, indexing user IDs, email addresses, or product SKUs makes sense because each value typically identifies a small number of nodes. Indexes also excel in graph traversal patterns where we anchor the query by finding specific starting nodes first, then exploring relationships from there. A typical pattern is matching users by name or ID, then traversing their social connections or purchase history.
Measuring Index Impact with PROFILE
To verify that an index actually improves performance, we use the PROFILE command. Let's compare the same query before and after creating an index:
Output (Without Index):
Now with the index:
Output (With Index):
The execution time dropped from 36 to 2ms — an 18x performance improvement! Notice also that database hits decreased dramatically from 1,053 to just 4. Without the index, Neo4j had to scan through all User nodes examining each one's name property. With the index, it jumped directly to the matching node using the index lookup mechanism. The dramatic reduction in both execution time and database hits clearly demonstrates the value of proper indexing.
Understanding Index Overhead
While indexes speed up reads, they come with tradeoffs that we must consider. Every time you create, update, or delete a node with indexed properties, Neo4j must update the index structures as well. This creates write amplification: a single property change triggers multiple storage operations. Indexes also consume disk space and compete with graph data for memory cache. For write-heavy workloads or properties with low selectivity (like boolean flags), the overhead may outweigh the benefits. The key is finding balance: index properties that appear frequently in query filters and have high cardinality, but avoid over-indexing properties that are rarely queried or have few distinct values.
Conclusion and Next Steps
In this lesson, we've covered the fundamentals of creating indexes in Neo4j to optimize query performance. We learned how to create indexes on node properties using the CREATE INDEX syntax, view all existing indexes with SHOW INDEXES, and understand when indexes provide genuine performance benefits versus when they introduce unnecessary overhead. We also explored how to measure index impact using the PROFILE command to confirm the query planner is utilizing our indexes effectively. The concepts of index selectivity, write amplification, and proper index design will serve as guiding principles as you optimize your own graph databases. Now it's time to put these concepts into practice and create indexes that will make your queries fly!
