KoderKoder.ai
ЦеныДля бизнесаОбразованиеДля инвесторов
ВойтиНачать

Продукт

ЦеныДля бизнесаДля инвесторов

Ресурсы

Связаться с намиПоддержкаОбразованиеБлог

Правовая информация

Политика конфиденциальностиУсловия использованияБезопасностьПолитика допустимого использованияСообщить о нарушении

Соцсети

LinkedInTwitter
Koder.ai
Язык

© 2026 Koder.ai. Все права защищены.

Главная›Блог›Оптимизация производительности Go + Postgres: практический плейбук для API
14 дек. 2025 г.·7 мин

Оптимизация производительности Go + Postgres: практический плейбук для API

Плейбук по оптимизации Go + Postgres для AI-генерируемых API: настройка пула, чтение планов EXPLAIN, разумные индексы, безопасная пагинация и быстрое формирование JSON.

Оптимизация производительности Go + Postgres: практический плейбук для API

Как выглядит «медленно» для Go API на Postgres

AI-генерируемые API могут казаться быстрыми в ранних тестах. Вы несколько раз вызываете эндпоинт, набор данных маленький, запросы идут по одному. Потом приходит реальный трафик: смешанные эндпоинты, всплески нагрузки, холодные кеши и больше строк, чем вы ожидали. Тот же код может начать ощущаться случайно медленным, хотя ничего явно не сломалось.

Медленность обычно проявляется так: всплески задержек (большинство запросов в порядке, но некоторые в 5–50× медленнее), таймауты (процент неудач), или сильно загруженный CPU (Postgres тратит CPU на выполнение запросов, или Go — на JSON, горутины, логирование и ретраи).

Частый сценарий — list-эндпоинт с гибким фильтром, который возвращает большой JSON. В тестовой БД он сканирует пару тысяч строк и быстро завершает работу. В продакшне он сканирует миллионы строк, сортирует их, и только потом применяет LIMIT. API всё ещё «работает», но p95 растёт, и некоторые запросы таймаутят при всплесках.

Чтобы отделить медленную базу от медленного приложения, держите простую модель в голове.

Если база медленная, ваш Go-хендлер большую часть времени ждёт выполнения запроса. Вы также можете увидеть много запросов «в полёте», тогда как CPU Go остаётся нормальным.

Если приложение медлит, запросы завершаются быстро, но время теряется после запроса: сбор крупных ответов, маршалинг JSON, дополнительные запросы на строку или слишком много работы на один запрос. CPU Go растёт, память растёт, и задержка растёт с размером ответа.

«Достаточно хорошо» перед запуском — не обязательно идеально. Для многих CRUD-эндпоинтов стремитесь к стабильному p95 (не только к среднему), предсказуемому поведению при всплесках и отсутствию таймаутов при ожидаемом пике. Цель простая: никаких неожиданных медленных запросов при росте данных и трафика и ясные сигналы при дрейфе.

Сначала базовая линия: несколько важных чисел

Прежде чем что-то настраивать, решите, что значит «хорошо» для вашего API. Без базовой линии легко часами менять настройки и не понять, улучшили ли вы что-то или просто сдвинули узкое место.

Три числа обычно рассказывают большую часть истории:

  • p95 задержка запросов (не среднее)
  • уровень ошибок (HTTP 5xx, таймауты, отмены)
  • время БД на запрос (сколько каждый запрос ждёт Postgres)

p95 — это метрика «плохого дня». Если p95 высокий, а среднее нормальное, значит небольшой набор запросов делает лишнюю работу, блокируется из-за локов или вызывает медленные планы.

Сделайте медленные запросы видимыми как можно раньше. В Postgres включите логирование медленных запросов с низким порогом для предрелизного тестирования (например, 100–200 ms) и логируйте полный statement, чтобы можно было скопировать его в SQL-клиент. Держите это временно: логировать все медленные запросы в продакшне быстро станет шумно.

Далее тестируйте реальными запросами, а не просто «hello world» маршрутом. Небольшой набор достаточно хорош, если он соответствует тому, что будут делать пользователи: вызов списка с фильтрами и сортировкой, страница детали с парой JOIN-ов, create/update с валидацией и поисковый запрос с частичными совпадениями.

