PostgreSQL - штука прекрасная. Пока не приходит момент:

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

Именно в такие моменты начинается археология:

«Так… где же была та команда с ALTER DEFAULT PRIVILEGES?..»

Ниже - большая практическая памятка по PostgreSQL. Без академической воды. С примерами из реальной инфраструктуры.

Подключение и базовые команды

Подключение к PostgreSQL

psql -h HOST -p 5432 -U USERNAME -d DATABASE

Пример:

psql -h 10.10.10.15 -U postgres -d app_db

Полезные команды внутри psql

Команда

Описание

\l

список баз

\du

список ролей

\dt

список таблиц

\dn

список схем

\conninfo

информация о подключении

\x

красивый вертикальный вывод

\q

выход

\d table_name

описание таблицы

\df

список функций

\dv

список view

\timing

показать время выполнения запросов

Пользователи, роли и права

Создание пользователей

Создать пользователя

CREATE USER app_user WITH PASSWORD 'SUPER_PASSWORD';

Создать пользователя с ограничением срока действия пароля

CREATE USER app_user
WITH PASSWORD 'SUPER_PASSWORD'
VALID UNTIL '2027-01-01';

Создать пользователя только для чтения

CREATE USER readonly_user
WITH PASSWORD 'PASSWORD';

Роли и группы

В PostgreSQL роль и пользователь - почти одно и то же.

Обычно делают так:

  • создают роль,
  • выдают ей права,
  • потом добавляют пользователей в эту роль.

Это сильно упрощает жизнь.

Потому что:

«Выдать доступ 40 аналитикам»
звучит намного лучше, чем
«40 раз руками делать GRANT».

Создаем роль readonly

CREATE ROLE readonly;

Создаем роль readwrite

CREATE ROLE readwrite;

Выдача RO-доступа

Доступ к существующим таблицам

GRANT USAGE ON SCHEMA public TO readonly;
GRANT SELECT
ON ALL TABLES IN SCHEMA public
TO readonly;

Доступ к будущим таблицам

Очень важный момент.

Без этого:

  • новые таблицы создались,
  • а доступов нет,
  • аналитики уже пишут в чат:

«У нас опять ничего не работает».

ALTER DEFAULT PRIVILEGES
IN SCHEMA public
GRANT SELECT ON TABLES TO readonly;

Доступ к sequence

Если используются SERIAL или BIGSERIAL.

GRANT SELECT
ON ALL SEQUENCES IN SCHEMA public
TO readonly;

Привязка пользователя к роли

GRANT readonly TO analyst_user;

Полный RW-доступ

GRANT CONNECT ON DATABASE app_db TO readwrite;
GRANT USAGE ON SCHEMA public TO readwrite;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO readwrite;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO readwrite;

Права на будущие таблицы

ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO readwrite;

Доступ только к конкретной таблице

GRANT SELECT ON TABLE users TO analyst_user;

Отозвать права

REVOKE INSERT, UPDATE, DELETE ON TABLE users FROM analyst_user;

Сделать пользователя суперпользователем

Обычно делать так не надо. Но иногда ночью в проде:

«Да выдайте уже ему superuser и спать пойдем».

ALTER USER app_user WITH SUPERUSER;

Удаление пользователей и ролей

Удалить пользователя

DROP USER app_user;

Удалить роль

DROP ROLE readonly;

Базы данных

Создание базы

CREATE DATABASE app_db;

Создание базы с владельцем

CREATE DATABASE app_db OWNER app_user;

Смена владельца базы

ALTER DATABASE app_db OWNER TO new_owner;

Переименование базы

ALTER DATABASE old_name RENAME TO new_name;

Просмотр размеров БД

SELECT
    datname,
    pg_size_pretty(pg_database_size(datname))
FROM pg_database
ORDER BY pg_database_size(datname) DESC;

Таблицы и схемы

Создать схему

CREATE SCHEMA analytics;

Выдать права на схему

GRANT USAGE ON SCHEMA analytics TO readonly;

Размер таблиц

SELECT
    relname AS table_name,
    pg_size_pretty(pg_total_relation_size(relid)) AS total_size
