Schema and indexes
Index-backed key constraints and type constraints
Every entity that an ingestion will MATCH or MERGE against should have a constraint and an index on whatever property identifies it, for the following reasons:
-
An index is needed because, without one, matching against a property means scanning every node with the relevant label, one at a time, rather than looking up the value directly.
-
A constraint is needed because, without one, ingestion has no guarantee of correctness: a
MATCHagainst a missing value silently matches nothing rather than erroring, andMERGEhas no way to enforce that the same identifying value always resolves to the same node rather than creating a duplicate.
A key constraint solves both: a backing index gets created automatically, while the uniqueness and existence of the property are also enforced, which is why a key constraint is preferred over a stand-alone index or uniqueness constraint.
Property type constraints (not index backed) can also be set prior to ingestion to ensure type consistency, though it is recommended to cast into the desirable data type prior to, or when merging.
Key and property type constraints can either be created as part of a holistic schema definition (graph type), or independently. The former offers additional, more sophisticated constraint types and a simplified approach for constraining and maintaining the shape of the data in a graph. It is, therefore, recommended to use graph types to enforce a schema.
| Graph types were introduced in Neo4j 2026.02, and are only available using Cypher 25. |
ALTER CURRENT GRAPH TYPE SET {
(n:Label => { id :: INTEGER NOT NULL }) REQUIRE n.id IS KEY
}
CREATE CONSTRAINT id_key_constraint FOR (n:Label) REQUIRE (n.id) IS NODE KEY;
CREATE CONSTRAINT id_type_constraint FOR (n:Label) REQUIRE (n.id) IS :: INTEGER;
|
When ingesting data, an index should only be created on the properties actually used by the ingestion (e.g., An index not needed for ingestion can be added later, once the data is loaded, if application queries turn out to need it. A constraint is different: it can still be added later, but only if the existing data already satisfies it. |
Schema and index verification
Before beginning an ingestion, there a few checks that can be performed to ensure the schema and indexes are in place and correct.
Verify the schema has been created
Before ingesting, confirm the schema was actually applied using either the SHOW CURRENT GRAPH TYPE or SHOW CONSTRAINTS command (constraints created outside of a graph type will be visible in the output of the former, and the constraints comprising the elements set in a graph type will appear in the latter).
SHOW CURRENT GRAPH TYPE
As of Neo4j 2026.07, a visual representation of a graph type can be returned by appending AS GRAPH to the SHOW CURRENT GRAPH TYPE command (i.e., SHOW CURRENT GRAPH TYPE AS GRAPH).
|
Verify indexes have been created and are online before ingesting
Creating an index, including one backing a constraint, does not mean it is immediately usable.
The backing index populates asynchronously, and a large ingestion run started before this process is finished will not benefit from it.
It is, therefore, necessary to verify both that an index has been created and that its state is ONLINE, as opposed to POPULATING.
SHOW RANGE INDEXES
YIELD name, type, state, labelsOrTypes, properties
Verify use of index in import with EXPLAIN
Before running an import, it is recommended to run an EXPLAIN of the import.
This will generate a plan for how the ingestion will be executed, without actually executing it.
Confirm the plan uses an index-backed operator (e.g., MergeUniqueNode) for the ingestion pattern, rather than a scan operator such as NodeByLabelScan, since this would mean every node with a specific label, gets checked one at a time.
Watch for typos in property names
A MERGE will only honor a constraint if the property name matches exactly (including case).
A subtle typo between the constraint definition and the MERGE pattern means the constraint offers no protection and duplicate nodes may be created silently with no error thrown.
Book:isbnCREATE CONSTRAINT book_isbn_key IF NOT EXISTS
FOR (b:Book) REQUIRE b.isbn IS KEY
UNWIND $rows AS row
MERGE (b:Book {isbn: row.Isbn})
row.Isbn (capital I) silently creates a different property than isbn, the one the constraint was actually created on.
Every row now creates a new, unconstrained Book node rather than resolving to an existing one, since the constraint never applies to Isbn.
Checking with EXPLAIN (previous section) helps catch this, since a mismatched property name will not be able to use the intended index at all, and shows up as a scan instead.
Post-ingestion verification
Once ingestion completes, compare recorded source counts to what actually landed in the database.
Compare this against the row count recorded from the source file before ingestion began.
If the count is lower than expected, check whether rows were silently dropped, for example by a MATCH that found nothing due to a missing reference.
If the count is higher than expected, check whether the relevant constraints were actually in place at ingestion time.