Раздел 6 — Структура, пользователи и администрирование

Как разграничить доступ, не потерять данные и понять, чем занят сервер

Прогресс курса Раздел 6 из 13

Что вы освоите в этом разделе

6 академических часов: 2 часа теории, 4 часа практики. Нужен тот же сервер, что и в разделе 5, с загруженной базой socialnet.
01

Схемы и search_path

Схема — пространство имён внутри базы данных. Таблицы, представления, функции и типы живут в схемах. Полное имя объекта состоит из двух частей: схема.таблица.

Зачем нужны схемы

  • Логическая группировка. Отдельно основные данные, отдельно отчётность, отдельно служебные таблицы.
  • Разграничение доступа. Права выдаются на схему целиком: аналитик видит reports и не видит billing.
  • Несколько приложений в одной базе. Каждое в своей схеме, имена таблиц не конфликтуют.
  • Разделение арендаторов. Схема на клиента в SaaS — рабочий приём при небольшом их числе.
CREATE SCHEMA reports;
CREATE SCHEMA staging AUTHORIZATION etl_user;

-- Полное имя объекта
CREATE TABLE reports.monthly_activity (
    month      date    PRIMARY KEY,
    active_users integer NOT NULL
);

-- Список схем
\dn

search_path — список схем, в которых сервер ищет объект, если имя указано без схемы. Работает как PATH в командной строке: побеждает первое совпадение.

SHOW search_path;
-- "$user", public

-- Изменить на время сессии
SET search_path TO reports, public;

-- Закрепить за ролью навсегда
ALTER ROLE analyst SET search_path TO reports, public;

-- Закрепить за базой
ALTER DATABASE socialnet SET search_path TO app, public;
Значение "$user" означает схему с именем текущей роли. Если такой схемы нет, элемент молча пропускается. Отсюда неочевидное поведение: создали схему student_app, и таблицы того же пользователя вдруг стали создаваться в ней, а не в public. В функциях и триггерах search_path задают явно — иначе поведение зависит от того, кто вызвал.

Табличное пространство — привязка объектов к каталогу на диске. Применяется, когда нужно разнести данные по разным носителям: горячие таблицы на быстрый SSD, архив на медленный большой диск.

CREATE TABLESPACE archive LOCATION '/mnt/hdd/pgdata';
CREATE TABLE old_logs (...) TABLESPACE archive;

-- Что есть в кластере
\db
На небольшом сервере с одним диском табличные пространства не нужны и только усложняют резервное копирование. Это инструмент для конфигураций, где носители действительно разные по скорости или объёму.
02

Роли

В PostgreSQL нет отдельных сущностей «пользователь» и «группа» — есть единое понятие роль. Роль с правом входа ведёт себя как пользователь, роль без него — как группа.

-- Роль для входа = пользователь
CREATE ROLE ivanov LOGIN PASSWORD '…';

-- Роль без входа = группа
CREATE ROLE analysts;

-- Включить пользователя в группу
GRANT analysts TO ivanov;

-- CREATE USER — это синоним CREATE ROLE ... LOGIN
АтрибутЧто даёт
LOGINправо подключаться к серверу
SUPERUSERобход всех проверок прав — выдавать крайне неохотно
CREATEDBправо создавать базы данных
CREATEROLEправо создавать и изменять роли
REPLICATIONподключение в режиме репликации
INHERITавтоматически получать права групповых ролей (по умолчанию включён)
CONNECTION LIMIT nпредел одновременных соединений роли
VALID UNTILсрок действия пароля
-- Посмотреть роли и их атрибуты
\du

-- Изменить атрибуты
ALTER ROLE ivanov CONNECTION LIMIT 5;
ALTER ROLE ivanov VALID UNTIL '2027-01-01';
ALTER ROLE ivanov NOLOGIN;   -- заблокировать вход, не удаляя роль
Роли общие для всего кластера, а не для отдельной базы. Создав роль в одной базе, вы увидите её во всех. Права же выдаются на объекты конкретной базы — поэтому наличие роли ещё ничего не разрешает.
03

Привилегии

У каждого объекта есть владелец — по умолчанию тот, кто его создал. Владелец может всё: изменять структуру, выдавать права другим, удалять объект. Остальные получают доступ только через явно выданные привилегии.

ПривилегияНа чтоЧто разрешает
SELECTтаблица, представлениечитать данные
INSERTтаблицадобавлять строки
UPDATEтаблицаизменять строки
DELETEтаблицаудалять строки
TRUNCATEтаблицаочищать целиком
REFERENCESтаблицассылаться внешним ключом
USAGEсхема, последовательностьобращаться к объектам схемы
CREATEсхема, базасоздавать объекты внутри
CONNECTбазаподключаться к базе
EXECUTEфункция, процедуравызывать
-- Выдать права
GRANT SELECT ON users TO analysts;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA app TO app_role;
GRANT USAGE ON SCHEMA app TO app_role;

