Уроки продуктивності PostgreSQL з досвіду 6 SaaS-продуктів
Уроки оптимізації PostgreSQL з експлуатації продакшн SaaS-баз даних у fluxLab.dev: індексація, тюнінг запитів, N+1 запити та пулінг з'єднань.
Вступ
Кожен SaaS-продукт fluxLab.dev працює на PostgreSQL. Через кілька продуктів та 46 000+ користувачів ми накопичили практичні знання про те, що робить PostgreSQL швидким — і що робить його повільним. Це уроки, які дійсно мали значення в продакшні.
Урок 1: Індекси не безкоштовні
Інстинкт «просто додай індекс» швидко призводить до проблем. Кожен індекс уповільнює запис і споживає пам'ять. Ми дотримуємось простого правила: додаємо лише індекси, що обслуговують реальний патерн запитів, підтверджений через EXPLAIN ANALYZE.
Що спрацювало
-- Composite index for common filter + sort pattern
CREATE INDEX idx_applications_user_status
ON applications (user_id, status)
WHERE status = 'active';
Часткові індекси — потужний інструмент. У Jobber більшість запитів фільтрують за user_id та status = 'active'. Частковий індекс лише на активних записах менший і швидший за повний.
Що не спрацювало
Додавання індексів на кожен foreign key за замовчуванням. Для невеликих таблиць (менше 10 000 рядків) послідовне сканування часто швидше за пошук через індекс. Ми видалили 12 непотрібних індексів у наших базах даних і побачили покращення продуктивності запису на 15%.
Урок 2: N+1 запити ховаються в ORM
Ми перестали використовувати ORM після третього продукту. З чистим SQL та pgx у Go кожен запит видимий і свідомий.
Патерн, який ми використовуємо
Замість того, щоб отримувати заявку, потім її вакансію, потім резюме, потім етапи — ми використовуємо один запит із JOIN і агрегуємо результат у Go:
SELECT a.id, a.status, a.applied_date,
j.title as job_title,
r.title as resume_title,
s.name as current_stage
FROM applications a
JOIN jobs j ON j.id = a.job_id
JOIN resumes r ON r.id = a.resume_id
LEFT JOIN stages s ON s.id = a.current_stage_id
WHERE a.user_id = $1
ORDER BY a.updated_at DESC
LIMIT 20 OFFSET $2;
Один запит замість чотирьох. Час відповіді знизився з 120мс до 8мс.
Урок 3: Пулінг з'єднань важливіший, ніж здається
PostgreSQL створює новий процес для кожного з'єднання. При 100 одночасних користувачах це 100 процесів ОС, що споживають пам'ять. Ми використовуємо pgxpool із ретельно налаштованими параметрами:
- Максимум з'єднань: 25 на інстанс сервісу (не 100)
- Мінімум з'єднань: 5 (тримає теплі з'єднання напоготові)
- Максимальний час простою: 5 хвилин
- Період health check: 30 секунд
Для наших серверів Hetzner Cloud з 4ГБ RAM 25 з'єднань на сервіс — це оптимальна точка. Більше з'єднань насправді знижує пропускну здатність через перемикання контексту.
Урок 4: Використовуйте EXPLAIN ANALYZE, а не EXPLAIN
EXPLAIN показує план запиту. EXPLAIN ANALYZE фактично виконує запит і показує реальний час виконання. Різниця важлива, бо оцінки вартості PostgreSQL можуть бути неточними.
Ми запускаємо EXPLAIN ANALYZE на кожному запиті, що займає більше 50мс у логах продакшну. Це дозволяє відловити повільні запити до того, як на них поскаржаться користувачі.
Урок 5: Timestamp-колонки потребують врахування часового поясу
Ми засвоїли це на гіркому досвіді з WashFlow. Використовуйте TIMESTAMPTZ (timestamp with time zone), а не TIMESTAMP. Зберігайте все в UTC. Конвертуйте в локальний час лише на фронтенді.
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
Урок 6: М'яке видалення ускладнює запити
Ми спробували soft delete (додавання колонки deleted_at) у нашому продукті Accounting. Кожен запит потребував WHERE deleted_at IS NULL, і ми пропустили це в кількох місцях. Тепер ми надаємо перевагу hard delete з належними foreign key constraints і окремою таблицею аудиту, коли нам потрібна історія.
Урок 7: Міграції повинні бути зворотними
Кожна міграція у fluxLab.dev має скрипти up і down. Ми використовуємо golang-migrate і тестуємо відкати в стейджингу перед деплоєм на продакшн. Це врятувало нас двічі, коли міграція спричиняла неочікувані проблеми.
Висновок
PostgreSQL надзвичайно здатний одразу з коробки. Більшість проблем продуктивності, з якими ми стикались, були спричинені нашими власними помилками: відсутні індекси, N+1 запити, забагато з'єднань або неправильні типи колонок. Найкраща оптимізація — писати правильні запити з самого початку і вимірювати все через EXPLAIN ANALYZE.
Якщо ваша база даних сповільнює ваш продукт, аудит продуктивності — це частина нашого технічного консалтингу.
Читайте також
Чому варто аутсорсити розробку в продуктову студію в Україні
Чому продуктова студія краща за традиційну аутсорсингову агенцію і що шукати в партнері в Україні. Практичний гайд від київської студії.
Додавання ШІ-функцій до SaaS-продуктів без великих витрат
Як fluxLab.dev інтегрує Claude AI в Jobber для парсингу вакансій, зіставлення резюме та генерації супровідних листів, контролюючи витрати.