Most graphs start as tables somewhere, and a correct, batched import is the first real project task.
LOAD CSV reads a CSV file row by row, from file:/// (the server's import directory) or an https:// URL. With WITH HEADERS each row is a map keyed by column name; without it, a list.
Every value arrives as a string. Convert explicitly: toInteger(row.age), toFloat, date(row.joined), split(row.tags, ';'). Empty fields become null; coalesce supplies defaults.
A robust import follows a pattern:
- create constraints first, so MERGE uses an index instead of scanning
- load nodes in one pass with
MERGEon the key, then relationships in a second pass that MATCHes both ends - for large files, batch with
CALL (row) { ... } IN TRANSACTIONS OF 10000 ROWSso each batch commits separately instead of building one gigantic transaction
Batched subqueries must run in an auto-commit (implicit) transaction; in Neo4j Browser prefix the query with :auto, and from drivers use session.run. Add ON ERROR CONTINUE or REPORT STATUS to control failures per batch.
For tens of millions of rows into a new database, the offline neo4j-admin database import tool is far faster. The older CALL { WITH row ... } importing-WITH form still works but is deprecated in favour of CALL (row) { ... }.
Example
// people.csv: email,name,born
// follows.csv: from,to,since
CREATE CONSTRAINT person_email IF NOT EXISTS
FOR (p:Person) REQUIRE p.email IS UNIQUE;
LOAD CSV WITH HEADERS FROM 'file:///people.csv' AS row
CALL (row) {
MERGE (p:Person {email: row.email})
SET p.name = row.name, p.born = toInteger(row.born)
} IN TRANSACTIONS OF 10000 ROWS;
LOAD CSV WITH HEADERS FROM 'file:///follows.csv' AS row
CALL (row) {
MATCH (a:Person {email: row.from}), (b:Person {email: row.to})
MERGE (a)-[f:FOLLOWS]->(b)
SET f.since = toInteger(row.since)
} IN TRANSACTIONS OF 10000 ROWS;Remember
- All CSV values are strings: convert with toInteger, date, split
- Constraints first, then nodes, then relationships
- CALL (row) { ... } IN TRANSACTIONS batches large imports
- Use neo4j-admin import for huge initial loads
Watch out for
- Importing without a constraint on the MERGE key, making each row scan every node of the label.