Schema and indexes

Schema and indexes make ingestion correct and fast at scale: a schema ensures the data being written is valid and consistent, while indexes ensure the database can resolve MATCH/MERGE operations against it without the cost of a full scan on every row.

For full information about Cypher®’s indexes and schema options, see the Cypher Manual → Indexes and Schema pages.

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 MATCH against a missing value silently matches nothing rather than erroring, and MERGE has 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.
Graph type with node element type setting both a type and key constraint on the id property
ALTER CURRENT GRAPH TYPE SET {
  (n:Label => { id :: INTEGER NOT NULL }) REQUIRE n.id IS KEY
}
Independent property type and key constraints on id property
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., n.id). Indexing anything else adds maintenance overhead on every write without providing any benefit during the load.

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).

Ensure graph type is applied before ingestion
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.

Ensure indexes are created and online before ingestion
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.

Create key constraint on Book:isbn
CREATE CONSTRAINT book_isbn_key IF NOT EXISTS
FOR (b:Book) REQUIRE b.isbn IS KEY
Ingest Book nodes with Isbn properties
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.