Если вы генерируете эндпоинты из спецификации (например, с помощью инструментов вроде Koder.ai), прогоняйте тот же небольшой набор запросов многократно с одинаковыми входными данными. Это облегчает измерение эффектов от индексов, правок пагинации и переписывания запросов.

Наконец, выберите цель, которую можно озвучить. Пример: «Большинство запросов — p95 < 200 ms при 50 одновременных пользователях, ошибки < 0.5%». Конкретные числа зависят от продукта, но ясная цель предотвращает бесконечную возню.

Пул соединений, который держит Postgres стабильным

Пул соединений ограничивает число открытых соединений к базе и переиспользует их. Без пула каждый запрос может открывать новое соединение, и Postgres тратит время и память на управление сессиями вместо выполнения запросов.

Цель — чтобы Postgres тратил ресурсы на полезную работу, а не на переключение контекста между множеством соединений. Это часто первый заметный выигрыш, особенно для AI-генерируемых API, которые незаметно превращаются в разговорные (chatty) эндпоинты.

Простые стартовые настройки

В Go обычно настраивают max open connections, max idle connections и lifetime соединений. Безопасная отправная точка для многих небольших API — небольшое кратное числу CPU (часто 5–20 соединений), с похожим числом idle и периодической ротацией соединений (например, каждые 30–60 минут).

Если у вас несколько инстансов API, помните, что пулы суммируются. Пул на 20 соединений в 10 инстансах — это 200 соединений к Postgres, и так команды неожиданно упираются в лимиты подключений.

Как понять, что проблема в пуле

Проблемы с пулом ощущаются иначе, чем медленный SQL.

Если пул слишком мал, запросы ждут, прежде чем попасть в Postgres. Появляются всплески задержек, но CPU БД и время запросов могут выглядеть нормальными.

Если пул слишком велик, Postgres выглядит перегруженным: много активных сессий, давление на память и разная латентность по эндпоинтам.

Быстрый способ различить: разделите тайминг DB-вызовов на два этапа — время ожидания соединения и время выполнения запроса. Если большая часть — «ожидание», узкое место в пуле. Если время в основном «в запросе», фокус на SQL и индексах.

Полезные быстрые проверки:

  • Логируйте статистику пула (open, in-use, idle) и смотрите, не застревает ли in-use на максимуме.
  • Добавьте таймаут на получение соединения, чтобы ожидания быстро падали в staging.
  • Наблюдайте активные соединения в Postgres и насколько вы близки к max_connections.
  • Убедитесь, что каждый запрос закрывает rows и быстро возвращает соединение в пул.
  • Нагрузочно тестируйте с тем же числом приложенческих инстансов, которые планируете запускать.

pgxpool vs database/sql

Если вы используете pgxpool, вы получаете пул, ориентированный на Postgres, с понятной статистикой и хорошими дефолтами для поведения Postgres. Если вы используете database/sql, у вас стандартный интерфейс для разных БД, но нужно явно задавать параметры пула и учитывать поведение драйвера.

Практическое правило: если вы полностью на Postgres и хотите прямой контроль, pgxpool часто проще. Если вы полагаетесь на библиотеки, ожидающие database/sql, оставайтесь с ним, явно настройте пул и измеряйте ожидания.

Пример: эндпоинт, который в норме выполняется за 20 ms, при 100 параллельных пользователях прыгает до 2 s. Если логи показывают 1.9 s ожидания соединения, настройка SQL не поможет, пока правильно не подобрать пул и общее число соединений к Postgres.

Планирование выполнения запросов: быстрое чтение вывода EXPLAIN

Когда эндпоинт медлит, посмотрите, что реально делает Postgres. Быстрый разбор EXPLAIN часто указывает на исправление за минуты.

Запустите это для того самого SQL, который отправляет ваш API:

EXPLAIN (ANALYZE, BUFFERS)
SELECT id, status, created_at
FROM orders
WHERE user_id = $1 AND status = $2
ORDER BY created_at DESC
LIMIT 50;

Несколько строк важны больше всего. Смотрите на верхний узел (что выбрал планировщик) и итоги внизу (сколько времени заняло). Затем сравните estimated vs actual rows. Большие расхождения обычно означают, что планировщик ошибся.

