Что делать, если SELECT запрос выполняется слишком долго
Что проверяют
Понимаете ли вы, что в Go «долгий SELECT» часто оказывается ожиданием соединения, а не работой базы, и умеете ли различать это инструментами стандартной библиотеки, а не на глаз.
Как отвечать
Порядок такой: сначала понять, где стоит время, потом смотреть план.
- Проверить пул:
db.Stats()показываетWaitCountиWaitDuration. Если запросы ждут свободное соединение, чинить надоSetMaxOpenConns,SetConnMaxLifetimeи длинные транзакции, а не сам запрос. - Найти утечки:
rowsне закрыт,rows.Err()не проверен, раннийreturnвнутри цикла безdefer rows.Close(). Утёкшие соединения исчерпывают пул, и медленным становится вообще всё. - Ходить в базу только с контекстом:
QueryContextс таймаутом, чтобы отменённый клиентский запрос не держал соединение до конца. - Снять план на боевых данных (
EXPLAIN (ANALYZE, BUFFERS)) и посмотреть тип доступа и расхождение оценки с фактом.
Дальше по самому запросу: индекс под условие и сортировку, отсутствие функции над колонкой, keyset-пагинация вместо глубокого OFFSET, выборка только нужных колонок, разумный размер страницы.
rows, err := db.QueryContext(ctx, q, id)
if err != nil { return err }
defer rows.Close()Где спотыкаются
Открывают подключение на каждый запрос. sql.DB это уже пул, его создают один раз на процесс, а Open внутри обработчика превращает каждый вызов в установку соединения и согласование шифрования.
Второе: оставляют пул с настройками по умолчанию и упираются в лимит соединений на стороне сервера базы. Число открытых соединений задаётся явно и согласуется с числом экземпляров сервиса.
Третье: забывают про пулер соединений в режиме транзакций: подготовленные выражения и сессионные настройки там не переживают запрос. Драйвер, готовящий выражение перед каждым вызовом, добавляет лишний круг к базе, и лечится это режимом без подготовки на стороне сервера.