Что мешает быстро вставить большие объемы данных в базу?
Что проверяют
Знаете ли вы, что скорость вставки в Go решает не язык, а протокол общения с базой, и умеете ли пользоваться пулом соединений и режимом COPY вместо цикла с Exec.
Как отвечать
Разложить причины по весу:
- строка за запрос:
db.Execв цикле это круговая задержка на каждую строку плюс неявная транзакция с записью в журнал. Отсюда начинается любая оптимизация; - правильный инструмент: у
pgxестьCopyFrom, то есть протоколCOPY, и он обгоняет пачкуINSERTна порядок. ЕслиCOPYнедоступен, собирают многострочныйINSERT, помня про ограничение в 65535 параметров расширенного протокола; - транзакции: коммит на каждую строку означает синхронный сброс журнала на диск. Пачка в тысячи строк на транзакцию убирает основную часть этих сбросов;
- пул соединений:
SetMaxOpenConnsиSetMaxIdleConns. Тысяча горутин запись не ускорит, они встанут в очередь за соединениями, а база всё равно упрётся в диск под журналом; - подготовленные запросы: через пулер в режиме транзакций они перестают работать, и у драйвера есть режим без них;
- на стороне базы: индексы, внешние ключи и триггеры отрабатывают на каждой строке, а синхронная репликация добавляет ожидание реплики к каждому коммиту.
Где спотыкаются
Раздают одну транзакцию нескольким горутинам. Транзакция живёт на одном соединении, параллельные запросы в ней выстраиваются в очередь, а при неаккуратном коде просто валятся ошибкой занятого соединения.
Второе: не закрывают rows и не проверяют ошибки. Соединение не возвращается в пул, пул исчерпывается, и загрузка не падает с понятным сообщением, а намертво встаёт на ожидании свободного соединения.
Третье: пытаются уложить миллион строк в один INSERT. Упираются в лимит параметров, в память процесса и во время разбора запроса, хотя смотреть надо было на обе стороны: часто Go занят формированием строк, а база в это время простаивает.