> Что лучше: выбрать из базы одним запросом тысячу 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 Itemerr := 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 Itemif 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.
> Похожие задачи по Go
Как с помощью контекста завершить работу горутины в Go
Как принимаются решения о внедрении изменений
Как послать сигнал в канал, чтобы все горутины получили уведомление о завершении работы в Go
Как работает перебор мапы в цикле в Go и является ли он случайным
> Похожие задачи по backend
Как с помощью контекста завершить работу горутины в Go
Как принимаются решения о внедрении изменений
Как послать сигнал в канал, чтобы все горутины получили уведомление о завершении работы в Go
Как работает перебор мапы в цикле в Go и является ли он случайным
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью