PostgreSQL Admin
PostgreSQL — объектно-реляционная СУБД. Администратор отвечает за конфигурацию сервера, управление ролями и правами, репликацию, производительность, обслуживание таблиц и контроль выполнения запросов.
Примеры рассчитаны на современные версии PostgreSQL. Перед изменениями в production проверяйте документацию установленной версии и готовьте план отката.
Содержание
- Подключение и первичная диагностика
- Настройка postgresqlconf
- Пользователи роли и права
- Безопасность подключений pg_hbaconf
- Репликация базово
- Мониторинг производительности
- Вакуумирование и обслуживание
- Продвинутая оптимизация индексов
- explain и explain analyze
- Диагностика медленного запроса
- Итоговый чек-лист
Подключение и первичная диагностика
Подключение через psql
sudo -u postgres psql
psql -h 127.0.0.1 -p 5432 -U app_user -d app_db -WПараметры можно задавать переменными окружения:
export PGHOST=127.0.0.1
export PGPORT=5432
export PGDATABASE=app_db
export PGUSER=app_user
psqlДля автоматизации лучше использовать ~/.pgpass, а не передавать пароль в командной строке:
hostname:port:database:username:password
127.0.0.1:5432:app_db:app_user:strong-passwordchmod 600 ~/.pgpassПолезные команды psql
\conninfo -- текущее подключение
\l -- базы данных
\c app_db -- подключиться к базе
\dn -- схемы
\dt -- таблицы
\d+ orders -- структура таблицы
\di -- индексы
\du+ -- роли
\dp -- права доступа
\x -- расширенный вывод
\timing on -- время выполнения
\q -- выйтиПроверка состояния
SELECT version();
SELECT current_database(), current_user, current_schema();
SHOW server_version;
SHOW data_directory;
SHOW config_file;
SHOW hba_file;
SHOW port;Активные подключения и ожидания:
SELECT pid, usename, datname, client_addr, application_name,
state, wait_event_type, wait_event, query_start,
LEFT(query, 160) AS query
FROM pg_stat_activity
ORDER BY query_start NULLS LAST;Настройка postgresql.conf
postgresql.conf содержит сетевые параметры, настройки памяти, WAL, журналирования, планировщика и autovacuum.
Просмотр и изменение параметров
SELECT name, setting, unit, source, context, pending_restart
FROM pg_settings
WHERE name IN (
'listen_addresses', 'port', 'max_connections',
'shared_buffers', 'work_mem', 'maintenance_work_mem',
'effective_cache_size', 'wal_level', 'max_wal_size',
'shared_preload_libraries'
)
ORDER BY name;Изменение на уровне сервера:
ALTER SYSTEM SET work_mem = '16MB';
ALTER SYSTEM SET log_min_duration_statement = '500ms';
SELECT pg_reload_conf();ALTER SYSTEM записывает настройки в postgresql.auto.conf. Для отдельной базы или роли используются:
ALTER DATABASE app_db SET timezone = 'UTC';
ALTER ROLE app_user SET statement_timeout = '30s';Параметры, требующие перезапуска:
SELECT name, setting, pending_restart
FROM pg_settings
WHERE pending_restart = true;Сеть и подключения
listen_addresses = '127.0.0.1,10.10.0.5'
port = 5432
max_connections = 200Не увеличивайте max_connections без расчёта памяти. Большое число клиентов часто лучше обслуживать через пул подключений, например PgBouncer.
Память
shared_buffers = 25% от RAM как отправная точка
work_mem = 16MB
maintenance_work_mem = 256MB
effective_cache_size = 50-75% от RAM как оценка кеша ОС и PostgreSQLwork_mem выделяется отдельно для каждой сортировки или hash-операции каждого процесса. Фактическое потребление может быть намного выше значения параметра, поэтому его нельзя механически умножать только на число подключений.
WAL и checkpoints
wal_level = replica
max_wal_size = 4GB
min_wal_size = 1GB
checkpoint_timeout = 15min
checkpoint_completion_target = 0.9WAL нужен для восстановления после сбоя и репликации. Слишком частые checkpoints вызывают всплески I/O. Подбирайте параметры по журналам checkpoints, нагрузке и скорости диска.
Логирование
logging_collector = on
log_directory = 'log'
log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'
log_rotation_age = 1d
log_rotation_size = 100MB
log_line_prefix = '%m [%p] %u@%d %r '
log_checkpoints = on
log_lock_waits = on
log_temp_files = 10MB
log_min_duration_statement = 500mslog_statement = 'all' не следует постоянно включать на нагруженной production-базе: объём логов может резко вырасти. Для slow query обычно подходят log_min_duration_statement и pg_stat_statements.
Тайм-ауты
ALTER ROLE app_user SET statement_timeout = '30s';
ALTER ROLE app_user SET lock_timeout = '5s';
ALTER ROLE app_user SET idle_in_transaction_session_timeout = '60s';statement_timeoutограничивает время выполнения запроса.lock_timeoutограничивает ожидание блокировки.idle_in_transaction_session_timeoutзакрывает забытую транзакцию.
Для миграций и ETL задавайте отдельные значения, если им требуется больше времени.
pg_stat_statements
Для статистики запросов добавьте расширение в shared_preload_libraries и перезапустите сервер:
shared_preload_libraries = 'pg_stat_statements'В базе:
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;Самые затратные запросы:
SELECT calls, total_exec_time, mean_exec_time, rows,
shared_blks_hit, shared_blks_read,
LEFT(query, 240) AS query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;Расширение помогает выбрать запросы для детального анализа, но не заменяет EXPLAIN.
Пользователи, роли и права
Пользователь PostgreSQL — это роль с атрибутом LOGIN. Роль без LOGIN удобно использовать как группу прав.
Создание ролей
CREATE ROLE app_owner NOLOGIN;
CREATE ROLE app_runtime NOLOGIN;
CREATE ROLE app_migrations NOLOGIN;
CREATE ROLE app_user LOGIN PASSWORD 'from-secret-manager';
GRANT app_runtime TO app_user;В production не храните пароли в Git, SQL-файлах или CI-логах. Пароль можно задать интерактивно:
\password app_userАтрибуты роли:
ALTER ROLE app_user CONNECTION LIMIT 50;
ALTER ROLE app_user SET statement_timeout = '30s';
ALTER ROLE app_user NOLOGIN; -- запретить входСписок ролей:
SELECT rolname, rolcanlogin, rolsuper, rolcreatedb,
rolcreaterole, rolreplication, rolbypassrls, rolconnlimit
FROM pg_roles
ORDER BY rolname;Принцип наименьших привилегий
Приложению обычно не нужен superuser. Выдайте только необходимые права:
GRANT CONNECT ON DATABASE app_db TO app_user;
GRANT USAGE ON SCHEMA app TO app_runtime;
GRANT SELECT, INSERT, UPDATE, DELETE
ON ALL TABLES IN SCHEMA app TO app_runtime;
GRANT USAGE, SELECT
ON ALL SEQUENCES IN SCHEMA app TO app_runtime;Права для будущих объектов:
ALTER DEFAULT PRIVILEGES FOR ROLE app_migrations IN SCHEMA app
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_runtime;
ALTER DEFAULT PRIVILEGES FOR ROLE app_migrations IN SCHEMA app
GRANT USAGE, SELECT ON SEQUENCES TO app_runtime;Важно: ALTER DEFAULT PRIVILEGES действует для объектов, создаваемых указанной ролью.
Read-only роль:
CREATE ROLE app_readonly LOGIN PASSWORD 'from-secret-manager';
GRANT CONNECT ON DATABASE app_db TO app_readonly;
GRANT USAGE ON SCHEMA app TO app_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA app TO app_readonly;
ALTER DEFAULT PRIVILEGES FOR ROLE app_migrations IN SCHEMA app
GRANT SELECT ON TABLES TO app_readonly;Права можно проверить так:
SELECT grantee, table_schema, table_name, privilege_type
FROM information_schema.role_table_grants
WHERE grantee IN ('app_runtime', 'app_readonly')
ORDER BY grantee, table_schema, table_name;Не выдавайте приложению SUPERUSER, CREATEDB, CREATEROLE, REPLICATION или BYPASSRLS, если это не требуется отдельной задаче.
Безопасность подключений: pg_hba.conf
pg_hba.conf определяет, кто и откуда может подключаться, к какой базе и каким способом аутентифицироваться.
# TYPE DATABASE USER ADDRESS METHOD
local all postgres peer
host app_db app_user 10.10.0.0/16 scram-sha-256
host app_db app_readonly 10.10.0.0/16 scram-sha-256
host all all 0.0.0.0/0 rejectДля новых конфигураций предпочтителен scram-sha-256. Порядок правил имеет значение: применяется первая подходящая строка.
После изменения:
SELECT pg_reload_conf();
SELECT line_number, type, database, user_name,
address, auth_method, error
FROM pg_hba_file_rules
ORDER BY line_number;Не открывайте порт PostgreSQL в интернет без необходимости. Ограничивайте доступ firewall, security groups, VPN или приватной сетью.
Репликация: базово
Репликация передаёт изменения с primary на standby. Она используется для высокой доступности и чтения с реплики, но не заменяет backup и point-in-time recovery.
Основные термины
- Primary — сервер, принимающий записи.
- Standby — сервер, применяющий WAL от primary.
- Streaming replication — передача WAL в потоковом режиме.
- Synchronous replication — primary ожидает подтверждение синхронной реплики для соответствующих транзакций.
- Asynchronous replication — primary не ждёт реплику; при аварии возможна потеря последних подтверждённых изменений.
- Replication slot — удерживает WAL, пока потребитель его не заберёт.
Настройка primary
wal_level = replica
max_wal_senders = 10
max_replication_slots = 10
wal_keep_size = 1GBРазрешение в pg_hba.conf только для standby:
host replication replicator 10.10.0.20/32 scram-sha-256Отдельная роль репликации:
CREATE ROLE replicator
WITH REPLICATION LOGIN PASSWORD 'from-secret-manager';Создание standby через base backup
pg_basebackup \
-h primary.internal \
-U replicator \
-D /var/lib/postgresql/data \
-Fp \
-Xs \
-P \
-R-R создаёт настройки подключения к primary для standby. Пароль передавайте через .pgpass с правами 600 или через секрет-хранилище.
Состояние primary:
SELECT client_addr, application_name, state, sync_state,
sent_lsn, write_lsn, flush_lsn, replay_lsn
FROM pg_stat_replication;Состояние standby:
SELECT pg_is_in_recovery();
SELECT status, sender_host, sender_port,
latest_end_lsn, latest_end_time
FROM pg_stat_wal_receiver;Репликационный lag
SELECT client_addr, application_name, state,
pg_size_pretty(pg_wal_lsn_diff(sent_lsn, replay_lsn))
AS replay_lag
FROM pg_stat_replication;Лаг оценивают и в байтах, и во времени. Причины могут включать медленный диск, нагрузку на standby, сетевые проблемы, долгие запросы и проблемы архивирования WAL.
Replication slots
SELECT slot_name, slot_type, active, restart_lsn,
wal_status, safe_wal_size
FROM pg_replication_slots;Неиспользуемый слот может заполнить диск WAL. Удаляйте его только после проверки:
SELECT pg_drop_replication_slot('old_slot');Ошибочный DELETE обычно реплицируется на standby, поэтому реплика не заменяет backup.
Мониторинг производительности
Оценивайте latency, throughput, блокировки, cache hit ratio, I/O, checkpoints, vacuum, рост таблиц и число подключений вместе, а не по одному показателю.
Активные запросы и долгие транзакции
SELECT pid, usename, datname, state,
wait_event_type, wait_event,
now() - query_start AS duration,
LEFT(query, 300) AS query
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY duration DESC;SELECT pid, usename, datname, xact_start,
now() - xact_start AS xact_age,
state, LEFT(query, 200) AS query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start;Долгие транзакции удерживают старые версии строк и могут мешать vacuum.
Блокировки
SELECT blocked.pid AS blocked_pid,
blocking.pid AS blocking_pid,
now() - blocked.query_start AS blocked_for,
LEFT(blocked.query, 200) AS blocked_query,
LEFT(blocking.query, 200) AS blocking_query
FROM pg_stat_activity AS blocked
JOIN pg_locks AS blocked_lock
ON blocked_lock.pid = blocked.pid
AND NOT blocked_lock.granted
JOIN pg_locks AS blocking_lock
ON blocking_lock.locktype = blocked_lock.locktype
AND blocking_lock.database IS NOT DISTINCT FROM blocked_lock.database
AND blocking_lock.relation IS NOT DISTINCT FROM blocked_lock.relation
AND blocking_lock.page IS NOT DISTINCT FROM blocked_lock.page
AND blocking_lock.tuple IS NOT DISTINCT FROM blocked_lock.tuple
AND blocking_lock.virtualxid IS NOT DISTINCT FROM blocked_lock.virtualxid
AND blocking_lock.transactionid IS NOT DISTINCT FROM blocked_lock.transactionid
AND blocking_lock.classid IS NOT DISTINCT FROM blocked_lock.classid
AND blocking_lock.objid IS NOT DISTINCT FROM blocked_lock.objid
AND blocking_lock.objsubid IS NOT DISTINCT FROM blocked_lock.objsubid
AND blocking_lock.granted
JOIN pg_stat_activity AS blocking
ON blocking.pid = blocking_lock.pid
WHERE blocked.pid <> blocking.pid;Для отдельного процесса:
SELECT pg_blocking_pids(<blocked_pid>);Статистика таблиц
SELECT schemaname, relname, n_live_tup, n_dead_tup,
n_mod_since_analyze, last_autovacuum,
last_autoanalyze, autovacuum_count
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;Большое количество n_dead_tup может указывать на неэффективный autovacuum, долгие транзакции или большое число обновлений и удалений.
Использование индексов
SELECT schemaname, relname AS table_name,
indexrelname AS index_name, idx_scan,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
ORDER BY idx_scan ASC, pg_relation_size(indexrelid) DESC;Нулевой idx_scan не означает, что индекс можно немедленно удалить: статистика могла сброситься, а индекс может поддерживать уникальность или редкий важный запрос.
Cache hit ratio и размеры
SELECT datname, blks_hit, blks_read,
round(100.0 * blks_hit / NULLIF(blks_hit + blks_read, 0), 2)
AS cache_hit_percent
FROM pg_stat_database
WHERE datname IS NOT NULL;SELECT schemaname, relname,
pg_size_pretty(pg_total_relation_size(relid)) AS total_size,
pg_size_pretty(pg_table_size(relid)) AS table_size,
pg_size_pretty(pg_indexes_size(relid)) AS indexes_size
FROM pg_catalog.pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC
LIMIT 20;Вакуумирование и обслуживание
PostgreSQL использует MVCC: после UPDATE или DELETE старые версии строк не всегда удаляются немедленно. Их очищает vacuum, когда они больше не нужны активным транзакциям.
VACUUM, VACUUM FULL, ANALYZE
VACUUM app.orders;
VACUUM (VERBOSE, ANALYZE) app.orders;
ANALYZE app.orders;Обычный VACUUM возвращает место для повторного использования внутри таблицы и обычно не блокирует обычную работу надолго. Он не обязан уменьшать физический файл на диске.
VACUUM (FULL, VERBOSE, ANALYZE) app.orders;VACUUM FULL переписывает таблицу, требует сильную блокировку и дополнительное место. Это не обычная ежедневная операция production. Для контролируемого уменьшения также применяют инструменты вроде pg_repack, проверив их ограничения.
ANALYZE собирает статистику для планировщика:
ALTER TABLE app.orders
ALTER COLUMN customer_id SET STATISTICS 500;
ANALYZE app.orders;Autovacuum
SELECT name, setting, unit
FROM pg_settings
WHERE name LIKE 'autovacuum%'
ORDER BY name;Пример отправных параметров:
autovacuum = on
autovacuum_max_workers = 3
autovacuum_naptime = 1min
autovacuum_vacuum_scale_factor = 0.2
autovacuum_analyze_scale_factor = 0.1Для активно изменяемой таблицы настройки можно переопределить:
ALTER TABLE app.events SET (
autovacuum_vacuum_scale_factor = 0.02,
autovacuum_analyze_scale_factor = 0.01,
autovacuum_vacuum_threshold = 5000,
autovacuum_analyze_threshold = 5000
);Слишком агрессивный autovacuum увеличивает I/O, а слишком редкий приводит к росту таблиц и устаревшей статистике. Подбирайте параметры по скорости накопления dead tuples.
Freeze и wraparound
Не отключайте autovacuum без отдельного плана обслуживания. Проверяйте возраст XID:
SELECT datname, age(datfrozenxid) AS xid_age
FROM pg_database
ORDER BY xid_age DESC;SELECT schemaname, relname, age(relfrozenxid) AS xid_age,
n_live_tup, n_dead_tup
FROM pg_stat_all_tables
ORDER BY age(relfrozenxid) DESC
LIMIT 20;Обслуживание индексов
REINDEX INDEX CONCURRENTLY app.orders_customer_created_idx;
REINDEX TABLE CONCURRENTLY app.orders;CONCURRENTLY уменьшает блокирование, но требует больше времени и места и не выполняется внутри транзакционного блока.
Продвинутая оптимизация индексов
Индекс ускоряет чтение, но занимает место и увеличивает стоимость INSERT, UPDATE и DELETE. Его нужно подбирать по реальным шаблонам запросов.
B-tree и составные индексы
CREATE INDEX orders_customer_created_idx
ON app.orders (customer_id, created_at DESC);Порядок столбцов важен. Такой индекс подходит для фильтрации по customer_id и сортировки по created_at. Индекс (created_at, customer_id) может вести себя иначе для тех же условий.
Типы индексов
- B-tree — равенство, диапазоны и сортировка.
- Hash — равенство, применяется реже B-tree.
- GIN — массивы,
jsonb, полнотекстовый поиск. - GiST — геоданные, диапазоны и специальные типы.
- SP-GiST — отдельные разреженные структуры.
- BRIN — очень большие таблицы с корреляцией физического порядка и значения, например временные события.
CREATE INDEX products_attributes_gin_idx
ON app.products USING gin (attributes);
CREATE INDEX reservations_period_gist_idx
ON app.reservations USING gist (reserved_period);
CREATE INDEX events_created_brin_idx
ON app.events USING brin (created_at);Частичный индекс
CREATE INDEX users_active_email_idx
ON app.users (email)
WHERE deleted_at IS NULL;Запрос должен содержать совместимое условие:
SELECT id, email
FROM app.users
WHERE deleted_at IS NULL
AND email = 'user@example.com';Индекс по выражению
CREATE INDEX users_lower_email_idx
ON app.users (lower(email));SELECT *
FROM app.users
WHERE lower(email) = lower('User@example.com');Covering index и INCLUDE
CREATE INDEX orders_customer_created_cover_idx
ON app.orders (customer_id, created_at DESC)
INCLUDE (status, total_amount);INCLUDE может сделать возможным Index Only Scan, но это зависит от visibility map, стоимости плана и актуальности страниц.
Уникальность и ограничения
Для бизнес-правил используйте ограничения:
ALTER TABLE app.users
ADD CONSTRAINT users_email_unique UNIQUE (email);Индекс ограничения нельзя удалять отдельно, не изменив само ограничение.
Keyset-pagination
Большой OFFSET может быть дорогим:
SELECT id, created_at
FROM app.events
ORDER BY created_at DESC, id DESC
LIMIT 50 OFFSET 100000;Для keyset-pagination:
CREATE INDEX events_created_id_idx
ON app.events (created_at DESC, id DESC);SELECT id, created_at
FROM app.events
WHERE (created_at, id) < ('2026-09-21 12:00:00+00', 5000)
ORDER BY created_at DESC, id DESC
LIMIT 50;Нерациональные и дублирующиеся индексы
SELECT schemaname, relname AS table_name,
indexrelname AS index_name, idx_scan,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;Перед удалением проверьте период сбора статистики, ограничения, редкие запросы, отчёты, порядок колонок, WHERE, INCLUDE и тип индекса. Дублирующий индекс увеличивает стоимость записи без пользы для чтения.
EXPLAIN и EXPLAIN ANALYZE
EXPLAIN показывает выбранный план. EXPLAIN ANALYZE фактически выполняет запрос и добавляет измеренные значения.
EXPLAIN
SELECT *
FROM app.orders
WHERE customer_id = 42;EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS)
SELECT id, created_at, total_amount
FROM app.orders
WHERE customer_id = 42
ORDER BY created_at DESC
LIMIT 20;Условный план:
Limit (cost=0.42..12.10 rows=20 width=32)
(actual time=0.080..0.120 rows=20 loops=1)
-> Index Scan using orders_customer_created_idx on orders
(cost=0.42..1200.00 rows=2000 width=32)
(actual time=0.078..0.116 rows=20 loops=1)
Index Cond: (customer_id = 42)
Planning Time: 0.400 ms
Execution Time: 0.150 mscost— внутренняя оценка, не миллисекунды.rows— ожидаемое число строк.actual time— фактическое время узла.loops— число повторных запусков узла.Planning Time— построение плана.Execution Time— выполнение.BUFFERS— обращения к буферам.
Основные узлы
Seq Scan— последовательное чтение таблицы.Index Scan— чтение индекса с обращением к таблице.Index Only Scan— чтение из индекса при подходящем visibility map.Bitmap Index Scan/Bitmap Heap Scan— пакетный доступ по индексу.Nested Loop— вложенные циклы.Hash Join— hash-соединение.Merge Join— соединение отсортированных потоков.Sort— сортировка, иногда с временными файлами.Aggregate/HashAggregate— агрегация.
Оценка строк и статистика
Сравнивайте rows и actual rows:
(actual time=10.000..50.000 rows=5 loops=1)Сильное расхождение означает устаревшую статистику или сложное распределение данных:
ANALYZE app.orders;
ALTER TABLE app.orders
ALTER COLUMN customer_id SET STATISTICS 500;
ANALYZE app.orders;Для связанных столбцов можно создать расширенную статистику:
CREATE STATISTICS orders_customer_status_stats
(dependencies, ndistinct, mcv)
ON customer_id, status
FROM app.orders;
ANALYZE app.orders;BUFFERS, WAL и временные файлы
EXPLAIN (ANALYZE, BUFFERS, WAL)
UPDATE app.orders
SET status = 'archived'
WHERE created_at < now() - interval '1 year';Большой temp read или temp written может указывать на сортировку или hash-операцию, не поместившуюся в work_mem.
EXPLAIN ANALYZE для INSERT, UPDATE и DELETE изменяет данные. Для теста можно использовать транзакцию:
BEGIN;
EXPLAIN (ANALYZE, BUFFERS)
DELETE FROM app.orders WHERE id = 123;
ROLLBACK;Даже внутри транзакции возможны блокировки и запуск триггеров, поэтому на production нужна осторожность.
Что проверять в плане
- Соответствует ли оценка
rowsфактическим строкам. - Нет ли неожиданного полного сканирования большой таблицы.
- Нет ли сортировок с временными файлами.
- Подходят ли порядок и состав индексов.
- Не повторяется ли дорогой узел из-за большого
loops. - Нет ли неявного приведения типов.
- Актуальна ли статистика.
- Не ухудшил ли индекс запись и autovacuum.
Seq Scan не всегда означает проблему: для маленькой таблицы или чтения большей части данных он может быть оптимальным.
Диагностика медленного запроса
1. Найти запрос
SELECT calls, mean_exec_time, total_exec_time, rows,
LEFT(query, 500) AS query
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 20;2. Получить план на репрезентативных параметрах
EXPLAIN (ANALYZE, BUFFERS, SETTINGS)
SELECT id, status, created_at
FROM app.orders
WHERE customer_id = 42
AND status = 'paid'
ORDER BY created_at DESC
LIMIT 100;3. Проверить статистику
ANALYZE app.orders;4. Внести одно изменение
CREATE INDEX CONCURRENTLY orders_paid_customer_created_idx
ON app.orders (customer_id, created_at DESC)
WHERE status = 'paid';5. Сравнить результат
Сравнивайте не только latency, но и нагрузку на запись, размер индекса, I/O, autovacuum и планы похожих запросов на разных параметрах.
6. Зафиксировать решение
Храните исходный план, описание проблемы, изменение, новый план, дату и условия теста. Это помогает откатывать неудачные оптимизации.
Шпаргалка команд
-- Сервер и конфигурация
SELECT version();
SHOW ALL;
SELECT * FROM pg_settings WHERE name = 'shared_buffers';
SELECT pg_reload_conf();
-- Подключения и блокировки
SELECT * FROM pg_stat_activity;
SELECT pg_blocking_pids(<pid>);
SELECT * FROM pg_locks;
-- Размеры
SELECT pg_size_pretty(pg_database_size(current_database()));
SELECT pg_size_pretty(pg_total_relation_size('app.orders'));
SELECT pg_size_pretty(pg_relation_size('app.orders'));
SELECT pg_size_pretty(pg_indexes_size('app.orders'));
-- Обслуживание
VACUUM (ANALYZE) app.orders;
ANALYZE app.orders;
REINDEX INDEX CONCURRENTLY app.orders_customer_created_idx;
-- Диагностика
EXPLAIN (ANALYZE, BUFFERS, SETTINGS)
SELECT ...;
-- Репликация
SELECT pg_is_in_recovery();
SELECT * FROM pg_stat_replication;
SELECT * FROM pg_stat_wal_receiver;
SELECT * FROM pg_replication_slots;Итоговый чек-лист
- Конфигурация PostgreSQL изменяется контролируемым способом.
-
listen_addressesиpg_hba.confразрешают только необходимый доступ. - Приложения используют отдельные роли без superuser.
- Секреты не находятся в Git и логах.
- Включены подходящие журналы и настроена ротация.
- Заданы тайм-ауты запросов, блокировок и простаивающих транзакций.
- Autovacuum работает, контролируются dead tuples и возраст XID.
- Таблицы анализируются после массовых изменений.
- Репликация отслеживается по lag и состоянию WAL.
- Replication slots не удерживают бесконечно растущий WAL.
- CPU, RAM, I/O, диск и подключения мониторятся.
- Медленные запросы анализируются через
pg_stat_statementsиEXPLAIN. - Индексы создаются по реальным запросам и периодически проверяются.
- Перед
VACUUM FULL,REINDEXи массовыми изменениями оцениваются блокировки и свободное место. - Backup и восстановление регулярно тестируются.
PostgreSQL администрируется как система взаимосвязанных практик: корректные права, контролируемая конфигурация, актуальная статистика, рабочий autovacuum, мониторинг, проверенные планы запросов и регулярно протестированное восстановление.