> Что лучше: выбрать из базы одним запросом тысячу id или сделать тысячу запросов по одному id (Go)

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

Компании: ЦУМ

Стек: Go

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

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

Один запрос на тысячу id почти всегда лучше: меньше round-trip, ниже нагрузка на сеть и connection pool, проще транзакционная целостность. Тысяча отдельных запросов создаёт latency, умноженный на тысячу, и риск исчерпания пула соединений. Исключение - если каждый id обрабатывается независимо и долго, тогда можно батчить, но не по одному. В Go это особенно критично из-за горутин: легко запустить тысячу конкурентных запросов и уронить БД.

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

Основной фактор - стоимость round-trip. Один запрос с WHERE id IN (...) - это один сетевой обмен, один план выполнения, одна транзакция. Тысяча запросов - тысяча сетевых обменов, тысяча планирований, тысяча транзакций. Даже при pipeline в PostgreSQL выгода одного запроса сохраняется: меньше парсинга, меньше блокировок, меньше WAL-записей.

Второй фактор - connection pool. В Go типичный пул (например, database/sql с SetMaxOpenConns) ограничен. Тысяча горутин, каждая со своим запросом, будет ждать соединение. Это создаёт head-of-line blocking и может привести к таймаутам. Один запрос использует одно соединение.

Третий фактор - атомарность. Если нужно прочитать данные по тысяче id и обработать как единое целое, один запрос гарантирует согласованный снимок (при REPEATABLE READ или SERIALIZABLE). Тысяча отдельных запросов может увидеть разные состояния данных, если параллельно идут записи.

Четвёртый фактор - размер ответа. Тысяча id - это обычно несколько килобайт. Один запрос вернёт один result set. Тысяча запросов - тысяча result sets, каждый со своим overhead (metadata, типы, колонки). Суммарный трафик будет в разы больше.

Пятый фактор - план выполнения. Для IN (1000 значений) PostgreSQL и MySQL строят эффективный план: hash join или index scan. Для тысячи отдельных запросов - тысяча index lookups, каждый с накладными расходами на навигацию по B-tree.

Ограничение: если список id превышает лимит параметров (например, 65535 в PostgreSQL) или размер запроса (например, 1 MB в MySQL), придётся разбивать на батчи по 100-500. Но это всё равно лучше, чем по одному.

На практике

В Go с database/sql один запрос с IN - стандартный паттерн. Используйте pq.Array или pgx для передачи слайса. Для MySQL - sql.In из go-sql-driver/mysql или ручное построение плейсхолдеров.

Если данных много (сотни тысяч id), разбивайте на батчи по 500-1000 и выполняйте их последовательно или с ограниченной конкурентностью (например, 5-10 горутин). Это снижает нагрузку на БД и позволяет избежать проблем с размером запроса.

Если запрос выполняется часто и список id стабилен, рассмотрите временную таблицу или unnest в PostgreSQL - это часто быстрее, чем IN.

Пример кода

GO
// Плохо: тысяча запросов
func getByIdsBad(ctx context.Context, db *sql.DB, ids []int64) ([]Item, error) {
items := make([]Item, 0, len(ids))
for _, id := range ids {
var item Item
err := db.QueryRowContext(ctx, "SELECT id, name FROM items WHERE id = $1", id).
Scan(&item.ID, &item.Name)
if err != nil {
return nil, err
}
items = append(items, item)
}
return items, nil
}
// Хорошо: один запрос
func getByIdsGood(ctx context.Context, db *sql.DB, ids []int64) ([]Item, error) {
rows, err := db.QueryContext(ctx,
"SELECT id, name FROM items WHERE id = ANY($1)",
pq.Array(ids),
)
if err != nil {
return nil, err
}
defer rows.Close()
items := make([]Item, 0, len(ids))
for rows.Next() {
var item Item
if err := rows.Scan(&item.ID, &item.Name); err != nil {
return nil, err
}
items = append(items, item)
}
return items, rows.Err()
}

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

Начните с прямого ответа: один запрос лучше. Затем объясните причины: round-trip, connection pool, атомарность, overhead. Упомяните, что есть исключения: очень большие списки, необходимость независимой обработки, лимиты БД. Приведите пример из Go с pq.Array. Если спросят про конкурентность, скажите, что тысяча горутин с запросами - это антипаттерн, лучше батчи с ограниченным параллелизмом.

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

  • Понимание стоимости сетевого round-trip и планирования запросов.
  • Знание работы connection pool в Go (database/sql).
  • Умение оценивать trade-off между одним большим запросом и множеством маленьких.
  • Практический опыт с батчингом и лимитами БД.
  • Понимание атомарности и согласованности данных.

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

  • Утверждение, что тысяча запросов быстрее из-за параллелизма - неверно: БД станет узким местом.
  • Игнорирование лимита на количество параметров в IN - нужно упомянуть батчинг.
  • Предложение использовать JOIN с временной таблицей без объяснения, когда это оправдано.
  • Отсутствие упоминания о размере ответа и overhead каждого result set.
  • Забывают про defer rows.Close() и обработку rows.Err() в Go.

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

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