Docs
View as markdown

Indexes and constraints

Create each index and constraint kind relate supports, list them with SHOW, check with EXPLAIN when the planner uses an index, and drop them.

cyphercli

Before you start: Your first graph in the CLI

This guide shows you how to add index A structure the planner uses to find nodes or relationships by a property without scanning a label: range, text, point, vector or full-text. Read more in the glossary that make lookups fast and constraint A rule the store enforces on every write, such as a unique property, a required property, a property type or a node key; a violating write fails. Read more in the glossary that keep the data clean, list both, check with EXPLAIN that the planner uses an index, and drop them. An index changes how fast a statement runs and never what it returns; a constraint changes which writes succeed. The cells run in page order on one in-memory store; the plan steps also run in a terminal.

Prerequisites

Load the example graph

This cell writes the shared graph: Ada, Grace and Noor, the relate and opencypher-tck projects, and who works on what.

Cypher
CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
       (grace:Person {name: 'Grace', role: 'engineer'}),
       (noor:Person {name: 'Noor', role: 'maintainer'}),
       (relate:Project {name: 'relate', language: 'Rust'}),
       (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
       (ada)-[:WORKS_ON {since: 2024}]->(relate),
       (grace)-[:WORKS_ON {since: 2023}]->(relate),
       (grace)-[:WORKS_ON {since: 2022}]->(tck),
       (noor)-[:MAINTAINS]->(relate),
       (ada)-[:KNOWS]->(grace)
Recorded result
(no rows) · nodes created: 5, relationships created: 5, properties set: 13, labels added: 5

Choose an index

Every index has a name, one label A name that classifies a node, such as Person; a node has zero or more, and patterns, indexes and constraints refer to them. Read more in the glossary or relationship type The single name every relationship carries, such as KNOWS or WORKS_ON; patterns match on it, and a relationship cannot change it. Read more in the glossary, and the properties it covers. IF NOT EXISTS after the name makes a create a no-op when an index of that name exists.

KindStatementServes
Range, the defaultCREATE INDEX <name> FOR (n:<Label>) ON (n.<prop>); CREATE RANGE INDEX is the same; ON (n.<a>, n.<b>) covers two propertiesEquality and range comparisons on the property, IN a list, and STARTS WITH
Range, on relationshipsCREATE INDEX <name> FOR ()-[r:<TYPE>]-() ON (r.<prop>)Created and listed; on the verified version, PROFILE shows a relationship-property filter still reading every relationship of the type
TextCREATE TEXT INDEX <name> FOR (n:<Label>) ON (n.<prop>)CONTAINS on a string property
PointCREATE POINT INDEX <name> FOR (n:<Label>) ON (n.<prop>)point.distance(<prop>, <center>) < <bound> and point.withinBBox (geospatial queries)
Full-textCREATE FULLTEXT INDEX <name> FOR (n:<Label>) ON EACH [n.<prop>, …]SEARCH … FULLTEXT INDEX (full-text and hybrid search)
VectorCREATE VECTOR INDEX <name> FOR (n:<Label>) ON (n.<prop>) OPTIONS {…}SEARCH … VECTOR INDEX (vector search)
LookupCREATE LOOKUP INDEX <name> FOR (n) ON EACH labels(n)Created and listed by SHOW INDEXES

This cell creates a range index on a person's role, a text index on a project's language, and a two-property index:

Cypher
CREATE INDEX person_role IF NOT EXISTS FOR (p:Person) ON (p.role);
CREATE TEXT INDEX project_language IF NOT EXISTS FOR (p:Project) ON (p.language);
CREATE INDEX person_name_role IF NOT EXISTS FOR (p:Person) ON (p.name, p.role)
Recorded result
(no rows) · indexes added: 3

The search indexes take options and have their own rules; the search reference lists them.

Add constraints

A constraint is checked on every write, and on the existing data when you create it. A write that breaks one fails as a whole and changes nothing.

KindStatementRefuses
UniquenessCREATE CONSTRAINT <name> FOR (n:<Label>) REQUIRE n.<prop> IS UNIQUE; on relationships, FOR ()-[r:<TYPE>]-()A second node or relationship with the same value. One without the property passes
Existence… REQUIRE n.<prop> IS NOT NULLA node or relationship without the property, or a REMOVE of it
Property type… REQUIRE n.<prop> IS :: <TYPE>, such as INTEGER or STRINGA value of another type
Node key… REQUIRE (n.<a>, n.<b>) IS NODE KEYA node missing any of the properties, or repeating their combination
Relationship keyFOR ()-[r:<TYPE>]-() REQUIRE r.<prop> IS RELATIONSHIP KEYThe same, for relationships

A uniqueness constraint and a key constraint each create a range index of the same name, which serves lookups like any range index. This cell adds three constraints: unique project names, a required role, and an integer since:

Cypher
CREATE CONSTRAINT project_name IF NOT EXISTS FOR (p:Project) REQUIRE p.name IS UNIQUE;
CREATE CONSTRAINT person_role_required IF NOT EXISTS FOR (p:Person) REQUIRE p.role IS NOT NULL;
CREATE CONSTRAINT works_on_since IF NOT EXISTS FOR ()-[w:WORKS_ON]-() REQUIRE w.since IS :: INTEGER
Recorded result
(no rows) · constraints added: 3

A second relate project now fails, and the statement writes nothing:

Cypher
CREATE (:Project {name: 'relate', language: 'Go'})
Expected error
error: semantic error: ConstraintValidationFailed: `project_name` uniqueness

MERGE on the constrained key finds the existing node instead of creating a duplicate, so it is the write to use for "create unless it exists":

Cypher
MERGE (p:Project {name: 'relate'})
ON CREATE SET p.language = 'Go'
RETURN p.name AS name, p.language AS language
Recorded result
namelanguage
"relate""Rust"

1 row

A constraint that the existing data already breaks is refused when you create it. No project has a tag, so this one fails:

Cypher
CREATE CONSTRAINT project_tag FOR (p:Project) REQUIRE p.tag IS NOT NULL
Expected error
error: semantic error: ConstraintValidationFailed: `project_tag` requires properties

A graph type declares the same rules for a whole graph at once, and an ontology An Ontology Graph Spec document stored with a store, managed by the db.ontology.* procedures behind the ontology preview, and appliable as the graph type. Read more in the glossary can be applied as one (use an ontology).

List indexes and constraints

SHOW INDEXES and SHOW CONSTRAINTS return one row per entry. YIELD picks the columns:

Cypher
SHOW CONSTRAINTS YIELD name, type, entityType, labelsOrTypes, properties, propertyType
RETURN name, type, entityType, labelsOrTypes, properties, propertyType
ORDER BY name
Recorded result
nametypeentityTypelabelsOrTypespropertiespropertyType
"person_role_required""NODE_PROPERTY_EXISTENCE""NODE"["Person"]["role"]null
"project_name""NODE_PROPERTY_UNIQUENESS""NODE"["Project"]["name"]null
"works_on_since""RELATIONSHIP_PROPERTY_TYPE""RELATIONSHIP"["WORKS_ON"]["since"]"INTEGER"

3 rows

Cypher
SHOW INDEXES YIELD name, type, labelsOrTypes, properties, owningConstraint, state
RETURN name, type, labelsOrTypes, properties, owningConstraint, state
ORDER BY name
Recorded result
nametypelabelsOrTypespropertiesowningConstraintstate
"person_name_role""RANGE"["Person"]["name", "role"]null"ONLINE"
"person_role""RANGE"["Person"]["role"]null"ONLINE"
"project_language""TEXT"["Project"]["language"]null"ONLINE"
"project_name""RANGE"["Project"]["name"]"project_name""ONLINE"

4 rows

owningConstraint names the constraint an index belongs to, and state is ONLINE once the index serves reads. SHOW RANGE INDEXES, SHOW UNIQUE CONSTRAINTS and the other kinds narrow the list to one kind.

Check that the planner uses an index

EXPLAIN plans a statement without running it, and returns the plan and the planner's notifications. In this page's cells, the uniqueness constraint's index serves a lookup by project name, but nothing serves a lookup by language: the text index on language serves CONTAINS, not equality, so the planner scans every Project and names the range index that would help:

Cypher
EXPLAIN MATCH (p:Project {language: 'Rust'}) RETURN p.name AS name
Recorded result
(no rows)

In a browser, the plan viewer draws the plan as a tree. Compare with the index creates the range index the notification names on this page's database, plans the statement without it and with it, and shows the two side by side; pressed again, it drops the index.

The plan recorded for cell explain-language, as relate --mode table prints it:

Query: MATCH (p:Project {language: 'Rust'}) RETURN p.name AS name
Plan:
+---------------------+----+-------------+----------------+
| Operator            | Id | Details     | Estimated Rows |
+---------------------+----+-------------+----------------+
| + ProduceResults    | 0  | 1 item(s)   | -              |
+---------------------+----+-------------+----------------+
| | + NodeByLabelScan | 1  | (p:Project) | 2              |
+---------------------+----+-------------+----------------+
Notifications:
Id 1: INFORMATION 03C91 Relate.Notification.Statement.MissingIndex: the equality predicate on language filters :Project without a range index on (:Project {language}) Remedy: CREATE INDEX relate_Project__language FOR (n:Project) ON (n.language)

Notifications, each with its remedy:

  • 03C91 Missing index on operator 1: the equality predicate on language filters :Project without a range index on (:Project {language}). Remedy: CREATE INDEX relate_Project__language FOR (n:Project) ON (n.language)

In a browser, the viewer's comparison runs CREATE INDEX relate_Project__language FOR (n:Project) ON (n.language) on the page's in-memory database only.

Live: the plan is drawn as a tree, and PROFILE runs the statement on the page's database in your browser and adds each operator's rows, time and database hits as bars; the index button creates the index on the page's database, plans the statement with and without it side by side with the changes, and drops it again; a PROFILE downloads as a speedscope file.

In a terminal, --mode table prints the plan and the notifications. This step writes the example graph to a new store, team.rl:

relate --database ./team.rl "CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
       (grace:Person {name: 'Grace', role: 'engineer'}),
       (noor:Person {name: 'Noor', role: 'maintainer'}),
       (relate:Project {name: 'relate', language: 'Rust'}),
       (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
       (ada)-[:WORKS_ON {since: 2024}]->(relate),
       (grace)-[:WORKS_ON {since: 2023}]->(relate),
       (grace)-[:WORKS_ON {since: 2022}]->(tck),
       (noor)-[:MAINTAINS]->(relate),
       (ada)-[:KNOWS]->(grace)"

Without an index, a lookup by name scans every Project, and the planner says which index would help:

relate --database ./team.rl --read-only --mode table "EXPLAIN MATCH (p:Project {name: 'relate'}) RETURN p.language AS language"
Query: MATCH (p:Project {name: 'relate'}) RETURN p.language AS language
Plan:
+---------------------+----+-------------+----------------+
| Operator            | Id | Details     | Estimated Rows |
+---------------------+----+-------------+----------------+
| + ProduceResults    | 0  | 1 item(s)   | -              |
+---------------------+----+-------------+----------------+
| | + NodeByLabelScan | 1  | (p:Project) | 2              |
+---------------------+----+-------------+----------------+
Notifications:
Id 1: INFORMATION 03C91 Relate.Notification.Statement.MissingIndex: the equality predicate on name filters :Project without a range index on (:Project {name}) Remedy: CREATE INDEX relate_Project__name FOR (n:Project) ON (n.name)

The uniqueness constraint's index serves the same lookup. This command writes the constraint to team.rl:

relate --database ./team.rl "CREATE CONSTRAINT project_name IF NOT EXISTS FOR (p:Project) REQUIRE p.name IS UNIQUE"
relate --database ./team.rl --read-only --mode table "EXPLAIN MATCH (p:Project {name: 'relate'}) RETURN p.language AS language"
Query: MATCH (p:Project {name: 'relate'}) RETURN p.language AS language
Plan:
+-------------------+----+---------------------------------------------------+----------------+
| Operator          | Id | Details                                           | Estimated Rows |
+-------------------+----+---------------------------------------------------+----------------+
| + ProduceResults  | 0  | 1 item(s)                                         | -              |
+-------------------+----+---------------------------------------------------+----------------+
| | + NodeIndexSeek | 1  | RANGE INDEX p:Project(name) WHERE name = 'relate' | 1              |
+-------------------+----+---------------------------------------------------+----------------+

NodeIndexSeek starts from the one node the index finds, and the notification is gone. A plan names an index only when it starts from it: NodeIndexSeek for an equality and NodeIndexSeekByRange for a range comparison. A point index, a text index, and a range index serving IN or STARTS WITH work inside the scan's WHERE filter instead, so the plan still shows NodeByLabelScan; PROFILE shows the difference as fewer database hits (geospatial queries has a worked example, and tune a query reads a profile).

Drop an index or a constraint

DROP INDEX <name> and DROP CONSTRAINT <name> remove one; IF EXISTS makes a missing name a no-op. Dropping a constraint also drops the index it owns:

Cypher
DROP INDEX person_name_role IF EXISTS;
DROP CONSTRAINT person_role_required IF EXISTS
Recorded result
(no rows) · indexes removed: 1, constraints removed: 1

An index that belongs to a constraint cannot be dropped on its own:

Cypher
DROP INDEX project_name
Expected error
error: Unable to drop 'project_name': the index belongs to a constraint.

Verify

List what the store holds after the drops:

Cypher
SHOW INDEXES YIELD name, type, owningConstraint, state
RETURN name, type, owningConstraint, state
ORDER BY name
Recorded result
nametypeowningConstraintstate
"person_role""RANGE"null"ONLINE"
"project_language""TEXT"null"ONLINE"
"project_name""RANGE""project_name""ONLINE"

3 rows

Three ONLINE indexes, one of them owned by project_name, and the NodeIndexSeek plan in the terminal mean the indexes are in place and serve the lookups they were made for.

If it goes wrong

MessageCauseRemedy
semantic error: ConstraintValidationFailed: `project_name` uniqueness (exit code 6)A write repeats a unique value or key.Use MERGE on the key, or change the value.
semantic error: ConstraintValidationFailed: `<name>` requires propertiesA write leaves out a required or key property, or the existing data lacks it when the constraint is created.Set the property, or fill it in on the existing data first.
semantic error: ConstraintValidationFailed: `<name>` property typeA value of another type than the constraint names.Convert the value, such as toInteger(row.since).
semantic error: IndexAlreadyExists or semantic error: ConstraintAlreadyExistsThe name is taken.Add IF NOT EXISTS, or check SHOW INDEXES and SHOW CONSTRAINTS.
semantic error: EquivalentSchemaRuleAlreadyExistsAn index or constraint on the same label and properties exists under another name, such as a constraint's own index.Keep the existing one.
semantic error: AlreadyExists: there already exists an index (:Person {role}). A constraint cannot be created until the index has been dropped.A uniqueness constraint over properties that a range index already covers.Drop the index, then create the constraint, which brings its own.
Unable to drop 'project_name': the index belongs to a constraint. (exit code 1)DROP INDEX on a constraint's index.Drop the constraint.
semantic error: NoSuchIndex: `<name>` or semantic error: ConstraintNotFoundThe name does not exist.Add IF EXISTS, or check the name with SHOW.
  • Schema clauses: the grammar of index, constraint and graph type statements.
  • Tune a query: read a plan and a profile, and act on the planner's notifications.
  • Search: vector and full-text index options.
  • Errors: constraint failures and their exit codes, at stable anchors.