-- Права на отдельные столбцы
GRANT SELECT (id, name, city) ON users TO analysts;

-- Отозвать
REVOKE DELETE ON users FROM app_role;

-- Посмотреть выданные права
\dp users

Права по умолчанию — то, о чём забывают

GRANT ... ON ALL TABLES действует только на таблицы, существующие в момент выполнения. Созданная завтра таблица прав не получит, и приложение упадёт после ближайшей миграции.

-- Права на всё, что БУДЕТ создано ролью owner в схеме app
ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA app
    GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_role;

ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA app
    GRANT USAGE, SELECT ON SEQUENCES TO app_role;

-- Посмотреть настроенные умолчания
\ddp
Умолчания привязаны к роли, которая создаёт объект. Если миграции запускаются то от app_owner, то от postgres, права получит только часть таблиц. Договоритесь, кто владеет схемой, и запускайте миграции всегда от этой роли.

Отдельная тонкость — роль PUBLIC, означающая «все роли». По умолчанию она имеет CONNECT к новым базам и USAGE, а в старых версиях имела и CREATE на схему public. Начиная с PostgreSQL 15 право CREATE у PUBLIC отобрано — это изменение ломало множество инструкций из интернета.

-- Закрыть базу от посторонних подключений
REVOKE CONNECT ON DATABASE socialnet FROM PUBLIC;
GRANT CONNECT ON DATABASE socialnet TO app_role;
04

Рабочая модель прав: владелец и приложение

Схема доступа, которая применяется в промышленных системах и которую стоит взять за образец. Две роли вместо одной.

Роль владельца

app_owner — владеет схемой и всеми объектами. Ей выполняются миграции: CREATE TABLE, ALTER, DROP. Используется редко и только процессом развёртывания.

Роль приложения

app_user — под ней работает приложение в бою. Права только на данные: SELECT, INSERT, UPDATE, DELETE. Структуру менять не может.

Смысл разделения простой: ошибка или уязвимость в коде приложения не должна приводить к DROP TABLE. При SQL-инъекции злоумышленник получает права той роли, под которой работает приложение — и разница между «прочитал данные» и «удалил базу» определяется здесь.

-- 1. Роли
CREATE ROLE app_owner LOGIN PASSWORD '…';
CREATE ROLE app_user  LOGIN PASSWORD '…';

-- 2. База и схема принадлежат владельцу
CREATE DATABASE socialnet OWNER app_owner;
\c socialnet
CREATE SCHEMA app AUTHORIZATION app_owner;

-- 3. Закрываем всё лишнее
REVOKE ALL ON SCHEMA public FROM PUBLIC;
REVOKE CONNECT ON DATABASE socialnet FROM PUBLIC;

-- 4. Приложению — доступ к схеме и данным, но не к структуре
GRANT CONNECT ON DATABASE socialnet TO app_user;
GRANT USAGE ON SCHEMA app TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE
    ON ALL TABLES IN SCHEMA app TO app_user;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA app TO app_user;

-- 5. И на всё, что появится в будущих миграциях
ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA app
    GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;
ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA app
    GRANT USAGE, SELECT ON SEQUENCES TO app_user;

-- 6. Чтобы приложение не писало схему в запросах
ALTER ROLE app_user SET search_path TO app;
Третья роль — app_readonly с одним лишь SELECT — выдаётся аналитикам и системам отчётности. Тяжёлый отчёт под такой ролью не испортит данные при любой ошибке в запросе.

Когда разграничения на уровне таблиц недостаточно, применяют защиту на уровне строк: пользователь видит только свои записи.

ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

CREATE POLICY orders_own ON orders
    FOR ALL
    TO app_user
    USING (manager_id = current_setting('app.manager_id')::integer);
Защита на уровне строк не действует на владельца таблицы и на суперпользователя, если явно не указано FORCE ROW LEVEL SECURITY. Проверять политику нужно под той ролью, для которой она написана, иначе легко решить, что она работает, когда она просто не применялась.
05

Резервное копирование и восстановление

Два принципиально разных подхода: логическая выгрузка и физическая копия файлов.

Логическая копия: pg_dump

Выгружает содержимое базы в набор SQL-команд или в специальный формат. Работает на живой базе, не блокирует её, даёт согласованный снимок на момент запуска.

# Простой текстовый SQL — читаемо, восстанавливается через psql
pg_dump -U postgres -d socialnet -f socialnet.sql

# Custom-формат — сжатый, позволяет выборочное восстановление
pg_dump -U postgres -d socialnet -F c -f socialnet.dump

