Что мешает быстро вставить большие объемы данных в базу?
Что проверяют
Умеете ли вы разложить проблему по слоям, клиент, сеть, движок, диск, и назвать главный фактор, а не перечислить всё подряд. Плюс знание платформы: чем SqlBulkCopy принципиально отличается от цикла с INSERT.
Как отвечать
Главная причина почти всегда одна: данные вставляются по строке за запрос. Каждая строка это круговая задержка и отдельная транзакция, а каждая транзакция это принудительная запись в журнал. Дальше по убыванию веса:
- клиент: EF Core с
SaveChangesна каждую сущность и разрастающийся трекер изменений. На массовой заливке берутSqlBulkCopyили пакетную вставку, а контекст пересоздают порциями; - транзакции и журнал: пачки по несколько тысяч строк в одной транзакции вместо строки за строкой и вместо одной транзакции на весь объём;
- индексы и ключи: каждая вставка обновляет все индексы таблицы. Случайный
Guidв роли кластерного ключа добавляет расщепления страниц и фрагментацию, последовательный ключ их убирает; - проверки: внешние ключи, триггеры, вычисляемые колонки и индексированные представления отрабатывают на каждой строке;
- блокировки: конкурирующие читатели и эскалация блокировок до уровня таблицы;
- диск: запись журнала синхронна, и медленный том под ним ограничивает всё остальное.
Где спотыкаются
Заливают сотни тысяч строк через EF Core с одним контекстом. Трекер держит все сущности, память растёт, а каждое SaveChanges начинает перебирать всё, что уже накопилось, поэтому загрузка замедляется по ходу дела.
Второе: заворачивают весь объём в одну транзакцию. Журнал разрастается до размера данных, блокировки держатся до конца, а откат на последней строке стоит столько же времени, сколько сама загрузка.
Третье: оптимизируют вслепую. Пока не сняты ожидания сервера, непонятно, упирается загрузка в журнал, в блокировки или вообще в однопоточную подготовку данных на стороне приложения.