Что обычно значат ключевые строки

Если вы видите Index Scan или Index Only Scan, Postgres использует индекс — обычно это хорошо. Bitmap Heap Scan может быть нормой для средних выборок. Seq Scan означает чтение всей таблицы, что годится, только если таблица маленькая или почти все строки соответствуют фильтру.

Типичные сигналы тревоги:

  • Seq Scan по большой таблице
  • Оценочные строки vs реальные сильно расходятся (например, 10 оценено vs 10 000 реально)
  • Сортировка занимает большую часть времени (часто вместе с ORDER BY)
  • «Filter:» отбрасывает много строк после скана
  • Много shared read blocks в BUFFERS (много данных прочитано)

Почему планы идут неверно (и простые исправления)

Медленные планы обычно из-за нескольких паттернов:

  • Отсутствует индекс под ваш WHERE + ORDER BY (например, (user_id, status, created_at))
  • Несоответствие типов (например, сравнение UUID-столбца с текстовым параметром), что мешает использованию индекса
  • Функции в WHERE (например, WHERE lower(email) = $1), которые вынуждают сканы, если не добавить индекс по выражению

Если план выглядит странно и оценки сильно промахиваются, вероятно, статистика устарела. Запустите ANALYZE (или дождитесь autovacuum), чтобы Postgres узнал текущие распределения значений. Это важно после больших импортов или когда новые эндпоинты быстро пишут много данных.

Индексирование под реальные запросы

Get Rewards for Sharing
Создавайте контент или приглашайте других и зарабатывайте кредиты для продолжения работы на Koder.ai.
Earn Credits

Индексы помогают только тогда, когда они соответствуют тому, как API запрашивает данные. Если строить их наугад, вы получите более медленные записи, больший объём хранения и мало ускорения.

Практичный подход: индекс — это ярлык для конкретного вопроса. Если API задаёт другой вопрос, Postgres проигнорирует ярлык.

Стройте индексы вокруг фильтров + порядка сортировки

Если эндпоинт фильтрует по account_id и сортирует по created_at DESC, единый составной индекс обычно лучше двух отдельных. Он помогает Postgres найти нужные строки и вернуть их в нужном порядке с меньшими затратами.

Правила, которые обычно помогают:

  • Индексируйте колонки, по которым чаще фильтруете, затем добавляйте колонку сортировки.
  • Держите составные индексы компактными. 2 колонки — обычно, 3 иногда ок, больше — подозрительно.
  • Ставьте наиболее селективный фильтр первым (тот, что сильнее сужает результаты).
  • Избегайте отдельных индексов, полностью покрываемых лучшим составным индексом.
  • Предпочитайте один хорошо подобранный индекс нескольким «возможно полезным».

Пример: если ваш API GET /orders?status=paid и всегда показывает самые новые, индекс (status, created_at DESC) хорошо подходит. Если большинство запросов также фильтрует по customer, (customer_id, status, created_at) может быть лучше, но только если это действительно отражает работу эндпоинта в продакшне.

Частичные индексы для популярных фильтров

Если трафик в основном попадает в узкую часть строк, частичный индекс может быть дешевле и быстрее. Например, если приложение в основном читает активные записи, индекс только WHERE active = true делает индекс меньше и более вероятно удержится в памяти.

Чтобы подтвердить пользу индекса:

  • Запустите EXPLAIN (или EXPLAIN ANALYZE в безопасной среде) и ищите индексный проход, соответствующий запросу.
  • Сравните время и прочитанные строки с индексом и без.
  • Следите за «Rows Removed by Filter» — часто это знак, что индекс не соответствует фильтру.

Удаляйте неиспользуемые индексы осторожно. Проверьте статистику использования (сканируется ли индекс). Удаляйте по одному в окна низкого риска и имейте план отката. Неиспользуемые индексы — не безвредны: они замедляют вставки и обновления при каждой записи.

Паттерны пагинации, которые не замедляются со временем

Пагинация — частое место, где быстрый API начинает казаться медленным, даже когда база здорова. Рассматривайте пагинацию как задачу проектирования запроса, а не как деталь UI.