# Каталог, параллельно в 4 потока — быстрее всего на больших базах
pg_dump -U postgres -d socialnet -F d -j 4 -f socialnet_dir/

# Только схема без данных / только данные
pg_dump -U postgres -d socialnet --schema-only -f schema.sql
pg_dump -U postgres -d socialnet --data-only   -f data.sql

# Отдельная таблица
pg_dump -U postgres -d socialnet -t app.users -f users.sql

# Роли и настройки кластера — их pg_dump НЕ включает
pg_dumpall -U postgres --globals-only -f globals.sql
pg_dump выгружает одну базу и не сохраняет роли, пароли и параметры кластера. Восстановив такой дамп на чистом сервере, вы получите таблицы, принадлежащие несуществующим ролям, и приложение, которому нечем подключиться. Всегда делайте pg_dumpall --globals-only рядом.
# Восстановление текстового дампа
psql -U postgres -d socialnet_new -f socialnet.sql

# Восстановление custom-формата, параллельно
pg_restore -U postgres -d socialnet_new -j 4 socialnet.dump

# Только одна таблица из полного дампа
pg_restore -U postgres -d socialnet_new -t users socialnet.dump

# Посмотреть содержимое дампа, ничего не восстанавливая
pg_restore --list socialnet.dump

Физическая копия: pg_basebackup

Копирует каталог данных целиком. Восстанавливается только на ту же мажорную версию PostgreSQL, зато вместе с журналами позволяет восстановиться на произвольный момент времени.

pg_basebackup -U replicator -D /backup/base -F t -z -P
pg_dumppg_basebackup
Что копируетодну базу логическивесь кластер физически
Перенос между версиямиданет
Выборочное восстановлениеда, вплоть до таблицынет
Восстановление на момент временинетда, с архивом WAL
Скорость на большой баземедленнобыстро
Основное применениеперенос, обновление версии, выгрузка частирезервирование прода, создание реплик
Резервная копия, из которой ни разу не восстанавливались, резервной копией не является. Регулярная проверка восстановления на отдельном сервере — обязательная часть регламента. Типичные обнаружения при первой же проверке: в скрипте нет --globals-only, у файла нулевой размер третий месяц, или места на диске для восстановления не хватает.
06

VACUUM и раздувание таблиц

PostgreSQL использует многоверсионность: UPDATE не изменяет строку на месте, а создаёт новую версию и помечает старую как устаревшую. DELETE тоже не освобождает место немедленно — он лишь помечает версию мёртвой.

Так реализована изолированность транзакций: старую версию всё ещё могут читать транзакции, начавшиеся раньше. Плата — раздувание (bloat): файл таблицы растёт, хотя строк в ней не прибавилось.

Инструменты обслуживания

  • VACUUM — помечает место мёртвых версий как свободное для повторного использования. Не блокирует таблицу, размер файла не уменьшает.
  • VACUUM FULL — перезаписывает таблицу целиком, возвращая место операционной системе. Блокирует таблицу полностью и требует места под копию. На боевой базе — только в окно обслуживания.
  • ANALYZE — обновляет статистику распределения данных. Без неё планировщик выбирает плохие планы.
  • REINDEX — перестраивает раздувшийся индекс. Вариант REINDEX CONCURRENTLY работает без долгой блокировки.
  • autovacuum — фоновый процесс, делающий VACUUM и ANALYZE автоматически. Включён по умолчанию; выключать его нельзя.
-- Ручной запуск
VACUUM ANALYZE users;
VACUUM (VERBOSE, ANALYZE) users;

-- Когда таблицу последний раз обслуживали и сколько в ней мёртвых строк
SELECT relname,
       n_live_tup,
       n_dead_tup,
       round(n_dead_tup * 100.0 / nullif(n_live_tup + n_dead_tup, 0), 1) AS dead_pct,
       last_autovacuum,
       last_autoanalyze
FROM   pg_stat_user_tables
ORDER BY n_dead_tup DESC;
Долгие открытые транзакции — главный враг autovacuum. Пока транзакция жива, очистка не может удалить версии строк, которые она теоретически способна увидеть, — даже если она просто висит без дела. Приложение, забывшее сделать COMMIT, за сутки раздувает таблицу в разы. Ищите такие транзакции запросом к pg_stat_activity по полю xact_start.
07

Мониторинг

PostgreSQL показывает своё состояние через системные представления. Знать нужно несколько.

-- Кто сейчас подключён и чем занят
SELECT pid, usename, datname, state,
       now() - xact_start AS xact_age,
       now() - query_start AS query_age,
       wait_event_type, left(query, 60) AS query
FROM   pg_stat_activity
WHERE  state <> 'idle'
ORDER BY xact_start;

