Питання на співбесіді: Експлуатація БД
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
16 питань
Найпростіший спосіб - логічний дамп:
# PostgreSQL: власний стиснений формат, зручний для вибіркового відновлення
pg_dump -Fc -d app > app.dump
pg_restore -d app_restored app.dump
# MySQL
mysqldump --single-transaction --routines app > app.sql
pg_dump бере узгоджений знімок і не блокує роботу застосунку. Для MySQL на InnoDB той самий ефект дає --single-transaction.
Бекап, який ніхто не пробував відновити, - не бекап. Мінімум:
- Регулярно відновлювати копію на окремому сервері й перевіряти, що застосунок з нею працює. Автоматично, за розкладом.
- Зберігати копії не там, де база: інший сервер, інший дата-центр, об'єктне сховище. Пожежа чи видалений акаунт не мають забрати і базу, і бекапи.
- Кілька поколінь: щоденні за тиждень, щотижневі за місяць. Помилку в даних часто помічають не одразу.
- Моніторинг: сповіщення, якщо бекап не створився чи раптом став набагато меншим.
Правило 3-2-1: три копії, на двох різних носіях, одна - поза основним майданчиком.
Обмеження дампу: відновлення - лише на момент створення копії. Усе, що сталося після, втрачено. Для відновлення на довільний момент потрібне безперервне архівування журналу (WAL, binlog).
Докладніше в документації: Резервне копіювання і відновлення
Репліка - копія бази на іншому сервері, яка безперервно отримує зміни з основного сервера (primary) і застосовує їх у себе.
Як це працює: primary записує всі зміни в журнал (WAL у PostgreSQL, binlog у MySQL), а репліка отримує цей журнал по мережі й програє його. Зазвичай реплікація асинхронна: primary не чекає підтвердження від репліки.
Навіщо:
- Відмовостійкість. Якщо primary впав, репліку можна підвищити до нового primary і продовжити роботу за хвилини, а не за години відновлення з бекапу.
- Масштабування читання. Важкі звіти, аналітику й частину запитів на читання відправляють на репліки, розвантажуючи primary.
- Обслуговування без простою. Оновлення чи перевірки можна спершу робити на репліці.
Чого репліка НЕ замінює - бекапів. DELETE FROM users без WHERE за мілісекунди реплікується на всі копії. Від логічних помилок рятує лише бекап чи відновлення на момент у часі.
Головний нюанс для розробника - затримка реплікації: щойно записані дані на репліці можуть з'явитися із запізненням. У Laravel є опція sticky: після запису в межах того самого запиту читання йде на primary.
psql - консольний клієнт PostgreSQL. Окрім SQL, у ньому є метакоманди з \, що швидко показують структуру бази.
Підключення:
psql -h localhost -U app -d app_production
psql "postgresql://app@localhost:5432/app_production"
Структура бази:
\l список баз
\c app_test підключитися до іншої бази
\dt таблиці
\d orders структура таблиці: колонки, індекси, обмеження, тригери
\di індекси
\dn схеми
\du ролі
\df функції
\dx встановлені розширення
\dt+ таблиці з розміром
Робота із запитами:
\x auto розгорнутий вивід для широких рядків
\timing on показувати час виконання кожного запиту
\e редагувати останній запит у $EDITOR
\i file.sql виконати файл
\copy orders TO 'orders.csv' CSV HEADER експорт з клієнтського боку
\watch 2 повторювати запит кожні 2 секунди
\q вийти
Корисні прийоми:
\set ON_ERROR_STOP onу скриптах - зупинитися на першій помилці замість виконання решти.BEGIN;перед ризикованими змінами на проді - подивитися результат і вирішити:COMMITчиROLLBACK.\set AUTOCOMMIT off- кожна команда автоматично в транзакції, доки не зробитиCOMMIT.~/.psqlrc- налаштування за замовчуванням (\timing,\x auto, кольоровий промпт для продакшену).~/.pgpass- паролі для підключень, щоб не вводити їх щоразу й не передавати в командному рядку.
На проді - обережно: psql на продакшн-базі - повний доступ. Безпечніше підключатися до репліки для читання і мати окрему роль лише з правами читання для розслідувань.
Поширена практика «застосунок підключається як postgres» означає, що SQL-ін'єкція чи помилка в коді може видалити будь-яку таблицю, прочитати будь-які дані, змінити налаштування сервера. Принцип найменших привілеїв: кожна роль має лише ті права, які їй потрібні.
Типова схема ролей:
-- Власник схеми: від його імені ганяють міграції
CREATE ROLE app_owner LOGIN PASSWORD '...';
CREATE SCHEMA app AUTHORIZATION app_owner;
-- Застосунок: лише робота з даними
CREATE ROLE app_user LOGIN PASSWORD '...';
GRANT USAGE ON SCHEMA app TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA app TO app_user;
GRANT USAGE ON ALL SEQUENCES IN SCHEMA app TO app_user;
-- Права на таблиці, які створять пізніше міграції
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 ON SEQUENCES TO app_user;
-- Аналітика й розслідування: лише читання
CREATE ROLE readonly LOGIN PASSWORD '...';
GRANT pg_read_all_data TO readonly; -- PostgreSQL 14+
Що це дає:
- Застосунок не може
DROP TABLE,ALTER, створювати ролі чи читати системні налаштування. - Міграції виконуються окремою роллю - її пароль не живе в змінних середовища вебсерверів.
- Аналітики й розробники на проді працюють від ролі лише для читання.
Деталі, про які забувають:
- Права за замовчуванням (
ALTER DEFAULT PRIVILEGES) - без них нова таблиця з міграції буде недоступна застосунку. - Схема
public: з PostgreSQL 15 звичайні ролі за замовчуванням не можуть створювати в ній об'єкти - на старіших версіях це право варто забрати (REVOKE CREATE ON SCHEMA public FROM PUBLIC). - Послідовності потребують окремого
USAGE, інакше вставка з автоінкрементом впаде. - Підключення обмежують ще й на рівні
pg_hba.conf: з яких адрес, до яких баз, яким методом автентифікації (scram-sha-256).
Мінорні оновлення (17.4 → 17.6) - лише виправлення помилок: досить оновити пакет і перезапустити сервер. Формат даних не змінюється.
Мажорні (16 → 17 → 18) змінюють внутрішній формат зберігання, тож файли даних старої версії нова прочитати не може. Є три способи:
1. pg_dump + відновлення - найпростіше й найнадійніше:
pg_dump -Fc -d app > app.dump
# новий сервер
pg_restore -d app app.dump
Простій - на весь час дампу й відновлення з перебудовою індексів. Для бази в кілька гігабайтів - нормально, для терабайтів - години.
2. pg_upgrade - перетворює каталог даних «на місці»:
pg_upgrade --old-datadir ... --new-datadir ... --old-bindir ... --new-bindir ... --link
З --link файли даних не копіюються, а використовуються жорсткі посилання - оновлення займає хвилини навіть для великих баз. Обов'язково спершу --check (сухий прогін). Після оновлення потрібна статистика для планувальника (vacuumdb --analyze-in-stages); з PostgreSQL 18 pg_upgrade уміє переносити її сам.
3. Логічна реплікація - майже без простою: новий сервер на новій версії підписується на зміни старого, наздоганяє його, і застосунок перемикається за секунди. Найскладніше в налаштуванні: послідовності й зміни схеми не реплікуються, їх переносять окремо.
Як підготуватися:
- Прочитати release notes усіх мажорних версій між поточною й цільовою - розділи про несумісності.
- Перевірити, що розширення (PostGIS, pgvector тощо) доступні для нової версії.
- Прогнати тести застосунку на новій версії, відрепетирувати оновлення на копії продакшн-даних, виміряти час.
- Мати свіжий бекап і план відкату.
На керованих сервісах (RDS, Cloud SQL) мажорне оновлення - кнопка, але простій і перевірки сумісності ті самі - про них варто подбати заздалегідь.
Логічний бекап (pg_dump, mysqldump) зберігає дані як SQL-команди чи логічний формат: «створити таблицю, вставити ці рядки».
- Плюси: переноситься між версіями й платформами, можна відновити одну таблицю.
- Мінуси: на великих базах створення й особливо відновлення (перебудова всіх індексів) тривають години; відновлення лише на момент дампу.
Фізичний бекап (pg_basebackup, Percona XtraBackup, знімок диска) - копія файлів даних як вони є.
- Плюси: швидке відновлення - файли просто кладуть на місце; основа для реплік.
- Мінуси: прив'язаний до мажорної версії й платформи; лише цілий кластер.
PITR (Point-In-Time Recovery) = фізичний базовий бекап + безперервний архів журналу змін (WAL у PostgreSQL, binlog у MySQL). Під час відновлення база розгортає базовий бекап і програє журнал до потрібного моменту:
# PostgreSQL: архівувати кожен заповнений сегмент WAL
archive_mode = on
archive_command = '...' # або готовий інструмент: pgBackRest, WAL-G, Barman
# під час відновлення
recovery_target_time = '2026-10-04 14:31:00'
Навіщо це на практиці: о 14:32 хтось виконав руйнівну міграцію. Дамп учорашньої ночі втратить пів дня даних, а PITR поверне базу на 14:31.
На керованих базах (RDS, Cloud SQL, DigitalOcean) PITR зазвичай увімкнений за замовчуванням з вікном у кілька днів - варто знати, як ним скористатися, до аварії.
За асинхронної реплікації репліка застосовує зміни з запізненням - зазвичай мілісекунди, але під навантаженням, при великих транзакціях чи проблемах мережі це можуть бути секунди й хвилини.
Що бачить користувач:
- Створив замовлення, його перенаправили на сторінку замовлення - а там «не знайдено», бо читання пішло на репліку, куди запис ще не дійшов.
- Змінив пароль чи налаштування - а сторінка показує старе значення.
- Воркер черги отримує завдання з ID щойно створеного запису, читає з репліки - і не знаходить його.
Як з цим жити:
- Читати свої записи з primary. У Laravel - опція
sticky => true: після запису в межах того самого запиту всі читання йдуть на primary. - Критичні читання - завжди з primary: перевірка балансу перед списанням, авторизація, все, що веде до запису.
- Передавати дані, а не лише ID, або відкладати завдання черги до коміту (
afterCommit) і читати їх з primary. - Моніторити затримку і прибирати відсталу репліку з ротації читання. У PostgreSQL -
pg_stat_replicationна primary таnow() - pg_last_xact_replay_timestamp()на репліці.
Синхронна реплікація прибирає затримку для підтверджених транзакцій, але кожен COMMIT чекає репліку - запис повільніший, а падіння репліки може зупинити запис на primary. Тому її вмикають свідомо, для даних, втрата яких неприпустима.
Фізична (потокова) реплікація передає на репліку WAL - журнал змін на рівні байтів сторінок. Репліка - точна побайтова копія всього кластера.
- Копіюється все: усі бази, таблиці, індекси, зміни схеми.
- Репліка лише для читання (hot standby) і може стати новим primary при відмові.
- Primary і репліка мусять мати ту саму мажорну версію й архітектуру.
Логічна реплікація передає зміни даних на рівні рядків - «вставлено цей рядок у таку таблицю» - за моделлю публікація/підписка:
-- на джерелі
CREATE PUBLICATION orders_pub FOR TABLE orders, order_items;
-- на отримувачі
CREATE SUBSCRIPTION orders_sub
CONNECTION 'host=primary dbname=app user=replicator'
PUBLICATION orders_pub;
- Можна реплікувати окремі таблиці (і навіть рядки й колонки - з PostgreSQL 15).
- Отримувач - звичайна база, у яку можна писати: мати власні таблиці, індекси, іншу структуру.
- Працює між різними мажорними версіями - основа оновлення з мінімальним простоєм.
Обмеження логічної реплікації:
- Зміни схеми (DDL) не реплікуються - таблиці на отримувачі створюють і змінюють окремо.
- Послідовності не реплікуються - перед перемиканням їх значення переносять вручну.
- Таблиці потребують ідентифікатора рядка (первинний ключ або
REPLICA IDENTITY) дляUPDATEіDELETE. - Конфлікти (наприклад, порушення унікальності на отримувачі) зупиняють підписку, доки їх не розв'язати.
Типові задачі:
- Фізична - відмовостійкість, масштабування читання, бекапи з репліки.
- Логічна - оновлення версії без простою, міграція на інший сервер чи в хмару, передача частини даних в аналітичне сховище, консолідація кількох баз в одну.
Обидві використовують слоти реплікації, і забутий неактивний слот утримує WAL на диску primary - це поширена причина переповнення диска.
Autovacuum запускає вакуум для таблиці, коли кількість мертвих рядків перевищить поріг:
поріг = autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor × кількість рядків
= 50 + 0.2 × кількість рядків
Тобто вакуум починається, коли змінено 20% таблиці. Для таблиці на 100 мільйонів рядків - це 20 мільйонів мертвих версій до першого вакууму: таблиця роздута, запити повільні.
Налаштування для конкретних таблиць:
ALTER TABLE orders SET (
autovacuum_vacuum_scale_factor = 0.01, -- 1% замість 20%
autovacuum_analyze_scale_factor = 0.005
);
-- таблиця-черга з постійними вставками й видаленнями
ALTER TABLE jobs SET (
autovacuum_vacuum_scale_factor = 0,
autovacuum_vacuum_threshold = 1000 -- кожні 1000 мертвих рядків
);
Швидкість роботи вакууму. Autovacuum навмисно сповільнений, щоб не заважати запитам (autovacuum_vacuum_cost_limit, autovacuum_vacuum_cost_delay). На сучасних SSD значення за замовчуванням часто занадто обережні - вакуум не встигає за змінами. Типове рішення - збільшити autovacuum_vacuum_cost_limit (глобально чи для таблиці).
Кількість воркерів: autovacuum_max_workers (за замовчуванням 3). Якщо великих таблиць багато, воркери зайняті на одній, а інші чекають.
Як зрозуміти, що вакуум не встигає:
SELECT relname, n_live_tup, n_dead_tup,
round(100.0 * n_dead_tup / nullif(n_live_tup + n_dead_tup, 0), 1) AS dead_percent,
last_autovacuum
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 10;
Високий відсоток мертвих рядків і давній last_autovacuum - сигнал.
Якщо вакуум працює, а мертві рядки не зникають - його блокує довга транзакція, забутий слот реплікації чи prepared transaction. Налаштування тут не допоможуть: потрібно прибрати те, що утримує горизонт.
Не вимикати autovacuum «бо він навантажує сервер» - без нього таблиці роздуваються, а лічильник транзакцій наближається до переповнення.
З'єднання:
- кількість активних і простоюючих з'єднань щодо
max_connections; - сесії в стані
idle in transactionі їхня тривалість; - очікування блокувань (
wait_event_type = 'Lock').
Запити:
- найдорожчі запити за сумарним і середнім часом (
pg_stat_statements); - кількість повільних запитів (
log_min_duration_statement); - найдовший запит і найстаріша відкрита транзакція.
Кеш і ввід-вивід:
- коефіцієнт попадання в кеш (
blks_hit/blks_readуpg_stat_database); - тимчасові файли (
temp_files,temp_bytes) - сортування й хеші на диску через нестачуwork_mem; - навантаження на диск на рівні ОС.
Обслуговування:
- мертві рядки й час останнього вакууму для великих таблиць (
pg_stat_user_tables); - вік найстарішої транзакції -
age(datfrozenxid)- захист від переповнення лічильника; - розмір таблиць і індексів, темп їх зростання.
Реплікація:
- затримка реплік у байтах і секундах (
pg_stat_replicationна primary); - неактивні слоти реплікації й обсяг WAL, який вони утримують (
pg_replication_slots).
Ресурси сервера: CPU, пам'ять, вільне місце на диску для даних і для WAL, кількість контрольних точок (checkpoints_req проти checkpoints_timed - часті вимушені контрольні точки означають замалий max_wal_size).
Інструменти: postgres_exporter + Prometheus + Grafana, pganalyze, Datadog, моніторинг керованих сервісів (RDS Performance Insights).
Сповіщення варто налаштувати насамперед на те, що веде до аварії: місце на диску, кількість з'єднань близько до ліміту, затримка реплікації, вік транзакцій, неактивні слоти реплікації. Решта метрик - для розслідування й планування.
Кожна транзакція, що змінює дані, отримує ідентифікатор (XID) - 32-бітне число. Версія рядка позначена XID транзакції, що її створила, і PostgreSQL порівнює XID, щоб вирішити, які версії видимі якій транзакції.
Проблема: 32 біти - це близько 4 мільярдів значень, і лічильник «закручується» по колу. Порівняння працює в межах ~2 мільярдів транзакцій у минуле. Якби рядок, створений дуже давно, «пережив» цю межу, він раптом став би виглядати як створений у майбутньому - і зник би для всіх запитів.
Як PostgreSQL це запобігає - «заморожування». VACUUM позначає старі версії рядків як заморожені: «видимі всім, XID більше не має значення». Для цього autovacuum періодично запускає агресивний вакуум таблиці, навіть якщо мертвих рядків немає (autovacuum_freeze_max_age, за замовчуванням 200 мільйонів транзакцій).
Якщо заморожування не встигає:
- Спершу - попередження в логах про наближення межі.
- Близько до межі PostgreSQL перестає приймати транзакції, що змінюють дані, щоб не пошкодити дані. База доступна лише для читання, доки вакуум не виконано, - це повноцінна аварія.
Чому вакуум може не встигати:
- Довгі транзакції й забуті слоти реплікації блокують заморожування.
- Autovacuum вимкнений чи занадто повільний для навантаження.
- Дуже велика таблиця, агресивний вакуум якої триває годинами й постійно переривається.
Моніторинг:
SELECT datname, age(datfrozenxid) AS xid_age
FROM pg_database
ORDER BY xid_age DESC;
Сповіщення варто ставити задовго до 2 мільярдів - наприклад, на 500 мільйонів-1 мільярд.
Хороша новина: нові версії PostgreSQL значно покращили заморожування (visibility map з позначкою «усе заморожено», агресивніший вакуум), тож на налаштованому сервері з працюючим autovacuum це рідкість. Проблема з'являється саме тоді, коли щось систематично заважає вакууму.
Докладніше в документації: Запобігання переповненню ID транзакцій
Головна небезпека не в тривалості самої зміни, а в блокуваннях. ALTER TABLE у PostgreSQL бере ACCESS EXCLUSIVE-блокування. Якщо в цей момент іде довгий запит, ALTER стає в чергу - і всі нові запити до таблиці стають у чергу за ним. Сайт лягає, хоча сама операція займає мілісекунди.
Правила, що рятують:
SET lock_timeout = '3s'; -- краще впасти й повторити, ніж повісити таблицю
SET statement_timeout = '60s';
ALTER TABLE orders ADD COLUMN note text;
Що дешево, а що ні (PostgreSQL):
- Додати колонку без значення за замовчуванням чи з незмінним default (PostgreSQL 11+) - миттєво, лише метадані.
- Додати
NOT NULLдо наявної колонки - повна перевірка таблиці під блокуванням. Безпечніше:CHECK (col IS NOT NULL) NOT VALID, потімVALIDATE CONSTRAINT(без блокування запису), потімSET NOT NULL(PostgreSQL 12+ використає перевірене обмеження). - Зовнішній ключ - так само:
NOT VALID, потімVALIDATE. - Змінити тип колонки - часто переписування всієї таблиці. Краще нова колонка, поступове заповнення, перемикання.
- Індекс - лише
CONCURRENTLY.
Перейменування й видалення - через «розширити, потім звузити» (expand/contract):
- Додати нову колонку, код пише в обидві.
- Перенести старі дані порціями.
- Код читає з нової.
- Окремим деплоєм прибрати стару колонку.
Так кожен крок сумісний і з попередньою, і з наступною версією коду, і деплой можна відкотити.
MySQL: багато змін виконуються online (ALGORITHM=INSTANT для додавання колонки з 8.0), для решти - gh-ost чи pt-online-schema-change.
Партиціонування ділить одну логічну таблицю на кілька фізичних частин за ключем: за діапазоном дат, за списком значень чи за хешем. Для застосунку це одна таблиця, а база сама розкладає рядки по частинах.
CREATE TABLE events (
id bigint GENERATED ALWAYS AS IDENTITY,
created_at timestamptz NOT NULL,
payload jsonb
) PARTITION BY RANGE (created_at);
CREATE TABLE events_2026_10 PARTITION OF events
FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');
Коли це виправдано:
- Дані «старіють»: логи, події, метрики, де запити дивляться на останні тижні, а старе треба видаляти. Видалити місяць -
DROP TABLE events_2025_10(мить), а неDELETEмільйонів рядків з навантаженням на вакуум. - Запити майже завжди фільтрують за ключем партиціонування - тоді спрацьовує partition pruning: база читає лише потрібні частини.
- Таблиця настільки велика, що індекси не вміщаються в пам'ять, а обслуговування (вакуум, перебудова індексу) окремих частин значно легше.
Коли НЕ варто: таблиця на кілька мільйонів рядків, з якою добре справляються індекси. Партиціонування - не заміна індексам.
Обмеження й пастки:
- Запит без ключа партиціонування в умові читає всі частини - часто повільніше, ніж одна таблиця.
- Первинний ключ і унікальні обмеження мусять містити ключ партиціонування:
PRIMARY KEY (id, created_at). - Частини треба створювати заздалегідь (cron чи розширення
pg_partman), інакше вставка в майбутній місяць упаде - або потрапить уDEFAULT-партицію.
Партиціонування ≠ шардинг: частини живуть на одному сервері. Розподіл по серверах - окреме, набагато складніше рішення.
Failover - підвищення репліки до нового primary, коли старий недоступний. Сам PostgreSQL уміє лише виконати підвищення за командою (pg_promote()); вирішувати, коли це робити, має зовнішній інструмент.
Інструменти: Patroni (найпоширеніший), pg_auto_failover, repmgr, Stolon; у хмарі - керовані сервіси (RDS Multi-AZ, Cloud SQL HA), які роблять це самі.
Як працює Patroni:
- Кожен вузол PostgreSQL має агента Patroni.
- Хто зараз лідер, записано в розподіленому сховищі консенсусу (etcd, Consul, ZooKeeper) з обмеженим терміном оренди.
- Лідер постійно поновлює оренду. Якщо він зник і оренда спливла, агенти реплік обирають нового лідера - зазвичай найменш відсталу репліку, - і та підвищується.
- Застосунок підключається через точку доступу, що завжди веде до поточного лідера: HAProxy з перевіркою стану, віртуальний IP чи DNS.
Split-brain - два вузли одночасно вважають себе primary і обидва приймають записи. Дані розходяться, і злити їх автоматично неможливо. Типовий сценарій: мережа між старим primary і рештою розірвалася, решта обрала нового лідера, а старий продовжує працювати для частини клієнтів.
Як захищаються:
- Консенсус і кворум: лідером може бути лише той, хто тримає оренду в сховищі консенсусу; вузол, що втратив зв'язок з більшістю, сам знижується до репліки.
- Fencing («огорожа»): гарантовано зупинити старий primary (вимкнути, відрізати від мережі, watchdog), перш ніж підвищувати новий.
- Синхронна реплікація - щоб при перемиканні не втратити підтверджені транзакції.
Що ще враховувати:
- Асинхронна реплікація = можлива втрата останніх транзакцій при failover. Скільки - визначає затримка реплікації.
- Застосунок має переживати перемикання: повторні підключення, повтори транзакцій, короткі таймаути з'єднань.
- Регулярні навчання: failover, який ніколи не перевіряли, у критичний момент зазвичай не спрацьовує.
Row-Level Security (RLS) - політики на рівні бази, що визначають, які рядки таблиці бачить і може змінювати роль. Фільтр додається до кожного запиту автоматично - навіть якщо в коді його забули.
Ізоляція тенантів:
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
ALTER TABLE projects FORCE ROW LEVEL SECURITY; -- діє й на власника таблиці
CREATE POLICY tenant_isolation ON projects
USING (tenant_id = current_setting('app.tenant_id')::bigint)
WITH CHECK (tenant_id = current_setting('app.tenant_id')::bigint);
USING- які рядки видно (SELECT,UPDATE,DELETE).WITH CHECK- які рядки дозволено записати (INSERT,UPDATE): не можна вставити рядок з чужимtenant_id.
Застосунок на початку кожного запиту чи транзакції встановлює тенанта:
BEGIN;
SET LOCAL app.tenant_id = '42';
SELECT * FROM projects; -- лише проєкти тенанта 42, без WHERE у коді
COMMIT;
Чому це цінно: у мультитенантному застосунку найнебезпечніший баг - запит без фільтра за тенантом. З RLS такий запит поверне не чужі дані, а лише дані поточного тенанта (або нічого, якщо тенант не встановлений).
Підводні камені:
- Суперкористувачі й ролі з
BYPASSRLSполітики ігнорують, а власник таблиці - теж, безFORCE ROW LEVEL SECURITY. Застосунок має працювати від звичайної ролі. - Пул з'єднань: з PgBouncer у режимі transaction pooling - лише
SET LOCALусередині транзакції.SETбезLOCALзалишить тенанта в з'єднанні, яке потім отримає інший запит - найгірший можливий сценарій. - Продуктивність: умова політики додається до кожного запиту - потрібен індекс на
tenant_id, а функції в політиці мають бути простими. - Фонові задачі й адмінка мають явно працювати або в контексті тенанта, або від окремої ролі з обґрунтованим обходом.
- Налагодження складніше: «чому запит нічого не повертає» часто означає, що не встановлено змінну тенанта.
RLS - сильний додатковий рівень захисту, але не заміна перевіркам у застосунку.