В ноябре был на конференции Highload++, о которой подробно писал в телеграм-канале.
Было много хороших тезисов про работу с БД, на одном из докладов приводился топ 10. Ниже — углублённый разбор каждой рекомендации: что именно имел в виду спикер и как это применять в реально работе.
1️⃣ Коннекшены — это ограниченный ресурс
Суть:
Подключение к базе — это не просто TCP-сокет. Это память, кэш, контекст транзакции и иногда рабочие потоки в СУБД. Неограниченное открытие коннектов легко приводит к too many connections.
Практика:
Kubernetes-сервис без лимитов на пул: при пике каждый pod открыл 100 соединений → PostgreSQL дошёл до лимита и начал отвечать ошибками.
Как решать:
- В Go:
db.SetMaxOpenConns,db.SetMaxIdleConns,db.SetConnMaxLifetime. - Используйте прокси‑пулеры (PgBouncer) в режиме transaction pooling для большого количества коротких соединений.
- Мониторьте количество active/idle conns (Prometheus метрики драйвера или pg_stat_activity).
Шаблон (Go):
| |
2️⃣ 1 connection = 1 транзакция в один момент времени
Суть:
Транзакция «привязывает» состояние к коннекту. Если вы начинаете BEGIN и ждёте — этот коннект занят. Под нагрузкой занятые коннекты быстро приводят к депрессии пула.
Практика:
Транзакции открывались заранее, чтобы потом по коду делать дополнительные SELECT’ы — под нагрузкой 90% пула было в состоянии idle in transaction.
Как решать:
- Держите транзакции минимальными:
BEGIN→ DML →COMMIT/ROLLBACK. - Никогда не вызывайте внешние I/O (HTTP, heavy CPU) внутри транзакции.
- Если надо координировать внешние вызовы — рассматривайте
outbox pattern.
Outbox схема (упрощённо):
- В транзакции пишем данные + запись в
outboxтаблицу. - Commit.
- Отдельный воркер читает
outboxи шлёт HTTP/сообщения.
3️⃣ Через connection надо постоянно отправлять запросы
Суть:
Idle транзакции и «пустые» ожидания — беда: они держат locks, блокируют VACUUM и тянут пул.
Практика:
ORM автоматически открывала транзакцию на весь HTTP‑запрос. Когда логика делала несколько вызовов внешних сервисов — транзакции висели десятки миллисекунд. Под пиковой нагрузкой это накопилось в серьёзное торможение.
Как решать:
- Избегайте авто‑транзакций вокруг всего запроса.
- Используйте явные короткие транзакции.
- Включите
idle_in_transaction_session_timeoutв Postgres, чтобы ловить просроки.
4️⃣ Нельзя делать HTTP‑запросы в транзакционном контексте
Суть:
Внешние вызовы ненадёжны и медленны. Если транзакция ждёт внешнего ответа — она удерживает ресурсы в БД.
Практика:
В одном сервисе в транзакции делали вызов биллинга. Биллинговый сервис стал отвечать 3s вместо 200ms — база начала деградировать.
Как решать:
- Outbox (см. выше).
- События после commit (eventual consistency).
- Компенсирующие транзакции, если нужен синхронный эффект.
Шаблон для outbox (SQL):
| |
5️⃣ Медленные запросы мешают быстрым
Суть:
Один «тяжёлый» запрос (full table scan, сложные сортировки, агрегации) забирает CPU / IO / Memory и может вытеснить быстрые запросы. Это особенно критично на репликах и при общем ресурсном пуле.
Практика:
Аналитический запрос на 40 млн строк в проде запускал temp‑файлы на диске и занимал 60% CPU. Обычные API запросы начали расти в latency.
Как решать:
- Используйте
EXPLAIN (ANALYZE, BUFFERS). - Установите
statement_timeoutдля user / role в Postgres. - Переносите аналитические запросы на реплику или в аналитическую базу (ClickHouse, DWH).
- Материализованные представления, предварительные агрегаты, batch jobs.
6️⃣ Нужно добавить индексы
Суть:
Индексы — мощный инструмент, но с побочными эффектами: они ускоряют чтение, замедляют запись и занимают место.
Практика:
Вероятно, каждый сталкивался с медленными запросами из-за отсутствия или неоптимального индекса. Про кейс, когда композитный индекс помог значительно ускорить запрос, писал в другой статье.
Как решать:
- Делайте индексы по реальным паттернам
WHERE/JOIN/ORDER BY. - Предпочитайте composite индексы, если запрос использует несколько колонок.
- Убедитесь, что порядок колонок в composite соответствует
WHEREиORDER BY. - Удаляйте неиспользуемые индексы (pg_stat_user_indexes поможет).
Шаблон (SQL):
Плохой вариант:
| |
Оптимальный вариант:
| |
7️⃣ Есть запросы, которые можно просто переписать — OR/IS NULL и др.
Суть:
Операторы OR, IS NULL, NOT IN и др. часто ломают использование индексов. Иногда простая алгебра запросов даёт огромный выигрыш.
Типичные паттерны:
ORпо индексируемым полям → full scan. Можно переписать вUNION ALLили использоватьCOALESCE.NOT IN→ лучшеNOT EXISTS.LEFT JOIN→INNER JOIN, если гарантирован match.
Шаблон (SQL):
Плохо:
| |
Лучше:
| |
8️⃣ Лучше заранее проектируйте API под keyset pagination
Суть:
Offset пагинация деградирует с ростом offset: СУБД вынуждена пробежать и отбросить OFFSET N строк.
Практика:
У сервиса хранилось ~90M событий, API выдавал 50 элементов через /events?page=N, offset на странице 1000 = offset 50 000.
Время нарастает линейно.
Как решать:
Keyset (seek) — стабильный и быстрый способ. Это пагинация не по позиции (offset), а по значению поля, которое уже есть в индексе.
Имеет смысл использовать для лент, логов, таблиц с большим количеством строк.
Шаблон (SQL)
| |
9️⃣ При проектировании БД обратите особое внимание на JSON
Суть:
JSON даёт гибкость, но скрывает схему. Частые ошибки: хранить активные поля в JSON и затем ожидать быстрых запросов по ним.
Как решать:
- JSONB хорош для нечасто фильтруемых/поисковых данных.
- Если по полям JSON нужно фильтровать часто — вынесите их в столбцы (normalization).
- Для JSONB используйте GIN индексы для поиска по ключам:
CREATE INDEX ON table USING GIN (data jsonb_path_ops). - Помните про размер и bloat; часто лучше явная колонка.
Пример:
Если user.preferences->'emails' постоянно используется в WHERE — вынесите pref_emails в отдельный столбец и индексируйте.
🔟 Не делайте типичных ошибок — делайте нетипичные
Суть:
Следовать шаблонам — хорошо, но прод‑системы требуют адаптации под нагрузку и модель данных. Иногда нестандартное решение выигрывает.
Примеры «нетипичных» подходов:
- Шардинг по пользователю для уменьшения contention.
- CQRS: раздельные модели для записи и чтения.
- Использование materialized views + background refresh вместо тяжёлых агрегаций.
Но: не изобретайте велосипед без причин. Профайлинг и метрики сначала.
Запомнить
- Ограничивайте пул коннектов, ставьте PgBouncer если нужно.
- Транзакция = один коннект; держите транзакции короткими.
- Не держите коннект в простое и не делайте внешние вызовы внутри транзакции.
- Вынесите HTTP в outbox/после commit.
- Аналитика и тяжёлые запросы — на реплику или в DWH.
- Индексы — подумайте перед созданием; composite > набор отдельных.
- Переписывайте OR/NOT/IS NULL в эквивалент, чтобы сохранить индекс.
- Планируйте keyset pagination заранее.
- JSON — удобно, но не для часто фильтруемых полей.
- Профилируйте, измеряйте, затем применяйте «нетипичные» архитектурные приёмы.
Читайте другие посты из мира Биг Теха и присоединяйтесь к обсуждению в Телеграм: @time2code