-- Зависшие транзакции: idle in transaction дольше 5 минут
SELECT pid, usename, now() - xact_start AS age, left(query, 80)
FROM   pg_stat_activity
WHERE  state = 'idle in transaction'
  AND  now() - xact_start > interval '5 minutes';

-- Аварийно прервать конкретный запрос или соединение
SELECT pg_cancel_backend(12345);     -- мягко: отменить запрос
SELECT pg_terminate_backend(12345);  -- жёстко: разорвать соединение
-- Размеры баз и таблиц
SELECT datname, pg_size_pretty(pg_database_size(datname)) AS size
FROM   pg_database ORDER BY pg_database_size(datname) DESC;

SELECT schemaname, relname,
       pg_size_pretty(pg_total_relation_size(relid)) AS total,
       pg_size_pretty(pg_relation_size(relid))       AS table_only
FROM   pg_catalog.pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC
LIMIT 10;

-- Эффективность кэша: доля попаданий должна быть выше 0.95
SELECT datname,
       blks_hit, blks_read,
       round(blks_hit * 1.0 / nullif(blks_hit + blks_read, 0), 3) AS hit_ratio,
       xact_commit, xact_rollback, deadlocks
FROM   pg_stat_database
WHERE  datname IS NOT NULL;

-- Неиспользуемые индексы: занимают место и замедляют запись
SELECT schemaname, relname, indexrelname, idx_scan,
       pg_size_pretty(pg_relation_size(indexrelid)) AS size
FROM   pg_stat_user_indexes
WHERE  idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;

Для поиска медленных запросов ставят расширение pg_stat_statements — оно накапливает статистику по всем выполненным запросам.

-- В postgresql.conf: shared_preload_libraries = 'pg_stat_statements'
-- затем перезапуск сервера и:
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

-- Самые дорогие запросы по суммарному времени
SELECT round(total_exec_time::numeric, 1) AS total_ms,
       calls,
       round(mean_exec_time::numeric, 2) AS mean_ms,
       left(query, 80) AS query
FROM   pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
Сортируйте по суммарному времени, а не по среднему. Запрос на 5 мс, вызываемый двести тысяч раз в час, съедает сервер сильнее, чем отчёт на 8 секунд раз в сутки. Оптимизировать нужно то, что дороже в сумме.
08

Журналирование и обновление версии

Настройки журнала, которые стоит включить сразу — они ничего не стоят и однажды спасут расследование:

log_min_duration_statement = 500      # писать запросы дольше 500 мс
log_checkpoints = on
log_connections = on
log_disconnections = on
log_lock_waits = on                   # ожидания блокировок
log_temp_files = 0                    # все временные файлы: признак нехватки work_mem
log_autovacuum_min_duration = 0
log_line_prefix = '%m [%p] %u@%d '    # время, pid, пользователь, база

Обновление на новую мажорную версию в Debian выполняется штатным инструментом:

# Установить новую версию рядом со старой
sudo apt install postgresql-18

# Новая версия создаст свой кластер — его надо убрать перед переносом
sudo pg_dropcluster 18 main --stop

# Перенести кластер 17/main в версию 18
sudo pg_upgradecluster 17 main

# Проверить, что всё работает, и только потом удалить старый
pg_lsclusters
sudo pg_dropcluster 17 main
Минорные обновления (17.4 → 17.5) безопасны и требуют только перезапуска — их ставит unattended-upgrades. Мажорные (17 → 18) меняют формат данных и требуют переноса. Перед мажорным обновлением обязательны свежий pg_dumpall и проверка, что расширения имеют версии под новый выпуск. Старый кластер удаляйте не раньше, чем приложение отработает на новом сутки.

Ключевые выводы раздела

Запомните главное

  • Роль — единое понятие: с LOGIN это пользователь, без него — группа; роли общие на весь кластер
  • search_path работает как PATH; "$user" даёт неочевидные эффекты
  • Владелец объекта может всё; остальным нужны явные привилегии
  • GRANT ON ALL TABLES не действует на будущие таблицы — нужен ALTER DEFAULT PRIVILEGES
  • Приложение работает под ролью без прав на изменение структуры — это граница между «утекли данные» и «удалена база»
  • pg_dump не сохраняет роли и пароли; рядом нужен pg_dumpall --globals-only
  • Копия, из которой не восстанавливались, копией не является
  • UPDATE создаёт новую версию строки; VACUUM освобождает место, VACUUM FULL блокирует таблицу
  • Долгие открытые транзакции блокируют очистку и раздувают таблицы
  • Медленные запросы ищут по суммарному времени, а не по среднему
Практические задания — на учебном портале. Задания, критерии оценивания, сдача работ и оценки преподавателя — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Раздел 5: Установка Раздел 7: DDL