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
- A first look at Cypher, such as your first graph.
- For the terminal steps, relate on your
PATH(install).
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.
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)(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.
| Kind | Statement | Serves |
|---|---|---|
| Range, the default | CREATE INDEX <name> FOR (n:<Label>) ON (n.<prop>); CREATE RANGE INDEX is the same; ON (n.<a>, n.<b>) covers two properties | Equality and range comparisons on the property, IN a list, and STARTS WITH |
| Range, on relationships | CREATE 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 |
| Text | CREATE TEXT INDEX <name> FOR (n:<Label>) ON (n.<prop>) | CONTAINS on a string property |
| Point | CREATE POINT INDEX <name> FOR (n:<Label>) ON (n.<prop>) | point.distance(<prop>, <center>) < <bound> and point.withinBBox (geospatial queries) |
| Full-text | CREATE FULLTEXT INDEX <name> FOR (n:<Label>) ON EACH [n.<prop>, …] | SEARCH … FULLTEXT INDEX (full-text and hybrid search) |
| Vector | CREATE VECTOR INDEX <name> FOR (n:<Label>) ON (n.<prop>) OPTIONS {…} | SEARCH … VECTOR INDEX (vector search) |
| Lookup | CREATE 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:
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)(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.
| Kind | Statement | Refuses |
|---|---|---|
| Uniqueness | CREATE 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 NULL | A node or relationship without the property, or a REMOVE of it |
| Property type | … REQUIRE n.<prop> IS :: <TYPE>, such as INTEGER or STRING | A value of another type |
| Node key | … REQUIRE (n.<a>, n.<b>) IS NODE KEY | A node missing any of the properties, or repeating their combination |
| Relationship key | FOR ()-[r:<TYPE>]-() REQUIRE r.<prop> IS RELATIONSHIP KEY | The 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:
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(no rows) · constraints added: 3
A second relate project now fails, and the statement writes nothing:
CREATE (:Project {name: 'relate', language: 'Go'})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":
MERGE (p:Project {name: 'relate'})
ON CREATE SET p.language = 'Go'
RETURN p.name AS name, p.language AS language| name | language |
|---|---|
| "relate" | "Rust" |
1 row
| name | language | |----------|----------| | "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:
CREATE CONSTRAINT project_tag FOR (p:Project) REQUIRE p.tag IS NOT NULLerror: 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:
SHOW CONSTRAINTS YIELD name, type, entityType, labelsOrTypes, properties, propertyType
RETURN name, type, entityType, labelsOrTypes, properties, propertyType
ORDER BY name| name | type | entityType | labelsOrTypes | properties | propertyType |
|---|---|---|---|---|---|
| "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
| name | type | entityType | labelsOrTypes | properties | propertyType | |------------------------|------------------------------|----------------|---------------|------------|--------------| | "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
SHOW INDEXES YIELD name, type, labelsOrTypes, properties, owningConstraint, state
RETURN name, type, labelsOrTypes, properties, owningConstraint, state
ORDER BY name| name | type | labelsOrTypes | properties | owningConstraint | state |
|---|---|---|---|---|---|
| "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
| name | type | labelsOrTypes | properties | owningConstraint | state | |--------------------|---------|---------------|------------------|------------------|----------| | "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:
EXPLAIN MATCH (p:Project {language: 'Rust'}) RETURN p.name AS name(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:
03C91Missing 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:
DROP INDEX person_name_role IF EXISTS;
DROP CONSTRAINT person_role_required IF EXISTS(no rows) · indexes removed: 1, constraints removed: 1
An index that belongs to a constraint cannot be dropped on its own:
DROP INDEX project_nameerror: Unable to drop 'project_name': the index belongs to a constraint.
Verify
List what the store holds after the drops:
SHOW INDEXES YIELD name, type, owningConstraint, state
RETURN name, type, owningConstraint, state
ORDER BY name| name | type | owningConstraint | state |
|---|---|---|---|
| "person_role" | "RANGE" | null | "ONLINE" |
| "project_language" | "TEXT" | null | "ONLINE" |
| "project_name" | "RANGE" | "project_name" | "ONLINE" |
3 rows
| name | type | owningConstraint | state | |--------------------|---------|------------------|----------| | "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
| Message | Cause | Remedy |
|---|---|---|
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 properties | A 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 type | A value of another type than the constraint names. | Convert the value, such as toInteger(row.since). |
semantic error: IndexAlreadyExists or semantic error: ConstraintAlreadyExists | The name is taken. | Add IF NOT EXISTS, or check SHOW INDEXES and SHOW CONSTRAINTS. |
semantic error: EquivalentSchemaRuleAlreadyExists | An 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: ConstraintNotFound | The name does not exist. | Add IF EXISTS, or check the name with SHOW. |
Related
- 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.