Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 18 из 25 ~20 мин +20 XP

Batch: сотни запросов за один рейс

Массовые запросы из Go к Postgres по умолчанию платят сетевым рейсом за каждую команду, и pgx.Batch убирает этот налог: Queue складывает сотни команд в очередь, SendBatch отправляет их одной поездкой, а BatchResults отдаёт результаты строго в порядке постановки. Разберём механику пачки, неявную транзакцию, цену забытого Close, ловушку DDL и границу, где batch уступает COPY.

Содержание урока · 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 — увидим её в интерактиве ниже.

Пятьсот платежей одной пачкой

GO
func 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.

Разнородная пачка: вставка, выборка, обновление

GO
batch := &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 — сигнал, что логика живёт не там: пачка экономит дорогу, а не чинит кривой план.

Правила работы с Batch#

  • Queue строит очередь без сети, SendBatch отправляет одним пакетом
  • Результаты читаются строго в порядке постановки — перепрыгнуть нельзя
  • defer br.Close() сразу после SendBatch: с пулом Close возвращает коннект
  • Пачка отправляется один раз; новая операция — новый Batch
  • По умолчанию пачка — неявная транзакция: сбой откатывает всё выполненное
  • Частичный успех — несколько пачек по независимым группам
  • DDL из пачек выносится: подготовка стейтментов опережает выполнение
  • Миллионы однотипных строк — не batch, а COPY из следующего урока

Проверка: сбой в середине пачки

Пачка из пяти INSERT: третий падает с ошибкой. Что стало с первыми двумя?

Соберите пачку

Восстановите каркас: очередь, отправка, закрытие, чтение.

batch := &pgx.{} batch.Queue("INSERT INTO tags (name) VALUES ($1)", "go") br := pool.(ctx, batch) defer br.() if _, err := br.(); err != nil { return err }

Соберите разнородную пачку

Расставьте строки: очередь из двух команд, отправка, чтение по порядку.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: pgx.Batch

Термин

pgx.Batch

нажмите, чтобы перевернуть
Определение

Очередь запросов для отправки одним пакетом. Queue ставит команду, Len считает, объект отправляется один раз.

1 / 6

Когда 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 и убедитесь, что чтение результатов по порядку — единственный работающий вариант.

Личные заметкиПусто
Хранятся только в вашем браузере (pgx · Batch: сотни запросов за один рейс)

Вопросы и ответы по уроку

Batch и транзакция — это одно и то же?

Нет. Транзакция — граница фиксации данных, batch — транспорт: экономия сетевых рейсов. Пачка по умолчанию выполняется в неявной транзакции, поэтому внешне похожа на атомарную операцию, но назначение у неё другое. Для контроля границ оборачивайте пачку в явную tx.SendBatch — тогда фиксация согласована с остальными операциями вашей транзакции.

Сколько команд ставить в одну пачку?

Ориентир — от сотен до пары тысяч: очередь живёт в памяти приложения, а каждый стейтмент занимает место на сервере при подготовке. Сверх этого режьте на блоки и отправляйте последовательно. Точная граница зависит от размера аргументов и памяти — измеряйте на своих данных, а не по чужим цифрам.

Почему результаты нельзя читать в произвольном порядке?

Потому что BatchResults — ридер потока: сервер присылает результаты в порядке постановки, и каждый вызов выдаёт следующий. Индексов и ключей у результатов нет. Если нужен произвольный доступ, разложите результаты по переменным в порядке чтения или разбейте операцию на несколько пачек.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

MongoDB25 уроков~12 чпрактика25 уроков уже доступны

MongoDB в Go: официальный драйвер mongo-driver v2

Курс MongoDB для Go: подключение, bson-теги, CRUD, фильтры, агрегации, индексы, транзакции и Change Streams. 25 уроков по mongo-driver v2.

  • Подключаться к MongoDB и настраивать клиента
  • Моделировать документы структурами и bson-тегами
  • Делать CRUD, сложные фильтры и проекции
  • Строить агрегации $match/$group/$lookup