Содержание урока · 10 разделов
Импорт справочника на пятьсот записей через цикл Exec — это пятьсот отдельных сетевых рейсов: каждый запрос уезжает на сервер, ждёт ответа и только потом уступает место следующему. При локальной базе с задержкой в полмиллисекунды дорога съедает четверть секунды, при удалённой с двадцатью миллисекундами на рейс — десять секунд чистого ожидания, хотя сам сервер тратит на команду доли миллисекунды. pgx.Batch закрывает эту проблему: команды складываются в очередь, отправляются одним пакетом, и сервер обрабатывает их без пауз на дорогу — работает конвейер расширенного протокола, где запросы и ответы идут потоком, а не ходят парами запрос-ответ. API из трёх частей: batch := &pgx.Batch{} создаёт пачку, batch.Queue(sql, args...) ставит запрос в очередь вместе с аргументами, conn.SendBatch(ctx, batch) или pool.SendBatch возвращает BatchResults — ридер результатов. Метод Len() показывает число постановок и заслуживает места в каждом логе массовой операции. За экономию рейсов платят дисциплиной: результаты читаются строго в порядке очереди, Close обязателен, а поведение при сбое посреди пачки подчиняется правилам неявной транзакции. Терминологическая граница пригодится в разговорах с коллегами: транзакция из тринадцатого урока — про границу фиксации, пачка — про транспорт, и совпадают они только внешне. Порядок урока: механика очереди, порядок чтения, судьба пачки при ошибке, ловушка DDL и граница, где пачка уступает COPY из следующего урока.
Механика: Queue, SendBatch, BatchResults#
Очередь строится без сети: Queue только запоминает SQL и аргументы и возвращает QueuedQuery — указатель на постановку. SendBatch отправляет всё скопом и отдаёт BatchResults, дальше контракты совпадают с обычными вызовами: br.Exec() читает результат следующей команды, как обычный Exec — CommandTag и ошибку; br.Query() открывает курсор следующего SELECT со знакомым циклом rows.Next из пятого урока; br.QueryRow() даёт Row со Scan и ErrNoRows. Единственное правило — порядок: первый вызов читает результат первой постановки, второй — второй, и перепрыгнуть нельзя. Пачка живёт на одном коннекте: у pgx.Conn соединение занято с момента SendBatch до Close — документация формулирует это буквально, ридер обязан быть закрыт до следующего использования коннекта. Пул в pool.SendBatch берёт коннект из стока, а возвращает ровно в Close — в исходниках pgxpool Release вызывается именно там. Отсюда рефлекс: defer br.Close() пишется сразу после SendBatch, и между ними не должно быть ни одного return. Переиспользовать Batch нельзя: комментарий в исходниках гласит, что пачка отправляется один раз — новая операция собирает новый объект. Len перед отправкой — дешёвая проверка, что цикл поставил столько команд, сколько ожидалось: пустая пачка уедет и вернёт пустой набор, а лишний Exec на исчерпанной очереди вернёт ошибку no more results in batch — увидим её в интерактиве ниже.
Пятьсот платежей одной пачкой
GOfunc savePayments(ctx context.Context, pool *pgxpool.Pool,
payments []Payment) error {
batch := &pgx.Batch{}
for _, p := range payments {
batch.Queue(
"INSERT INTO payments (order_id, amount) VALUES ($1, $2)",
p.OrderID, p.Amount)
}
log.Printf("в пачке %d команд", batch.Len())
br := pool.SendBatch(ctx, batch)
defer br.Close()
for range payments {
if _, err := br.Exec(); err != nil {
return err
}
}
return nil
}Очередь собирается в памяти без сети, затем одна SendBatch увозит все INSERT за одну поездку. Чтение идёт циклом по числу постановок: br.Exec() выдаёт CommandTag каждой вставки по очереди. defer br.Close() стоит сразу после SendBatch — с пулом Close возвращает коннект в сток, без него сток истощается под нагрузкой. Лог batch.Len() перед отправкой ловит рассинхрон цикла и ожиданий ещё до поездки.
Неявная транзакция: пачка падает целиком#
Документация SendBatch формулирует жёстко: все запросы пачки выполняются в неявной транзакции, если вы сами не послали команды управления транзакциями. Практическое следствие: сбой посреди пачки откатывает уже выполненные команды и рвёт остаток — четвёртый INSERT упал, значит первые три не фиксируются, а пятый и дальше вернут ошибку. Пачка по умолчанию — атомарный блок: либо всё, либо ничего, как транзакция из тринадцатого урока, только без явных BEGIN и COMMIT. Нужен частичный успех — режьте операцию на несколько независимых пачек по бизнес-группам: упавшая группа не тянет остальные и повторяется отдельно. Формулировка «если не послали команды управления» — это не формальность: BEGIN и COMMIT можно положить в очередь обычными элементами пачки, и границы сместятся, но такой стиль быстро становится нечитаемым — явная tx.SendBatch из четырнадцатого урока честнее. Ошибка в середине — обычная PgError из шестнадцатого урока: br.Exec() вернёт её на соответствующем месте очереди, 23505 от дубликата валит пачку целиком, а все последующие чтения возвращают ту же ошибку — конвейер уже оборван. Отсюда правило идемпотентности: пачку после сбоя можно отправить заново целиком, неявный откат гарантирует, что дубликатов внутри не останется. Тонкость для продакшена: журналировать стоит только факт провала группы и её размер — собирать «сколько успело» бессмысленно, успело ноль.
Проверка: порядок результатов
В пачку поставили UPDATE, затем SELECT, затем ещё UPDATE. Как читать результаты?
Close и коннект: цена забытого ридера#
BatchResults.Close читает все непрочитанные результаты, выполняет зарегистрированные колбэки, завершает операцию и освобождает коннект — на уровне пула это буквальный Release. Забытый Close означает коннект, который не вернулся в сток: при MaxConns из четвёртого урока достаточно нескольких утечек в минуту, чтобы сервис замер на Acquire без единой ошибки в логе — просто медленно, потом наглухо. Рецепт двумя строками: defer br.Close() сразу после SendBatch, повторный вызов безопасен — исходники гарантируют, что второй Close вернёт ту же ошибку без побочных эффектов. Сам Close возвращает агрегированную ошибку пачки: если нечитанные результаты содержали сбой, вы узнаете здесь — в сценариях с неполным чтением ошибку Close стоит логировать отдельно, она последняя, кто видел проблему. Вторая грань — параллельность: пачка выполняется на одном коннекте последовательно, распараллелить её внутренности между соединениями нельзя; масштабирование достигается несколькими SendBatch из разных горутин, каждая со своей пачкой, и потолок задаёт размер пула. Диагностика свободна: pool.Stat() в срезе метрик показывает занятые и ожидающие Acquire — ступенька вниз на графике свободных коннектов сразу указывает на утечку ридеров. Третья грань — память: очередь живёт в приложении до отправки, миллион Queue с аргументами съедает сотни мегабайт, поэтому большие объёмы режьте на пачки по несколько тысяч команд или переходите на COPY.
Разнородная пачка: вставка, выборка, обновление
GObatch := &pgx.Batch{}
batch.Queue("INSERT INTO notes (title) VALUES ($1)", "черновик")
batch.Queue("SELECT count(*) FROM notes")
batch.Queue("UPDATE notes SET archived = true WHERE title = $1",
"черновик")
br := pool.SendBatch(ctx, batch)
defer br.Close()
if _, err := br.Exec(); err != nil {
log.Fatal(err)
}
var n int
if err := br.QueryRow().Scan(&n); err != nil {
log.Fatal(err)
}
if _, err := br.Exec(); err != nil {
log.Fatal(err)
}
fmt.Println("заметок:", n)Три разных контракта в одной пачке: Exec, QueryRow, снова Exec — вызовы идут ровно в порядке постановки. QueryRow внутри пачки ведёт себя как обычный: Scan и ErrNoRows на пустой выборке. Пачка уезжает одним пакетом, хотя команды разнородные: сетевая экономия не зависит от типа команд. Сбой порядка чтения тихо сместит результаты — третий вызов прочитает чужой ответ, и код получит не то, что ожидал.
Что вернёт третий Exec?
Прочитайте код и выберите, что он выведет:
batch := &pgx.Batch{}
batch.Queue("SELECT 1")
batch.Queue("SELECT 2")
br := pool.SendBatch(ctx, batch)
defer br.Close()
_, _ = br.Exec()
_, _ = br.Exec()
_, err := br.Exec()
fmt.Println(err)Выбор инструмента для множества команд#
| Инструмент | Что посылает | Объём | Сценарий |
|---|---|---|---|
| Цикл Exec | по одному рейсу на команду | единицы | разовые правки, скрипты обслуживания |
| pgx.Batch | все команды одним пакетом | десятки–тысячи | разнородные команды, справочники, пачки вставок |
| Batch внутри tx | пачка под явным BEGIN/COMMIT | десятки–тысячи | нужен свой контур фиксации относительно других операций |
| COPY FROM | бинарный поток строк | десятки тысяч+ | однотипная вставка без конфликтов — следующий урок |
Ловушка DDL и подготовленных стейтментов#
Дефолтный режим выполнения pgx — QueryExecModeCacheStatement: перед выполнением пачки новые запросы готовятся все сразу, одним конвейером описаний. Документация предупреждает о следствии прямо: если в одной пачке сначала CREATE TABLE, а потом INSERT в неё, вставка может упасть — на момент подготовки стейтмента таблицы ещё нет, хотя CREATE формально уже выполнен. Ловушка расставляет границы: DDL в пачку не кладётся — схема меняется отдельной поездкой через Exec, как в пятом уроке, а пачка работает только с данными. Вторая тонкость: постановка выполняется в дефолтном режиме коннекта, поэтому поведение пачки зависит от QueryExecMode — на простом протоколе пачка склеивается в одну строку через точку с запятой, и правила игры другие; тесты обязаны гоняться на том же режиме, что и продакшен, иначе проверяется чужой драйвер. Аргументы каждой постановки едут через расширенный протокол отдельно, значит правила типизации из восьмого урока действуют на каждую команду в отдельности — nil против пустой строки и прочие сюрпризы не растворяются в массовой отправке. Сверхдлинный SQL внутри Queue — сигнал, что логика живёт не там: пачка экономит дорогу, а не чинит кривой план.
История из практики: после рефакторинга хендлер начал возвращать ошибку до чтения всех результатов, и через час под нагрузкой сервис замер — коннекты пула прицеплены к ридерам пачек, Acquire стоит вечно, health-check красный. Если BatchResults не закрыт, то коннект пула не возвращается, даже когда все данные уже прочитаны. Лекарство мгновенное: defer br.Close() сразу после SendBatch, а ошибку Close логировать отдельно. После починки графики pool.Stat() показывают ровный пилообразный пул вместо ступеньки вниз, которая и выдала утечку.
Правила работы с Batch#
- Queue строит очередь без сети, SendBatch отправляет одним пакетом
- Результаты читаются строго в порядке постановки — перепрыгнуть нельзя
- defer br.Close() сразу после SendBatch: с пулом Close возвращает коннект
- Пачка отправляется один раз; новая операция — новый Batch
- По умолчанию пачка — неявная транзакция: сбой откатывает всё выполненное
- Частичный успех — несколько пачек по независимым группам
- DDL из пачек выносится: подготовка стейтментов опережает выполнение
- Миллионы однотипных строк — не batch, а COPY из следующего урока
Проверка: сбой в середине пачки
Пачка из пяти INSERT: третий падает с ошибкой. Что стало с первыми двумя?
Соберите пачку
Восстановите каркас: очередь, отправка, закрытие, чтение.
Соберите разнородную пачку
Расставьте строки: очередь из двух команд, отправка, чтение по порядку.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: pgx.Batch
Queue возвращает *QueuedQuery с методами Exec, Query и QueryRow, принимающими колбэк: qq := batch.Queue(sql, args); qq.Exec(func(ct pgconn.CommandTag) error { ... }). Колбэки выполняются в Close по нечитанным постановкам — удобно для мелких пачек без ручного счётчика. Грабля с условием: если вы прочитали постановку вручную через br.Exec(), то её колбэк уже не выполнится — Close гонит функции только по остатку очереди; а если один колбэк вернул ошибку, то последующие не вызываются вовсе, и ошибка выезжает из Close.
Когда batch, а когда COPY#
Граница проходит по однородности и объёму. Пачка — про разнородность: вставить справочник, обновить счётчики, посчитать итоги — всё за одну поездку и с разными типами результатов. COPY — про объём: тысячи однотипных INSERT в одну таблицу через пачку всё равно готовят стейтмент на каждую постановку и тащат каждый результат, а COPY гонит бинарный поток строк без SQL вообще — механизм следующего урока. Между ними лежит ступенька мульти-VALUES: INSERT ... VALUES (...), (...) собирает много строк в один запрос, но упирается в потолок расширенного протокола — 65535 параметров на оператор, значит максимум строк в пачке равен 65535, делённому на число колонок; для широких таблиц потолок низкий, а длинные VALUES тяжки для планировщика. Ориентиры: единицы команд — обычный Exec, десятки-сотни — batch, тысячи — уже вопрос, десятки тысяч — точно COPY; финальное слово за замером на вашей базе. Атомарность тоже различается: пачка откатывается целиком при сбое, COPY — тоже, но конфликты уникальности он разрешать не умеет вовсе. Следующий урок покажет бенчмарк на глазах: одна и та же загрузка через цикл и через CopyFrom, цифры рядом.
Пачки сберегают рейсы и требуют дисциплины взамен: порядок чтения, Close, помните о неявной транзакции. Песочница урока исполняет чистый Go без сети, а pgx.Batch живёт только рядом с настоящим Postgres — гоняйте примеры локально или в Docker, подняв контейнер из первого урока. Дальше по курсу: в девятнадцатом — COPY FROM и миллионы строк; в шестнадцатом — pgconn.PgError, который приедет из середины пачки; в тринадцатом — явные транзакции, рядом с которыми пачка получает предсказуемый контур фиксации; в пятнадцатом — дедлайн, который стоит надеть на любую большую отправку. Домашнее задание превращает тройку Queue, SendBatch, Close в рефлекс.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- pgx.Batch собирает очередь команд: Queue ставит, Len считает, SendBatch отправляет одним пакетом.
- Результаты читаются строго в порядке постановки: Exec, Query или QueryRow на каждый элемент очереди.
- defer br.Close() сразу после SendBatch — с пулом Close возвращает коннект, без него сток истощается.
- Пачка по умолчанию — неявная транзакция: сбой откатывает выполненное и рвёт остаток.
- Batch отправляется один раз; конкурентность — несколько пачек, а не переиспользование.
- DDL в пачку не кладётся: подготовка стейтментов опережает CREATE TABLE.
- Память очереди живёт в приложении: большие объёмы режутся на пачки или уходят в COPY.
- Колбэки QueuedQuery выполняются в Close по нечитанным постановкам.
Домашнее задание
Напишите загрузку пятисот тегов двумя способами: циклом Exec и через pgx.Batch, замерьте time.Since обоих на локальной базе и запишите разницу в комментарий. Сломайте третий INSERT пачки дубликатом и проверьте, что первые два откатились, а пятый вернул ошибку. Добавьте defer br.Close() и уберите его — посмотрите через pool.Stat(), как меняется число свободных коннектов. Соберите разнородную пачку из INSERT, SELECT и UPDATE и убедитесь, что чтение результатов по порядку — единственный работающий вариант.
Личные заметкиПусто
Вопросы и ответы по уроку
Batch и транзакция — это одно и то же?
Нет. Транзакция — граница фиксации данных, batch — транспорт: экономия сетевых рейсов. Пачка по умолчанию выполняется в неявной транзакции, поэтому внешне похожа на атомарную операцию, но назначение у неё другое. Для контроля границ оборачивайте пачку в явную tx.SendBatch — тогда фиксация согласована с остальными операциями вашей транзакции.
Сколько команд ставить в одну пачку?
Ориентир — от сотен до пары тысяч: очередь живёт в памяти приложения, а каждый стейтмент занимает место на сервере при подготовке. Сверх этого режьте на блоки и отправляйте последовательно. Точная граница зависит от размера аргументов и памяти — измеряйте на своих данных, а не по чужим цифрам.
Почему результаты нельзя читать в произвольном порядке?
Потому что BatchResults — ридер потока: сервер присылает результаты в порядке постановки, и каждый вызов выдаёт следующий. Индексов и ключей у результатов нет. Если нужен произвольный доступ, разложите результаты по переменным в порядке чтения или разбейте операцию на несколько пачек.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
MongoDB в Go: официальный драйвер mongo-driver v2
Курс MongoDB для Go: подключение, bson-теги, CRUD, фильтры, агрегации, индексы, транзакции и Change Streams. 25 уроков по mongo-driver v2.
- Подключаться к MongoDB и настраивать клиента
- Моделировать документы структурами и bson-тегами
- Делать CRUD, сложные фильтры и проекции
- Строить агрегации $match/$group/$lookup