FROM pg_catalog.pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC;

Самые тяжелые таблицы

SELECT
    schemaname,
    relname,
    pg_size_pretty(pg_total_relation_size(relid)) AS size
FROM pg_catalog.pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC
LIMIT 20;

Дамп и восстановление

Дамп базы

Обычный dump

pg_dump \
-h HOST \
-p 5432 \
-U USER \
-d DATABASE \
-f dump.sql

Dump в custom формате

Самый удобный вариант.

pg_dump \
--format=custom \
--compress=5 \
--verbose \
--file=backup.dump \
--dbname=app_db

Dump directory format

Очень полезен для больших баз. Поддерживает parallel restore.

pg_dump \
--clean \
--encoding=utf8 \
--format=directory \
--jobs=5 \
--verbose \
--compress=5 \
--file=backup_dir \
--dbname=app_db

Исключение таблиц

pg_dump \
--exclude-table=logs \
--exclude-table=events \
...

Исключение данных таблицы

Структура будет, данных - нет.

pg_dump \
--exclude-table-data=logs

Архивирование

tar -cvf backup.tar.gz backup_dir/

Восстановление

Из SQL dump

psql \
-h HOST \
-U USER \
-d DATABASE \
-f dump.sql

Из custom dump

pg_restore \
--dbname=app_db \
--jobs=5 \
--verbose \
backup.dump

Из directory dump

pg_restore \
--dbname=app_db \
--format=directory \
--jobs=5 \
--verbose \
backup_dir

Восстановление только схемы

pg_restore --schema-only

Восстановление только данных

pg_restore --data-only

Репликация и Patroni

Проверка реплик

Лаг репликации

SELECT
    r.client_addr,
    r.usename,
    r.application_name,
    r.state,
    r.sync_state,
    (pg_wal_lsn_diff(pg_current_wal_lsn(), r.sent_lsn) / 1024)::int AS pending,
    (pg_wal_lsn_diff(r.sent_lsn, r.write_lsn) / 1024)::int AS write,
    (pg_wal_lsn_diff(r.write_lsn, r.flush_lsn) / 1024)::int AS flush,
    (pg_wal_lsn_diff(r.flush_lsn, r.replay_lsn) / 1024)::int AS replay,
    ((pg_wal_lsn_diff(pg_current_wal_lsn(), r.replay_lsn)) / 1024)::int AS total_lag
FROM pg_catalog.pg_stat_replication r;

Как интерпретировать lag

Поле

Что означает

pending

WAL не отправлен - сеть

write

WAL не записан - проблемы с диском

flush

WAL не flushed - IO

replay

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

total_lag

суммарное отставание

Проверка статуса реплики

На replica:

SELECT pg_is_in_recovery();
  • true → replica
  • false → master

Текущий WAL

SELECT pg_current_wal_lsn();

Проверка replication slots

SELECT * FROM pg_replication_slots;

Проверка archive status

SELECT * FROM pg_stat_archiver;

Активные подключения

Кто подключен к базе

SELECT
    pid,
    usename,
    application_name,
    client_addr,
    state,
    query
FROM pg_stat_activity;

Долгие запросы

SELECT
    pid,
    now() - query_start AS duration,
    query
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY duration DESC;

Завершить запрос

SELECT pg_cancel_backend(PID);

Убить подключение

Когда кто-то держит миграцию уже 3 часа

SELECT pg_terminate_backend(PID);

Блокировки

Кто кого блокирует

SELECT
    blocked_locks.pid     AS blocked_pid,
    blocked_activity.usename  AS blocked_user,
    blocking_locks.pid     AS blocking_pid,
    blocking_activity.usename AS blocking_user,
    blocked_activity.query AS blocked_statement
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity
    ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks blocking_locks
    ON blocking_locks.locktype = blocked_locks.locktype
WHERE NOT blocked_locks.granted;

VACUUM и обслуживание

Обычный vacuum

VACUUM;

Анализ статистики

ANALYZE;

Полный vacuum

Осторожно: блокирует таблицы.

VACUUM FULL;