Почему LIMIT/OFFSET замедляется

LIMIT/OFFSET выглядит просто, но глубокие страницы обычно дороже. Postgres всё равно должен пройти (и часто отсортировать) пропускаемые строки. Страница 1 может трогать десятки строк. Страница 500 — может заставить БД просканировать и отбросить десятки тысяч строк, чтобы вернуть 20 результатов.

Это также даёт нестабильные результаты при вставках/удалениях между запросами — пользователи могут видеть дубликаты или пропущенные элементы, потому что смысл "строки 10 000" меняется.

Keyset-пагинация (cursor) с примером «последней увиденной» строки

Keyset-пагинация задаёт другой вопрос: «Дайте следующие 20 строк после последней увиденной». Так база работает с маленькой, стабильной частью данных.

Простой вариант на возрастающем id:

SELECT id, created_at, title
FROM posts
WHERE id > $1
ORDER BY id
LIMIT 20;

API возвращает next_cursor как последний id на странице. Следующий запрос использует это значение как $1.

Для сортировки по времени используйте стабильный порядок и дополнительный tie-breaker. created_at сам по себе недостаточен, если две строки имеют одинаковую метку времени. Используйте составной курсор:

WHERE (created_at, id) < ($1, $2)
ORDER BY created_at DESC, id DESC
LIMIT 20;

Небольшие правила, чтобы избежать дубликатов и пропаж:

  • Всегда включайте уникальный tie-breaker в ORDER BY (обычно id).
  • Поддерживайте одинаковый порядок сортировки между запросами.
  • Делайте курсор непрозрачным для клиента (кодируйте created_at и id вместе).
  • Если есть фильтры, применяйте их на каждой странице.
  • Предпочитайте неизменяемые поля сортировки (время создания) изменяемым (status, score), когда возможно.

Формирование JSON: быстрее ответы с меньшими полезными нагрузками

Make JSON Responses Lean
Держите ответы лёгкими, формируя полезный набор полей и избегая непреднамеренного роста SELECT *.
Create API

Удивительно частая причина ощущения медленности — не база, а ответ. Большой JSON дольше строить, дольше отправлять и дольше парсить клиентом. Самый быстрый выигрыш часто — вернуть меньше данных.

Начните с SELECT. Если эндпоинту нужны только id, name и status, запрашивайте только эти колонки. SELECT * тихо становится тяжелей со временем, когда в таблицу добавляются большие тексты, JSON-блобы и служебные колонки.

Ещё частая медленность — паттерн N+1: вы получаете список из 50 элементов, а затем запускаете 50 дополнительных запросов, чтобы добавить связанные данные. Это может пройти в тестах, но рухнет под реальной нагрузкой. Предпочитайте один запрос, который возвращает всё нужное (осторожные JOIN-ы), или два запроса, где второй батчит по ID.

Несколько способов держать payload меньше, не ломая клиентов:

  • Используйте флаг include= (или маску fields=), чтобы ответы списка оставались лёгкими, а детали — опциональными.
  • Ограничивайте вложенные массивы (например, только последние 10 событий) и делайте отдельный эндпоинт для полной истории.
  • Не возвращайте сырые внутренние JSON-колонки, если клиентам нужны лишь пара ключей.
  • Используйте короткие коды вместо повторяющихся длинных меток.

Строить JSON в Postgres или в Go?

Оба подхода могут быть быстрыми. Выбирайте в зависимости от оптимизационной цели.

Функции Postgres (jsonb_build_object, json_agg) полезны, когда нужно меньше кругов общения с БД и предсказуемая форма из одного запроса. Формирование в Go удобно, когда нужна условная логика, повторное использование структур или поддерживаемость SQL. Если SQL для построения JSON становится нечитаемым, его сложно будет оптимизировать.

Хорошее правило: пусть Postgres фильтрует, сортирует и агрегирует, а Go делает финальное представление.

Если вы быстро генерируете API (например с Koder.ai), добавление include-флагов ранним этапом помогает избежать эндпоинтов, которые раздуваются со временем. Это даёт безопасный путь добавлять поля, не утяжеляя все ответы.

Пошаговый проход по оптимизации до первых пользователей

