Почему запрос с индексами может тормозить и как это решать
Что проверяют
Умеете ли вы искать причину замедления там, где она обычно и лежит, а не там, где привычно. Вопрос задают go-разработчику, и хороший ответ начинается не с индекса, а с того, как сервис ходит в базу.
Как отвечать
Разделите два уровня.
На стороне базы индекс может быть и при этом не давать выигрыша. Неселективный предикат отдаёт слишком много строк, и планировщик честно выбирает последовательное чтение. Индекс не покрывающий, поэтому за каждой найденной строкой идёт обращение к таблице вразнобой. Порядок сортировки в запросе не совпадает с порядком в индексе, и LIMIT не спасает, сортировать всё равно приходится целиком. Статистика устарела после массовой заливки, индекс раздут после долгой череды обновлений. Смотреть это нужно в EXPLAIN с фактическим временем и счётчиками буферов, сравнивая оценку числа строк с фактом.
На стороне сервиса причин обычно больше. Запросы в цикле по результатам первого дают классический N+1: каждый выполняется за доли миллисекунды, а вместе это секунды сетевых задержек. Пул соединений задан слишком узко, и горутина ждёт не базу, а свободное соединение. Забытый rows.Close или брошенный на середине обход держат соединение занятым. Контекст без таймаута превращает одну тяжёлую выборку в лавину.
Инструменты называйте конкретно: db.Stats для пула, pg_stat_statements для профиля запросов, трассировка для распределения времени по этапам.
Где спотыкаются
Меряют время вызова в коде и считают его временем работы базы. В замер попадают ожидание свободного соединения, разбор результата и сканирование в структуры, и растут часто именно они.
Второе: лечат симптом кэшем поверх медленного запроса. Кэш прячет проблему до первого промаха, а при массовом истечении ключей вся нагрузка приходит разом и добивает базу.
Третье: заводят индекс на каждое поле из условий. Каждый индекс оплачивается на записи и занимает память, а планировщик с десятком похожих индексов начинает выбирать хуже, а не лучше.