Лучшие практики Postgres для современных веб-приложений
Большинство разработчиков выбирают PostgreSQL, потому что он бесплатный, надёжный и проверенный в бою. Но выбрать его — это простая часть. Настоящая сложность заключается в правильном использовании — проектировании схем, которые выдерживают изменяющиеся требования, написании запросов, остающихся быстрыми по мере роста данных, и управлении соединениями таким образом, чтобы это незаметно не убило производительность вашего приложения.
Эта статья охватывает лучшие практики PostgreSQL для веб-приложений, которые имеют наибольшее значение в продакшене.
Ключевые выводы
- Используйте типизированные колонки с ограничениями для полей, по которым вы фильтруете, сортируете или выполняете объединения, и резервируйте JSONB для изменяющихся или неструктурированных данных.
- Выбирайте индексы осознанно — B-tree для запросов на равенство и диапазоны, GIN для JSONB и полнотекстового поиска, и частичные индексы для фильтрации с ограниченной областью.
- Используйте пулер соединений, такой как PgBouncer в транзакционном режиме, чтобы предотвратить исчерпание соединений, особенно в serverless-окружениях.
- Делайте миграции схемы аддитивными и неблокирующими, и всегда проверяйте, что генерирует ваш инструмент миграции, перед запуском в продакшене.
- Включите
pg_stat_statements, чтобы выявлять медленные запросы до того, как они станут инцидентами в продакшене.
Проектирование схемы: начните со структуры, сохраните гибкость
Хорошее проектирование схемы PostgreSQL начинается с чёткого разделения между тем, что вы точно знаете, что важно, и тем, что может оказаться важным позже.
Для полей, по которым вы будете фильтровать, сортировать или выполнять объединения — ID пользователей, временные метки, флаги статуса, внешние ключи — используйте правильно типизированные колонки с ограничениями. Для всего остального колонка JSONB даёт вам гибкость документного хранилища без отказа от реляционной целостности.
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
metadata JSONB DEFAULT '{}'::jsonb
);
Этот гибридный подход — одна из самых практичных современных возможностей PostgreSQL. Вы получаете принудительное соблюдение схемы там, где это важно, и гибкость там, где требования всё ещё формируются. Когда поле JSONB начинает регулярно появляться в WHERE-условиях, переведите его в настоящую колонку и добавьте ограничение. Такая миграция проста и стоит того.
Чего следует избегать: хранить всё в JSONB с самого начала. Это кажется гибким, пока вам не понадобится применить ограничение NOT NULL или добавить внешний ключ — и тогда вы всё равно переписываете половину своей схемы.
Стратегии индексирования PostgreSQL, которые действительно помогают
Индексы — это то место, где сходится большинство советов по производительности PostgreSQL. Настройки по умолчанию подходят для старта, но продакшен-нагрузки требуют осознанного выбора.
B-tree индексы обрабатывают большинство запросов на равенство и диапазоны. В последних версиях PostgreSQL (18+) планировщик может выполнять skip scans на многоколоночных B-tree индексах, что означает, что порядок колонок имеет меньшее значение, чем раньше — но начинать с наиболее селективной колонки всё равно остаётся более безопасным вариантом по умолчанию.
GIN индексы — правильный выбор для JSONB-колонок и полнотекстового поиска:
CREATE INDEX idx_user_metadata ON users USING GIN (metadata);
Это делает запросы вида WHERE metadata @> '{"plan": "premium"}'::jsonb быстрыми даже на больших таблицах.
Частичные индексы недостаточно используются и часто являются лучшим инструментом для фильтрации активных или недавних записей:
CREATE INDEX idx_active_users ON users (email) WHERE deleted_at IS NULL;
Избегайте индексирования каждой колонки. Каждый индекс добавляет накладные расходы на запись и потребляет память. Индексируйте то, по чему ваши запросы действительно фильтруют, проверяйте с помощью EXPLAIN ANALYZE и удаляйте неиспользуемые индексы.
Discover how at OpenReplay.com.
Управление соединениями для веб-нагрузок
Модель соединений PostgreSQL является stateful и относительно затратной для установки. Каждое соединение порождает выделенный процесс на сервере. В масштабе сотни короткоживущих соединений от веб-приложения заметно ухудшат производительность.
Решение — пулер соединений, расположенный между вашим приложением и базой данных. PgBouncer — стандартный выбор. В транзакционном режиме он мультиплексирует множество соединений приложения через гораздо меньшее количество реальных соединений с базой данных.
Это особенно важно в serverless-окружениях (Lambda, Vercel, Cloudflare Workers), где каждый вызов функции может пытаться открыть новое соединение. Без пулинга вы достигнете лимита max_connections PostgreSQL при умеренном трафике. См. документацию PostgreSQL по настройкам соединений для подробностей.
Безопасные миграции в продакшене
Изменения схемы — это то место, где всё идёт не так. Самый безопасный паттерн — сначала делать изменения аддитивными: добавить новую колонку со значением по умолчанию, заполнить данные, затем удалить старую колонку в следующем развёртывании.
Избегайте операций ALTER TABLE, которые блокируют таблицу под нагрузкой. Добавление колонки NOT NULL без значения по умолчанию, например, переписывает всю таблицу в старых версиях PostgreSQL. В PostgreSQL 11+ добавление колонки с неизменяемым значением по умолчанию больше не требует перезаписи таблицы — но всё равно стоит проверить, что именно генерирует ваш инструмент миграции, перед запуском в продакшене.
Всегда запускайте миграции внутри транзакций, где это возможно, и тестируйте пути отката перед развёртыванием.
Производительность запросов: сначала найдите медленные запросы
Не гадайте о проблемах производительности. Включите pg_stat_statements, чтобы отслеживать статистику выполнения запросов по всей вашей нагрузке:
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
Затем запросите его, чтобы найти самые медленные или наиболее вызываемые запросы:
SELECT query, calls, mean_exec_time, total_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
Как только у вас есть цель, используйте EXPLAIN (ANALYZE, BUFFERS), чтобы понять, что делает планировщик и поможет ли индекс.
Заключение
PostgreSQL вознаграждает разработчиков, которые серьёзно относятся к проектированию схемы, индексированию и управлению соединениями с самого начала. Это не продвинутые темы — это базовый уровень для запуска веб-приложения в продакшене без сюрпризов. Начните с типизированных колонок и ограничений там, где это важно, добавляйте JSONB только там, где действительно нужна гибкость, и индексируйте на основе реальных паттернов запросов, а не предположений. Соедините это с пулером соединений и дисциплинированной стратегией миграции, и вы избежите наиболее распространённых подводных камней, которые подводят продакшен-приложения.
Часто задаваемые вопросы
Переводите поле JSONB в отдельную колонку, когда вы регулярно фильтруете, сортируете или выполняете объединения по нему. Выделенные колонки позволяют добавлять ограничения NOT NULL, внешние ключи и стандартные B-tree индексы, что улучшает целостность данных и производительность запросов по сравнению с запросами к вложенным путям JSONB.
Транзакционный режим — правильный выбор по умолчанию для большинства веб-приложений, потому что он возвращает соединения в пул после каждой транзакции, максимизируя пропускную способность. Используйте сессионный режим только если ваше приложение полагается на функции уровня сессии, такие как advisory locks, LISTEN/NOTIFY, изменения конфигурации сессии или временные таблицы, которые должны сохраняться между несколькими транзакциями.
В PostgreSQL 11 и новее добавление колонки с неизменяемым значением по умолчанию является операцией только над метаданными и не переписывает таблицу. Однако добавление колонки NOT NULL без значения по умолчанию в старых версиях блокирует и переписывает всю таблицу. Всегда проверяйте, какой SQL генерирует ваш инструмент миграции, перед запуском против продакшена.
Фиксированного числа нет. Индексируйте только те колонки, по которым ваши запросы действительно фильтруют, сортируют или выполняют объединения. Каждый дополнительный индекс увеличивает накладные расходы на запись и использование памяти. Используйте pg_stat_user_indexes для выявления неиспользуемых индексов и удаляйте их. Начните с минимума, мониторьте с помощью EXPLAIN ANALYZE и добавляйте индексы на основе наблюдаемых паттернов запросов.
Gain control over your UX
See how users are using your site as if you were sitting next to them, learn and iterate faster with OpenReplay. — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.