Какие есть способы реализовать пакетное обновление (batch update) записей в базе данных на Go?

Ответ

Пакетное обновление (batch update) — это эффективный способ обновить множество строк в базе данных за один раз, что значительно снижает сетевые задержки и нагрузку на БД по сравнению с отправкой отдельных UPDATE запросов для каждой строки.

Рассмотрим несколько популярных подходов:

1. Множественные UPDATE в одной транзакции с pgx.Batch

Это самый простой и идиоматичный способ в Go при работе с pgx. Все запросы отправляются на сервер за один раунд-трип.

func batchUpdateWithPgx(ctx context.Context, conn *pgx.Conn, items []Item) error {
    batch := &pgx.Batch{}
    for _, item := range items {
        batch.Queue("UPDATE my_table SET value = $1 WHERE id = $2", item.Value, item.ID)
    }

    br := conn.SendBatch(ctx, batch)
    defer br.Close() // Важно закрывать BatchResults

    // Проверяем ошибки для каждой выполненной команды
    for i := 0; i < len(items); i++ {
        if _, err := br.Exec(); err != nil {
            return fmt.Errorf("error in batch on item %d: %w", i, err)
        }
    }

    return nil
}

Плюсы: Простота, хороший контроль над ошибками. Минусы: Может быть не так производительно, как один SQL-запрос для очень больших батчей.

2. UPDATE с выражением CASE

Этот подход формирует один большой SQL-запрос, который обновляет разные строки разными значениями.

UPDATE my_table
SET
    column1 = CASE id
        WHEN 1 THEN 'new_value_1'
        WHEN 2 THEN 'new_value_2'
        -- ... и так далее
    END,
    column2 = CASE id
        WHEN 1 THEN 100
        WHEN 2 THEN 200
        -- ... и так далее
    END
WHERE id IN (1, 2, ...);

Плюсы: Выполняется как один атомарный запрос, очень эффективно для средних батчей. Минусы: Требует динамической генерации SQL, что может быть сложно и небезопасно (риск SQL-инъекций, если не использовать плейсхолдеры).

3. Использование временной таблицы

Это самый мощный подход для очень больших объемов данных.

  1. Создать временную таблицу: CREATE TEMP TABLE updates (...)
  2. Загрузить данные: Использовать COPY FROM (самый быстрый способ в PostgreSQL) для массовой вставки данных во временную таблицу.
  3. Обновить основную таблицу: Выполнить один UPDATE с JOIN (или FROM в PostgreSQL) с временной таблицей.
-- Шаг 3
UPDATE my_table
SET column1 = u.column1
FROM updates AS u
WHERE my_table.id = u.id;

Плюсы: Наилучшая производительность для десятков тысяч и более обновлений. Минусы: Более сложная реализация, требует нескольких шагов.

Общие рекомендации:

  • Используйте транзакции: Всегда оборачивайте пакетные операции в транзакцию для обеспечения атомарности.
  • Ограничивайте размер батча: Не делайте батчи слишком большими (обычно 100-1000 записей — хороший старт), чтобы не исчерпать память или не вызвать длительные блокировки в БД.
  • Выбирайте метод по задаче: Для большинства случаев pgx.Batch — лучший выбор. Для сложных обновлений — CASE. Для массовых миграций — временные таблицы.

Ответ 18+ 🔞

Слушай, а вот этот ваш пакетный апдейт — это ж, блядь, просто песня, а не технология! Вместо того чтобы дрочить базу тысячей мелких запросов, ты одним махом, как говорится, всё и делаешь. Серьёзно, нагрузка падает в разы, а скорость — просто овердохуищная.

Смотри, какие есть варианты, чтобы не облажаться:

1. Старый добрый pgx.Batch — просто, как три копейки

Ты просто пакуешь кучу UPDATE-ов в одну пачку и шлёшь их разом. База их принимает, обрабатывает и не морщится, как от спама.

func batchUpdateWithPgx(ctx context.Context, conn *pgx.Conn, items []Item) error {
    batch := &pgx.Batch{}
    for _, item := range items {
        batch.Queue("UPDATE my_table SET value = $1 WHERE id = $2", item.Value, item.ID)
    }

    br := conn.SendBatch(ctx, batch)
    defer br.Close() // Эту хуйню закрывать надо обязательно, а то память потечёт!

    // А тут проверяем, не накосячили ли где
    for i := 0; i < len(items); i++ {
        if _, err := br.Exec(); err != nil {
            return fmt.Errorf("error in batch on item %d: %w", i, err)
        }
    }

    return nil
}

Что хорошо: Пишешь три строчки и уже герой. Ошибки ловить удобно. Что не очень: Если ты решишь обновить, блядь, миллион строк одним батчем — может и не взлететь, будет тормозить.

2. Хитрый UPDATE с CASE — для тех, кто любит головоломки

Тут ты извращаешься и пишешь один здоровенный SQL, который сам разбирается, что и куда класть. Типа умный апдейт.

UPDATE my_table
SET
    column1 = CASE id
        WHEN 1 THEN 'new_value_1'
        WHEN 2 THEN 'new_value_2'
        -- ... и так до посинения
    END,
    column2 = CASE id
        WHEN 1 THEN 100
        WHEN 2 THEN 200
        -- ... опять эта хуйня
    END
WHERE id IN (1, 2, ...);

Что хорошо: Один запрос — и весь твой список обновился. Красиво и быстро для не очень огромных пачек. Что не очень: Генерировать этот SQL надо аккуратно, а то, блядь, SQL-инъекцию себе в проект загонишь. И если данных много — запрос получится такой, что его читать будет только твой комп.

3. Временная таблица — тяжёлая артиллерия для настоящих маньяков

Когда строк — хуева туча (тысячи, десятки тысяч), то это твой метод. Работает в три шага, как в хорошем анекдоте.

  1. Создаёшь временную таблицу: CREATE TEMP TABLE updates (...)
  2. Забиваешь её данными: Используешь COPY FROM — это, блядь, реактивный снаряд по скорости.
  3. Обновляешь основную таблицу: Делаешь один апдейт с джойном.
-- Это тот самый третий шаг
UPDATE my_table
SET column1 = u.column1
FROM updates AS u
WHERE my_table.id = u.id;

Что хорошо: Скорость — просто ёперный театр! Для гигантских объёмов — лучше не придумаешь. Что не очень: Кода больше, шагов больше. Надо думать, а это не всем дано.

И главное, запомни, как "Отче наш":

  • Транзакции — твои друзья: Всё, что делаешь пачкой, оборачивай в транзакцию. Чтобы либо всё обновилось, либо нихуя. Никаких промежуточных состояний.
  • Не жадничай: Не пытайся впихнуть в один батч всю свою базу. Разбей на куски по 100-1000 записей — и будет тебе счастье.
  • Выбирай с умом: Для повседневки — pgx.Batch. Для сложной логики — CASE. Если данных — пиздец сколько — временные таблицы и COPY. Всё просто, как два пальца об асфальт.