Выгрузка из PostgreSQL в CSV через COPY оказалась самым быстрым и экономным по памяти способом среди пяти протестированных: база сама формирует файл, и в Node.js не создаётся ни одного объекта-строки. Серверный курсор идёт вровень с keyset-пагинацией по памяти и немного быстрее по времени, а OFFSET/LIMIT отстаёт по времени примерно в пять раз, оставаясь экономным по памяти.

Замеры провели на таблице примерно в 340 тысяч строк, CSV-файл весил около 37 МБ. Каждый способ запускали в отдельном процессе: поднимали инстанс, делали один запрос и гасили его, пик памяти снимали по heapUsed и rss каждые 25 мс с принудительной сборкой мусора перед прогоном. Автор оценивает результаты как примерные: прогонов было несколько, но пропорции оставались примерно одинаковыми.

Серверный курсор в PostgreSQL открывается через pg-query-stream, в Knex для этого есть метод.stream() с теми же фильтрами и сортировкой. Поток с backpressure запрашивает следующую порцию строк, только когда буфер не переполнен. Минус — транзакция и соединение держатся открытыми всю выгрузку, поэтому сотня параллельных долгих экспортов займёт сотню соединений на всё время скачивания; finally с destroy() обязателен, иначе оборванное скачивание оставит курсор и соединение висеть до таймаута.

COPY (SELECT…) TO STDOUT WITH (FORMAT csv) форматирует результат внутри базы, поток байт идёт из сокета БД, а pg-copy-streams оборачивает это в Readable. COPY тоже даёт согласованный целостный экспорт и тоже держит одно долгое соединение. Билдер остаётся только для сборки SELECT, дальше идёт сырой SQL: COPY не принимает bind-параметры, значения фильтров попадают в текст команды, и за этим нужно следить, чтобы не допустить SQL-инъекций.

Формат при COPY задаёт PostgreSQL, поэтому вывод не совпадает с ручной сборкой: булевы значения приходят как t и f, время — в формате PostgreSQL со смещением, а не ISO с Z, BOM для Excel и нейтрализацию формул (=, +, -, @) нужно добавлять отдельным преобразованием поверх потока. Четыре способа со сборкой CSV в JS дали байт в байт одинаковый файл, COPY сравнивали только по времени и памяти.

Если CSV нужен для машинной обработки и особое форматирование не требуется, можно брать COPY. Когда нужно собственное форматирование в Node.js, удобнее серверный курсор. Keyset остаётся разумной серединой, если согласованный снимок не нужен, а соединение в пуле держать не хочется — например, при выгрузке логов по ID или по времени и ID, когда данные не меняются.