Импорт данных через LOAD CSV
Самый частый способ наполнить граф реальными данными — построчная загрузка из CSV.
LOAD CSV — конструкция Cypher, которая читает CSV-файл построчно и позволяет на каждой строке выполнять CREATE/MERGE, превращая таблицу в граф.
Граф редко начинают с пустого места: данные почти всегда уже где-то лежат — в реляционной базе, в выгрузке из CRM, в Excel-таблице. Самый прямой мост между этим табличным миром и графом — формат CSV и конструкция LOAD CSV. Идея проста: вы выгружаете данные в плоские CSV-файлы (отдельно сущности, отдельно связи), а Cypher читает их построчно и на каждой строке решает, какой узел создать или какое ребро провести. Так таблица превращается в граф. Этот урок — про то, как сделать такой импорт быстрым, повторяемым и не убивающим сервер.
Базовая загрузка узлов
LOAD CSV отдаёт каждую строку как переменную, по которой можно строить узлы. С заголовками доступ идёт по имени колонки:
LOAD CSV WITH HEADERS FROM 'file:///people.csv' AS row
MERGE (p:Person {id: row.id})
SET p.name = row.name, p.born = toInteger(row.born)MERGE (а не CREATE) делает загрузку идемпотентной — повторный импорт не наплодит дублей. toInteger приводит строку CSV к числу: из файла всё приходит текстом.
Слово «идемпотентность» здесь ключевое. Реальный импорт — это не «запустил один раз и забыл»: файл могут перезалить с исправлениями, процесс может упасть на середине и повторно стартовать, данные приходят инкрементально каждую ночь. С CREATE каждый повторный прогон плодил бы дубли одного и того же человека. С MERGE по ключу id повторный запуск либо находит уже существующий узел, либо создаёт новый — результат всегда один и тот же, сколько ни запускай. Поэтому SET внутри идёт после MERGE: он обновляет свойства и у новых, и у уже существовавших узлов, аккуратно «доводя» данные до актуального состояния.
Загрузка связей
Рёбра грузят из отдельного файла, находя оба конца по ключу и MERGE-я связь:
LOAD CSV WITH HEADERS FROM 'file:///knows.csv' AS row
MATCH (a:Person {id: row.from})
MATCH (b:Person {id: row.to})
MERGE (a)-[r:ЗНАЕТ]->(b)
SET r.since = toInteger(row.since)Перед этим на :Person(id) обязательно должно стоять ограничение уникальности — иначе MATCH будет медленным, а MERGE рискует дублями.
Почему ограничение так важно именно при загрузке связей? Импорт миллиона рёбер делает два MATCH на каждую строку — это два миллиона поисков узла по ключу. Без индекса каждый такой поиск — полное сканирование всех людей, и импорт растягивается на часы. Ограничение уникальности CREATE CONSTRAINT FOR (p:Person) REQUIRE p.id IS UNIQUE попутно создаёт индекс, превращая каждый поиск из сканирования в мгновенный lookup. Заодно оно физически запрещает завести двух людей с одним id — страховка от грязных данных в источнике. Обратите внимание и на форму MERGE для связи: MERGE (a)-[r:ЗНАЕТ]->(b) ищет ребро именно между уже найденными a и b, поэтому повторный импорт не задвоит дружбу.
Батчи для больших файлов
Грузить миллион строк одной транзакцией опасно — память переполнится. Современный синтаксис разбивает импорт на батчи:
LOAD CSV WITH HEADERS FROM 'file:///big.csv' AS row
CALL {
WITH row
MERGE (p:Person {id: row.id})
SET p.name = row.name
} IN TRANSACTIONS OF 1000 ROWSIN TRANSACTIONS OF 1000 ROWS коммитит каждые 1000 строк, удерживая память под контролем.
Логика батчей такая же, как в уроке про транзакции: всё, что транзакция изменила, копится в памяти до commit. Одна транзакция на миллион строк — это миллион непримененных изменений в RAM, и сервер рискует упасть с нехваткой памяти. Подзапрос CALL { ... } IN TRANSACTIONS OF 1000 ROWS разбивает работу на множество маленьких транзакций по тысяче строк: каждая коммитится и освобождает память, прежде чем начнётся следующая. Размер батча подбирают опытно: слишком мелкий — лишние накладные на коммиты, слишком крупный — снова давление на память; 1000–10000 строк обычно хороший старт.
Порядок имеет значение
Сначала грузят все узлы, потом все связи. Если попытаться создать связь раньше, чем существуют её узлы, MATCH не найдёт концы и ребро не появится. Поэтому канонический порядок: constraints → узлы → связи.
Этот порядок — частая причина «молчаливых» багов импорта: ошибки нет, но половина связей не создалась. Разберём почему. Связь грузится через два MATCH, которые ищут уже существующие узлы. Если соответствующий человек ещё не загружен (его строка идёт в другом файле или позже), MATCH вернёт пусто, а MERGE для связи просто не выполнится — без всякой ошибки, ведь искать-то между чем. Поэтому канон железный: сначала CREATE CONSTRAINT (чтобы индекс был готов к моменту массовых поисков), затем загрузка всех узлов всех типов, и только в самом конце — все связи. Если перепутать, граф окажется «дырявым», и понять это удастся лишь по недобору рёбер.
Как работает под капотом
LOAD CSV — это итератор по строкам файла, встроенный в выполнение Cypher. Каждая строка проходит через тело запроса как отдельная единица потока. Для очень больших импортов (десятки миллионов узлов) у Neo4j есть отдельный офлайн-инструмент neo4j-admin import, который строит хранилище напрямую, минуя транзакции, — он на порядки быстрее, но требует пустой базы. LOAD CSV же удобен для инкрементальных и средних загрузок в живую базу.
Частые ошибки
- Грузить без ограничения на ключ. MATCH/MERGE по неиндексированному ключу на больших данных черепашьи медленный.
- Забыть приведение типов. Из CSV всё строки; числа и даты нужно
toInteger/toFloat/date(). - Связи раньше узлов. Канон: узлы, затем рёбра.
- Один гигантский импорт. Используйте
IN TRANSACTIONS OF N ROWSдля батчей.
Итоги
LOAD CSV WITH HEADERSчитает файл построчно; на каждой строке строим узлы/связи.MERGEделает импорт идемпотентным; типы приводим (toIntegerи др.).- Порядок: constraints → узлы → связи; ключ обязательно проиндексирован.
- Большие файлы грузим батчами
IN TRANSACTIONS OF N ROWS; для гигантских —neo4j-admin import.