Вам не нужна огромная тестовая лаборатория, чтобы поймать большинство проблем с производительностью. Короткий воспроизводимый проход выявит проблемы, которые превращаются в аварии при появлении трафика, особенно если начальная точка — сгенерированный код, который вы собираетесь выпустить.

Перед изменениями зафиксируйте простую базовую линию:

  • p95 и p99 задержки по самым загруженным эндпоинтам
  • уровень ошибок и таймауты
  • CPU БД и активные соединения
  • 5 самых медленных запросов по суммарному времени (не только самый худший)

Процесс оптимизации

Начинайте с малого, меняйте по одному параметру и тестируйте после каждого изменения.

  1. Запустите 10–15 минутный нагрузочный тест, имитирующий реальное использование. Попадайте в те же эндпоинты, что будут у первых пользователей (логин, списки, поиск, создание). Потом отсортируйте маршруты по p95 задержке и суммарному времени.

  2. Проверьте давление на соединения до правки SQL. Слишком большой пул перегружает Postgres. Слишком маленький пул вызывает долгие ожидания. Смотрите на растущее время ожидания соединения и всплески количества подключений. Настройте пул и idle лимиты, затем повторите нагрузку.

  3. EXPLAIN для самых медленных запросов и исправьте самый явный красный флаг. Обычные виновники: полные сканы больших таблиц, сортировки больших наборов результатов и JOIN-ы, которые взрывают число строк. Выберите один самый худший запрос и сделайте его «скучным» (предсказуемым).

  4. Добавьте или поправьте один индекс, затем тестируйте снова. Индексы помогают, когда они соответствуют WHERE и ORDER BY. Не добавляйте пять сразу. Если медленный эндпоинт — «список заказов по user_id, отсортированный по created_at», составной индекс (user_id, created_at) может решить всё.

  5. Уточните ответы и пагинацию, затем снова тестируйте. Если эндпоинт возвращает 50 строк с большими JSON-блобами, платят база, сеть и клиент. Возвращайте только поля, которые нужны UI, и предпочитайте пагинацию, которая не замедляется с ростом таблиц.

Ведите простой журнал изменений: что поменяли, почему и как двинулись p95. Если изменение не улучшило базовую линию — откатите и идите дальше.

Частые ошибки и подводные камни

Deploy and Use a Domain
Хостьте приложение и подключайте кастомный домен, когда будете готовы им поделиться.
Publish App

Большинство проблем с производительностью в Go API на Postgres — самосделанные. Хорошая новость: несколько проверок ловят многие из них до прихода реального трафика.

Классическая ловушка — считать размер пула «ручкой скорости». Установка его «насколько возможно больше» часто делает всё медленнее. Postgres начинает тратить время на управление сессиями, памятью и локами, и приложение начинает таймаутить волнами. Меньший, стабильный пул с предсказуемой конкуренцией обычно выигрывает.

Ещё одна ошибка — «проиндексировать всё». Лишние индексы помогают чтениям, но замедляют записи и могут неожиданно менять планы запросов. Если API часто вставляет или обновляет, каждый лишний индекс добавляет работу. Измеряйте до и после и пересмотрите планы после добавления индекса.

Долг пагинации подкрадывается незаметно. OFFSET-пагинация выглядит нормально сначала, но со временем p95 растёт, потому что базе приходится проходить больше строк.

Размер JSON — ещё один скрытый налог. Сжатие помогает по пропускной способности, но не убирает стоимость сборки, аллокаций и парсинга больших объектов. Тримьте поля, избегайте глубокой вложенности и возвращайте только то, что нужно экрану.

Если вы смотрите только на среднее время ответа, вы пропустите точки реального пользовательского боли. p95 (и иногда p99) — там, где сначала проявляются насыщение пула, ожидания локов и медленные планы.

Короткая предрелизная проверка:

  • Смотрите время ожидания пула и количество подключений Postgres при небольшом нагрузочном тесте.
  • Сравните среднее и p95 по одному и тому же эндпоинту.
  • Проверьте, не деградирует ли пагинация при увеличении таблицы в 10×.
  • Просмотрите размеры ответов для списковых эндпоинтов (байты важны).
  • Повторите EXPLAIN после добавления индексов или изменения фильтров.

Быстрый чек-лист и следующие шаги перед запуском

Перед приходом реальных пользователей вы хотите доказательства, что API остаётся предсказуемым под нагрузкой. Цель не в идеальных цифрах, а в ловле тех проблем, которые приводят к таймаутам, всплескам или когда база перестаёт принимать работу.

Прогоняйте проверки в staging, похожем на прод (приблизительный объём БД, те же индексы, те же настройки пула): измерьте p95 по ключевым эндпоинтам под нагрузкой, зафиксируйте самые медленные запросы по суммарному времени, смотрите время ожидания пула, запускайте EXPLAIN (ANALYZE, BUFFERS) для худшего запроса, чтобы подтвердить использование ожидаемого индекса, и проверьте размеры полезной нагрузки по самым загруженным маршрутам.

Затем сделайте один worst-case прогон, который имитирует, как ломается продукт: запросите глубокую страницу, примените самый широкий фильтр и попробуйте с холодного старта (рестарт API и тот же запрос первым). Если глубокая пагинация замедляется с каждой страницей, перейдите на курсорную пагинацию до релиза.

Запишите ваши дефолты, чтобы команда дальше делала согласованные выборы: лимиты и таймауты пула, правила пагинации (максимальный размер страницы, разрешение OFFSET или только курсоры), правила запросов (выбирать только нужные колонки, избегать SELECT *, ограничивать тяжёлые фильтры) и правила логирования (порог медленных запросов, как долго хранить сэмплы, как маркировать эндпоинты).

Если вы создаёте и экспортируете Go + Postgres сервисы с Koder.ai, короткая плановая проверка перед деплоем помогает держать фильтры, пагинацию и форму ответов в курсе. Как только вы начнёте править индексы и формы запросов, снимки и откаты облегчают отмену «фикса», который помог одному эндпоинту, но повредил другим. Если хотите единое место для итераций этого рабочего процесса, Koder.ai на koder.ai создан для генерации и доработки таких сервисов через чат, с возможностью экспортировать исходники, когда вы готовы.

FAQ

How do I quickly tell if my Go API is slow because of Postgres or because of my code?

Начните с разделения времени ожидания БД и времени работы приложения.

  • Если база данных медленная, хендлер в основном ждёт выполнения запроса. Часто при этом загрузка CPU Go остаётся нормальной, а запросы скапливаются „в полёте".
  • Если приложение медлит, запросы завершаются быстро, но время уходит на построение объектов, дополнительные запросы на строку, маршалинг большого JSON или логирование. CPU и память Go обычно растут вместе с размером ответа.

Добавьте простые тайминги вокруг «ожидания соединения» и «выполнения запроса», чтобы понять, какая сторона доминирует.

What metrics should I track first before tuning anything?

Используйте небольшой воспроизводимый базовый набор метрик:

  • p95 latency по ключевым эндпоинтам (не среднее)
  • error rate (5xx, таймауты, отмены)
  • DB time per request (время ожидания Postgres)

Задайте понятную цель, например «p95 < 200 ms при 50 одновременных пользователях, ошибки < 0.5%». Меняйте только по одному параметру и заново тестируйте тот же набор запросов.

Should I turn on Postgres slow query logging, and what threshold is practical?

Включите логирование медленных запросов с низким порогом в предрелизной среде (например, 100–200 ms) и сохраняйте полный текст запроса, чтобы можно было вставить его в SQL-клиент.

Держите это временно:

  • В продакшне с таким порогом будет много шума.
  • Избыточное логирование добавляет накладные расходы.

Когда найдёте основные виновники, переключитесь на выборку или повысите порог.

What are good starting connection pool settings for a Go API on Postgres?

Практичный дефолт — небольшое кратное числу CPU на инстанс API, часто 5–20 max open connections, с похожим числом idle и ротацией соединений каждые 30–60 минут.

Два частых режима отказа:

  • Слишком маленький пул: запросы ждут соединения, хотя время выполнения запросов выглядит нормальным.
  • Слишком большой пул: Postgres перегружен множеством активных сессий и латентность становится непредсказуемой.

Помните, что пулы суммируются между инстансами (20 × 10 = 200 соединений).