Reindex

REINDEX DATABASE app_db;

Проверка bloat

SELECT
    schemaname,
    relname,
    n_dead_tup
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;

Индексы

Проверка неиспользуемых индексов

SELECT
    relname AS table_name,
    indexrelname AS index_name,
    idx_scan
FROM pg_stat_user_indexes
ORDER BY idx_scan;

Размер индексов

SELECT
    indexrelname,
    pg_size_pretty(pg_relation_size(indexrelid))
FROM pg_stat_user_indexes
ORDER BY pg_relation_size(indexrelid) DESC;

Мониторинг производительности

Самые медленные запросы ( pg_stat_statements )

Сначала включаем расширение:

CREATE EXTENSION pg_stat_statements;

Топ запросов

SELECT
    query,
    calls,
    total_exec_time,
    mean_exec_time
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;

Cache hit ratio

SELECT
    sum(heap_blks_read) AS heap_read,
    sum(heap_blks_hit)  AS heap_hit,
    (sum(heap_blks_hit) - sum(heap_blks_read))
        / sum(heap_blks_hit) AS ratio
FROM pg_statio_user_tables;

Полезные команды для DevOps

Проверка версий

SELECT version();

Проверка конфигурации

SHOW shared_buffers;
SHOW work_mem;
SHOW maintenance_work_mem;
SHOW max_connections;

Где лежит конфиг

SHOW config_file;

Перезагрузка конфига

Без рестарта PostgreSQL.

SELECT pg_reload_conf();

Проверка checkpoint

SELECT * FROM pg_stat_bgwriter;

Проверка autovacuum

SELECT
    relname,
    last_autovacuum,
    vacuum_count
FROM pg_stat_user_tables;

WAL и архивы

Размер WAL

SELECT
    pg_size_pretty(
        sum(size)
    )
FROM pg_ls_waldir();

Принудительный switch WAL

SELECT pg_switch_wal();

Создать checkpoint

CHECKPOINT;

Полезные shell-команды

Проверка доступности PostgreSQL

pg_isready

Подключение без ввода пароля

Файл:

~/.pgpass

Содержимое:

HOST:PORT:DATABASE:USER:PASSWORD

Права:

chmod 600 ~/.pgpass

Экспорт переменных

export PGHOST=127.0.0.1
export PGPORT=5432
export PGUSER=postgres
export PGPASSWORD=secret

Полезные советы из боли и страданий.

Никогда не выдавайте SUPERUSER приложению

Потому что однажды ORM решит:

«А давай удалим всё».

Всегда используйте роли

Не выдавайте права напрямую пользователям.

Иначе через год:

  • кто что может,
  • кто откуда унаследовал права,
  • кто вообще жив -

будет выясняться через спиритический сеанс.

Используйте ALTER DEFAULT PRIVILEGES

Это одна из самых забываемых вещей в PostgreSQL. И одна из самых важных.

Не делайте VACUUM FULL

без причины

Это не:

«почистить немного».

Это:

«остановить жизнь таблицы на время операции».

Для больших БД используйте

directory
dump

Потому что:

  • parallel restore,
  • меньше боли,
  • быстрее восстановление,
  • удобнее миграции.

Следите за replication slots

Иначе WAL может внезапно занять:

  • 300 ГБ,
  • 800 ГБ,
  • «почему диск умер».

Мини-чеклист для продакшена

Что проверять

Почему

replication lag

чтобы replica не жила в прошлом

autovacuum

иначе bloat

pg_stat_statements

поиск медленных запросов

WAL size

чтобы не закончился диск

locks

чтобы миграции не висели

checkpoints

чтобы не убивать IO

unused indexes

лишняя нагрузка

long transactions

источник боли

PostgreSQL - очень мощная система. Но чем больше инфраструктура, тем чаще он превращается из «базы данных» в отдельную форму искусства и психологической устойчивости.

Особенно в два часа ночи, когда:

  • Patroni решил провести выборы,
  • реплика отстала,
  • autovacuum съел IO,
  • а разработчик говорит:

«Я всего лишь добавил индекс CONCURRENTLY…»