Importing data

This section documents the current Neo4j 2026.x calendar-versioned line — Neo4j moved from semantic to calendar versioning (YYYY.MM) in 2025, and the same line applies to the Graph Data Science library — as published at the Neo4j documentation, which is the reference these pages are written and verified against. No specific monthly patch is pinned. Some areas (Aura’s internal infrastructure, the Raft consensus implementation details, and the GDS Pregel API’s low-level internals) are linked, not documented in depth.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, as Neo4j iterates quickly.

This section’s bibliography lists the reference material consulted while preparing these pages.

Neo4j offers two very different ways to get data in: a transactional LOAD CSV for ongoing or moderate-sized loads against a running, online database, and an offline bulk loader for populating a new database (or adding to one) as fast as possible from flat files. Which one applies depends on whether the database needs to stay up and queryable while the data lands.

LOAD CSV with CALL \{ …​ \} IN TRANSACTIONS

LOAD CSV streams rows from a CSV file (local or over HTTP/HTTPS/S3) one at a time into a Cypher query, with WITH HEADERS exposing each column as a map key. Older guidance wrapped a whole LOAD CSV in a single implicit transaction and used USING PERIODIC COMMIT to commit every N rows so the transaction state would not grow unbounded — that clause was removed in Neo4j 5. The modern replacement wraps the per-row work in CALL \{ …​ \} IN TRANSACTIONS, which batches the inner unit of work into its own sub-transactions committed every rows rows:

// people.csv: personName,movieTitle,releaseYear,role
LOAD CSV WITH HEADERS FROM 'file:///people.csv' AS row
CALL {
  WITH row
  MERGE (p:Person {name: row.personName})
  MERGE (m:Movie {title: row.movieTitle})
    ON CREATE SET m.releaseYear = toInteger(row.releaseYear)
  MERGE (p)-[a:ACTED_IN]->(m)
    ON CREATE SET a.role = row.role
} IN TRANSACTIONS OF 500 ROWS

Each batch of 500 rows commits independently, so a failure partway through leaves earlier batches durable rather than rolling back the whole file — the trade-off is that the load as a whole is no longer atomic. CALL \{ …​ \} IN TRANSACTIONS can only appear as a top-level clause (not nested inside another transactional construct), and every variable the inner block needs — row here — must be passed in explicitly with WITH. Add ON ERROR CONTINUE to skip a batch that fails validation instead of aborting the whole load, and REPORT STATUS AS status to collect per-batch outcomes into a column for inspection.

Before loading, MERGE on a label needs a matching constraint or index or every MERGE becomes a label-scan; create it first:

CREATE CONSTRAINT person_name IF NOT EXISTS FOR (p:Person) REQUIRE p.name IS UNIQUE;
CREATE CONSTRAINT movie_title IF NOT EXISTS FOR (m:Movie) REQUIRE m.title IS UNIQUE;

LOAD CSV is the right tool for incremental loads, upserts (MERGE), and files up to a few million rows against a database that other clients are still using concurrently. See LOAD CSV for the full clause reference, including headerless files, custom field terminators, and remote URLs.

Bulk offline import and external sources

For an initial load of tens of millions of rows or more, LOAD CSV is far slower than writing the store files directly. neo4j-admin database import builds a new database’s files from CSV, bypassing the transaction log and query engine entirely — the target database must not exist yet (for full) and the DBMS must be offline for that database while the import runs.

# Full import: builds a brand-new database from scratch (DBMS must be stopped, or the
# target database must not exist).
neo4j-admin database import full moviedb \
  --nodes=Person=people.csv \
  --nodes=Movie=movies.csv \
  --relationships=ACTED_IN=roles.csv \
  --delimiter=","

# Incremental import: adds nodes/relationships/properties from more CSV files into an
# EXISTING database created by neo4j-admin, without a full reload.
neo4j-admin database import incremental moviedb \
  --force \
  --nodes=Person=new_people.csv \
  --relationships=ACTED_IN=new_roles.csv

The CSV headers for neo4j-admin import carry Neo4j’s own type and role syntax directly (:ID, :LABEL, :START_ID, :END_ID, :TYPE, name:string, …​), which is a different header convention from the plain-column CSV that LOAD CSV reads. incremental runs in stages and can resume a partially completed run with --stage=<name> if it is interrupted. For a GUI alternative on a single desktop instance, Neo4j Desktop's Data Importer tool maps CSV columns to node labels, relationship types and properties visually and drives the same import machinery underneath — useful for a one-off load without hand-writing the header syntax.

Neither tool reaches into another live system. To pull rows from a REST API, a JSON file, or a relational database directly inside Cypher, use the APOC procedures apoc.load.json (a URL or file path, walked as a stream of maps) and apoc.load.jdbc (a JDBC connection string and SQL query, streamed row by row) — both are typically driven through the same CALL \{ …​ \} IN TRANSACTIONS batching shown above rather than loaded as one giant transaction. See APOC and extensions for setup and more procedures.

Reference: Import (overview of both import paths), neo4j-admin import tutorial (worked walkthrough of the header syntax and multi-file imports), and CSV import (CSV shape and encoding requirements shared by both tools).

Choosing an approach

Tool Database state Best for

LOAD CSV + CALL \{ \} IN TRANSACTIONS

Online, concurrently used

Incremental loads, upserts, small-to-moderate files

neo4j-admin database import full

Offline, database must not exist

Very large initial bulk loads

neo4j-admin database import incremental

Offline, database already exists

Very large follow-up bulk loads

apoc.load.json / apoc.load.jdbc

Online

Pulling from an external API, file, or relational database

The same online-vs-offline split exists elsewhere: MongoDB separates ongoing writes from bulk tooling like mongoimport (see MongoDB schema design), and a relational engine separates row-by-row INSERT from bulk-loading utilities such as COPY or LOAD DATA (see SQL reference) — in every case, bypassing the normal write path trades transactional safety and concurrent access for raw load throughput.