How can I confirm the connection pool is the bottleneck (not SQL)?

Замерьте DB-вызовы в двух частях:

  • Время ожидания соединения (pool wait)
  • Время выполнения запроса (работа Postgres)

Если большая часть — ожидание пула, перенастройте размер пула, таймауты и число инстансов. Если большая часть — выполнение запроса, фокусируйтесь на EXPLAIN и индексах.

Также убедитесь, что вы всегда закрываете rows, чтобы соединения возвращались в пул.

What should I look for first in EXPLAIN when an endpoint is slow?

Запустите EXPLAIN (ANALYZE, BUFFERS) на том самом SQL, который посылает API, и смотрите:

  • по большой таблице
How do I choose the right index for a list endpoint with filters and sorting?

Индексы должны соответствовать тому, как endpoint реально запрашивает данные: фильтры + порядок сортировки.

Хороший шаблон:

  • Постройте композитный индекс для частого WHERE + ORDER BY.
  • Делайте индекс компактным (часто 2 столбца, иногда 3).
When is a partial index worth it in Postgres?

Частичный индекс полезен, когда трафик в основном обращается к предсказуемой подвыборке строк.

Пример: большинство чтений только для active = true — индекс ... WHERE active = true будет меньше, с большей вероятностью влезет в память и снизит накладные расходы на запись по сравнению с индексированием всего.

Подтвердите с помощью EXPLAIN, что Postgres действительно использует этот индекс для ваших горячих запросов.

Why does LIMIT/OFFSET pagination get slower over time, and what should I use instead?

LIMIT/OFFSET медлит на глубоких страницах, потому что Postgres всё равно проходит (и часто сортирует) пропускаемые строки. Страница 1 может обрабатывать десятки строк, страница 500 — десятки тысяч, чтобы вернуть 20 результатов.

Предпочитайте keyset (cursor) pagination:

My DB queries are fast, but responses are still slow—should I trim JSON payloads?

Чаще всего — да для списковых эндпоинтов. Самый быстрый ответ — тот, который вы не послали.

Практичные приёмы:

  • Выбирайте только нужные столбцы (избегайте SELECT *).
  • Добавьте include= или , чтобы клиенты сами включали тяжёлые поля.
Содержание
Как выглядит «медленно» для Go API на PostgresСначала базовая линия: несколько важных чиселПул соединений, который держит Postgres стабильнымПланирование выполнения запросов: быстрое чтение вывода EXPLAINИндексирование под реальные запросыПаттерны пагинации, которые не замедляются со временемФормирование JSON: быстрее ответы с меньшими полезными нагрузкамиПошаговый проход по оптимизации до первых пользователейЧастые ошибки и подводные камниБыстрый чек-лист и следующие шаги перед запускомFAQ
Поделиться
Koder.ai
Создайте свое приложение с Koder сегодня!

Лучший способ понять возможности Koder — попробовать самому.

Начать бесплатноЗаказать демо
Seq Scan
  • Большая разница между estimated rows и actual rows
  • Sort, занимающий основное время (часто с ORDER BY)
  • «Rows Removed by Filter» очень высоко
  • Много shared read blocks в BUFFERS (тяжёлые чтения)
  • Исправьте самый большой красный флаг в первую очередь; не пытайтесь править всё сразу.

  • Ставьте наиболее селективный фильтр первым, затем колонку сортировки.
  • Пример: если вы фильтруете по user_id и показываете новейшие, индекс (user_id, created_at DESC) часто решает проблему с p95.

  • Используйте стабильную сортировку плюс уникальный tie-breaker (часто id).
  • Сохраняйте одинаковый ORDER BY между запросами.
  • Кодируйте (created_at, id) или похожие значения в курсор.
  • Так стоимость каждой страницы остаётся примерно постоянной по мере роста таблицы.

    fields=
  • Ограничивайте вложенные массивы (например, последние 10 событий) и давайте отдельный эндпоинт для полной истории.
  • Избегайте N+1 (50 строк + 50 запросов). Используйте JOIN или батчинг по ID.
  • Часто уменьшение полезной нагрузки снижает CPU Go, использование памяти и хвостовую латентность.