> Что происходит при обновлении нескольких строк в одном запросе в базе данных (Go)

Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: Ozon

Стек: Go

> Пример ответа

Короткий ответ

При обновлении нескольких строк одним запросом база данных выполняет операцию как единую транзакцию: строки блокируются, изменяются, и изменения фиксируются атомарно. Это гарантирует согласованность данных, но требует учёта блокировок, размера пакета и возможных конфликтов с конкурентными операциями. В Go важно использовать контекст с таймаутом и обрабатывать ошибки, чтобы избежать частичного применения изменений.

Подробное объяснение

Когда вы выполняете UPDATE ... WHERE ... для нескольких строк, СУБД обрабатывает это как одну атомарную операцию. Основные этапы:

  1. Планирование запроса - оптимизатор определяет план доступа к строкам (по индексу или full scan).
  2. Блокировка строк - строки, попадающие под условие, блокируются (обычно exclusive lock) до конца транзакции. Это предотвращает гонки с другими транзакциями.
  3. Применение изменений - каждая строка обновляется в соответствии с новым значением. Если в процессе возникает ошибка (например, нарушение constraint), вся операция откатывается.
  4. Фиксация - после успешного выполнения изменения становятся видимыми для других транзакций (в зависимости от уровня изоляции).

Ключевые моменты:

  • Атомарность: либо все строки обновлены, либо ни одна. Промежуточного состояния не существует.
  • Блокировки: при большом количестве строк блокировки удерживаются долго, что может привести к блокировкам других запросов или deadlock'ам.
  • Уровень изоляции: по умолчанию в PostgreSQL - Read Committed, в MySQL - Repeatable Read. Это влияет на видимость изменений для других транзакций.
  • Размер пакета: в Go часто используют batch update (например, по 1000 строк), чтобы уменьшить время удержания блокировок и снизить нагрузку на WAL (write-ahead log).

На практике

В Go при работе с БД (например, через database/sql или pgx) важно:

  • Использовать context.WithTimeout для ограничения времени выполнения запроса.
  • Проверять RowsAffected - количество реально обновлённых строк (может отличаться от ожидаемого, если часть строк уже имела нужное значение).
  • Для больших объёмов данных разбивать обновление на батчи, но помнить, что это ломает атомарность - каждый батч будет отдельной транзакцией.
  • При использовании ORM (GORM, sqlx) учитывать, что Update с условием обновляет все подходящие строки, а не только одну.

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

Пример кода

GO
package main
import (
"context"
"database/sql"
"fmt"
"time"
_ "github.com/lib/pq"
)
func updateBatch(ctx context.Context, db *sql.DB, ids []int64) error {
// Разбиваем на батчи по 500
const batchSize = 500
for i := 0; i < len(ids); i += batchSize {
end := i + batchSize
if end > len(ids) {
end = len(ids)
}
batch := ids[i:end]
// Для каждого батча - отдельная транзакция
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
// Используем ANY для передачи массива
stmt, err := tx.PrepareContext(ctx, `
UPDATE users SET status = 'processed'
WHERE id = ANY($1::bigint[])
`)
if err != nil {
return err
}
defer stmt.Close()
res, err := stmt.ExecContext(ctx, batch)
if err != nil {
return err
}
affected, err := res.RowsAffected()
if err != nil {
return err
}
fmt.Printf("batch %d-%d: updated %d rows\n", i, end, affected)
if err := tx.Commit(); err != nil {
return err
}
}
return nil
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
db, err := sql.Open("postgres", "postgres://user:pass@localhost/db?sslmode=disable")
if err != nil {
panic(err)
}
defer db.Close()
ids := []int64{1, 2, 3, 4, 5}
if err := updateBatch(ctx, db, ids); err != nil {
panic(err)
}
}

Как отвечать на собеседовании

Начните с краткого определения атомарности, затем переходите к деталям: блокировки, уровни изоляции, влияние на производительность. Упомяните практические аспекты для Go: контекст, таймауты, обработка RowsAffected. Если спросят про конкурентность - расскажите про возможные deadlock'и и способы их избежать (порядок обновления, retry logic). Покажите, что понимаете trade-off между атомарностью одного большого запроса и батчами.

Что проверяет интервьюер

  • Понимание транзакционности и атомарности в БД.
  • Знание механизма блокировок и их влияния на конкурентный доступ.
  • Умение применять эти знания в Go-коде: работа с контекстом, транзакциями, обработка ошибок.
  • Осознание разницы между одним запросом и батчами (производительность, консистентность).
  • Способность рассуждать о крайних случаях: частичное обновление, конфликты, таймауты.

Типичные ошибки

  • Утверждение, что "несколько строк обновляются по одной" - это неверно, операция атомарна.
  • Игнорирование RowsAffected - можно не заметить, что часть строк не изменилась.
  • Отсутствие таймаута в Go-коде - запрос может висеть бесконечно при блокировках.
  • Разбиение на батчи без понимания потери атомарности - если один батч упадёт, предыдущие останутся применёнными.
  • Неучёт уровня изоляции - например, ожидание, что другие транзакции сразу увидят изменения, хотя при Read Committed это не так.
  • Использование UPDATE без WHERE - обновление всех строк таблицы, что часто является ошибкой.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью