Lesson 08

Indexes & Query Tuning

Difficulty Intermediate Time

Learning objectives

After this lesson you will be able to:

  1. Explain what RANGE indexes accelerate and when to create them on Movie-graph properties.
  2. Describe LOOKUP (token) indexes and how they support label and relationship-type access.
  3. Distinguish indexes from uniqueness and existence constraints.
  4. Use EXPLAIN to inspect a plan without executing side effects.
  5. Use PROFILE to measure operator row counts and database hits.
  6. Recognize common plan operators such as NodeByLabelScan, NodeIndexSeek, Expand, and Filter.
  7. Rewrite predicates and projections so the planner can choose index-backed seeks.

Core concepts

RANGE indexes and LOOKUP indexes

Without secondary structures, finding a person by name means scanning every Person node and filtering. That is acceptable for tiny demo graphs and disastrous for production multi-million node stores. A RANGE index on a property (for a label or relationship type) lets the planner perform seeks and range scans for equality and inequality predicates on that property. In modern Neo4j, RANGE is the default general-purpose property index type for many workloads (text, numbers, temporals), replacing older btree naming in documentation.

Creating an index is a schema operation, for example CREATE INDEX person_name IF NOT EXISTS FOR (p:Person) ON (p.name). Indexes improve read performance for matching predicates but add write overhead and storage. Index only properties you filter or join on frequently.

LOOKUP indexes (token indexes) help the database find all nodes for a label or all relationships for a type efficiently. They are not property uniqueness mechanisms. On current Neo4j versions, token lookup indexes are typically present by default; you still need property indexes for selective property access paths.

Full-text and vector indexes exist for specialized search and similarity workloads. This lesson focuses on RANGE and LOOKUP because they underpin ordinary Cypher transactional queries on the Movie graph and most OLTP application patterns.

CYPHER
// Property RANGE index for person name lookups
CREATE INDEX person_name IF NOT EXISTS
FOR (p:Person) ON (p.name);

// Property index for movie title
CREATE INDEX movie_title IF NOT EXISTS
FOR (m:Movie) ON (m.title);

Constraints are not “just indexes”

A uniqueness constraint declares a business rule: at most one node with a given label and property value. Neo4j backs uniqueness with an index so enforcement is efficient, but the constraint is what rejects duplicates. An index alone never stops two Person nodes from sharing the same email.

Property existence and type constraints (availability depends on edition and version) further protect data quality. For application design, put uniqueness on natural keys early— external ids, emails, SKUs—so imports and concurrent writers cannot fork identity. Lesson 10 returns to this theme in modeling and drivers.

Syntax sketch: CREATE CONSTRAINT person_name_unique IF NOT EXISTS FOR (p:Person) REQUIRE p.name IS UNIQUE. Exact keywords have evolved; consult the constraints section of the Cypher Manual for your version. Always prefer IF NOT EXISTS in migration scripts.

EXPLAIN, PROFILE, and reading operators

EXPLAIN prefixes a query to show the planner’s chosen operators without running the query against data (no result rows, no write effects from the explained statement’s execution). Use it while drafting to see whether you get an index seek or a label scan.

PROFILE executes the query and annotates each operator with runtime metrics: how many rows flowed out, and how many database hits occurred. PROFILE is the truth serum for performance—“I thought this was selective” becomes “Filter discarded 2 million rows after a label scan.”

Important operators to recognize:

  • NodeByLabelScan — visits all nodes with a label. Cheap on tiny graphs; costly on large ones when followed only by a Filter.
  • NodeIndexSeek / NodeIndexScan — uses a property index to find candidate nodes. Seeks are ideal for equality; scans cover ranges.
  • Expand(All) — walks relationships from a bound node to produce neighbors.
  • Filter — applies predicates that were not pushed into an index seek.
  • Eager aggregation or other eagerness boundaries — can materialize large intermediate sets; watch memory.

Tuning workflow: write correct Cypher first, PROFILE with realistic data volumes, ensure selective predicates can use indexes, reduce intermediate cardinality before expansions, and return projections instead of whole graphs. Avoid premature micro-optimization on demo data— plans change with statistics.

Execution plan contrast: NodeByLabelScan plus Filter versus NodeIndexSeek into Expand
Index seeks reduce candidates before expansion; label scans filter late and spend more db hits.

Worked examples

Dataset note: Expected results use the classic Neo4j Movie graph (37 movies, 133 people) and can differ if the data changes.

Index-friendly person lookup

Equality on an indexed property enables a seek. Run this after creating a name index; PROFILE should show a NodeIndexSeek (or similar) rather than a pure label scan plus filter. LIMIT 5 keeps the sample table aligned with the first five titles in alphabetical order.

CYPHER
PROFILE
MATCH (p:Person {name: 'Tom Hanks'})-[:ACTED_IN]->(m:Movie)
RETURN m.title AS title
ORDER BY title
LIMIT 5

Expected result (data rows; plan is visual in Browser)

title
A League of Their Own
Apollo 13
Cast Away
Charlie Wilson's War
Cloud Atlas

EXPLAIN a range predicate on released year

If you create a RANGE index on Movie(released), range filters can become index scans. Run the schema statement and the EXPLAIN query as separate statements (Neo4j Browser treats multi-statement paste carefully—prefer one runnable panel at a time).

CYPHER — schema (run first)
CREATE INDEX movie_released IF NOT EXISTS
FOR (m:Movie) ON (m.released);
CYPHER — EXPLAIN (run separately)
EXPLAIN
MATCH (m:Movie)
WHERE m.released >= 2000 AND m.released < 2010
RETURN m.title AS title, m.released AS released
ORDER BY released, title

Expected data if the MATCH is run without EXPLAIN (all 14 rows)

titlereleased
Cast Away2000
Jerry Maguire2000
The Replacements2000
Something's Gotta Give2003
The Matrix Reloaded2003
The Matrix Revolutions2003
The Polar Express2004
RescueDawn2006
The Da Vinci Code2006
V for Vendetta2006
Charlie Wilson's War2007
Frost/Nixon2008
Speed Racer2008
Ninja Assassin2009

Constraint-backed uniqueness for movie titles

Demonstrate the difference: after a uniqueness constraint, a duplicate create fails. Use this pattern for natural keys carefully—real films can share titles, so production models often use external ids instead. Here we illustrate mechanics on the sample graph’s title field.

CYPHER
// Prefer IF NOT EXISTS in migrations
CREATE CONSTRAINT movie_title_unique IF NOT EXISTS
FOR (m:Movie) REQUIRE m.title IS UNIQUE;

// This should fail if 'The Matrix' already exists
// CREATE (m:Movie {title: 'The Matrix', released: 1999});

MATCH (m:Movie {title: 'The Matrix'})
RETURN m.title AS title, m.released AS released

Expected result

titlereleased
The Matrix1999

Compare plans: filter after expansion

Start from a selective person seek, expand movies, then filter by year. PROFILE should show small row counts after the seek. Contrast mentally with matching all movies first—usually worse.

CYPHER
PROFILE
MATCH (p:Person {name: 'Keanu Reeves'})-[:ACTED_IN]->(m:Movie)
WHERE m.released >= 1999
RETURN m.title AS title, m.released AS released
ORDER BY released, title

Expected result

titlereleased
The Matrix1999
The Replacements2000
Something's Gotta Give2003
The Matrix Reloaded2003
The Matrix Revolutions2003

Pitfalls

Exercises

  1. 1. Create a name index

    Write a schema statement that creates a RANGE index on Person.name if it does not exist, then PROFILE a name lookup.

    STARTER
    CREATE INDEX ...
    PROFILE
    MATCH (p:Person {name: 'Meg Ryan'})
    RETURN p.born
  2. 2. EXPLAIN a title match

    EXPLAIN a query that finds a movie by title and returns its tagline. Note whether the plan mentions an index operator.

    STARTER
    EXPLAIN
    MATCH (m:Movie {title: 'Top Gun'})
    RETURN m.tagline
  3. 3. Uniqueness constraint draft

    Draft a uniqueness constraint for a hypothetical Person.email property (add the property only in a sandbox).

    STARTER
    CREATE CONSTRAINT person_email_unique IF NOT EXISTS
    FOR (p:Person) REQUIRE p.email IS UNIQUE
  4. 4. PROFILE co-actor query

    PROFILE a co-actor query for Tom Hanks and identify the expand and filter operators in the plan output.

    STARTER
    PROFILE
    MATCH (tom:Person {name: 'Tom Hanks'})-[:ACTED_IN]->(m)<-[:ACTED_IN]-(co)
    WHERE co <> tom
    RETURN count(DISTINCT co)

Lesson quiz

Five questions. Submit once to see your score and the full answer key.

Loading quiz…

Resources

Video

Watch on YouTube (opens in a new tab).