Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Питання на співбесіді: Експлуатація БД

Питання з реальних співбесід з відповідями: 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 на продакшн-базі - повний доступ. Безпечніше підключатися до репліки для читання і мати окрему роль лише з правами читання для розслідувань.

Докладніше в документації: 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_upgrade

Логічний бекап (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 зазвичай увімкнений за замовчуванням з вікном у кілька днів - варто знати, як ним скористатися, до аварії.

Докладніше в документації: Безперервне архівування і PITR

За асинхронної реплікації репліка застосовує зміни з запізненням - зазвичай мілісекунди, але під навантаженням, при великих транзакціях чи проблемах мережі це можуть бути секунди й хвилини.

Що бачить користувач:

  • Створив замовлення, його перенаправили на сторінку замовлення - а там «не знайдено», бо читання пішло на репліку, куди запис ще не дійшов.
  • Змінив пароль чи налаштування - а сторінка показує старе значення.
  • Воркер черги отримує завдання з ID щойно створеного запису, читає з репліки - і не знаходить його.

Як з цим жити:

  • Читати свої записи з primary. У Laravel - опція sticky => true: після запису в межах того самого запиту всі читання йдуть на primary.
  • Критичні читання - завжди з primary: перевірка балансу перед списанням, авторизація, все, що веде до запису.
  • Передавати дані, а не лише ID, або відкладати завдання черги до коміту (afterCommit) і читати їх з primary.
  • Моніторити затримку і прибирати відсталу репліку з ротації читання. У PostgreSQL - pg_stat_replication на primary та now() - pg_last_xact_replay_timestamp() на репліці.

Синхронна реплікація прибирає затримку для підтверджених транзакцій, але кожен COMMIT чекає репліку - запис повільніший, а падіння репліки може зупинити запис на primary. Тому її вмикають свідомо, для даних, втрата яких неприпустима.

Докладніше в документації: Hot Standby

Фізична (потокова) реплікація передає на репліку 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 «бо він навантажує сервер» - без нього таблиці роздуваються, а лічильник транзакцій наближається до переповнення.

Докладніше в документації: 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 мільйонів транзакцій).

Якщо заморожування не встигає:

  1. Спершу - попередження в логах про наближення межі.
  2. Близько до межі 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):

  1. Додати нову колонку, код пише в обидві.
  2. Перенести старі дані порціями.
  3. Код читає з нової.
  4. Окремим деплоєм прибрати стару колонку.

Так кожен крок сумісний і з попередньою, і з наступною версією коду, і деплой можна відкотити.

MySQL: багато змін виконуються online (ALGORITHM=INSTANT для додавання колонки з 8.0), для решти - gh-ost чи pt-online-schema-change.

Докладніше в документації: ALTER TABLE

Партиціонування ділить одну логічну таблицю на кілька фізичних частин за ключем: за діапазоном дат, за списком значень чи за хешем. Для застосунку це одна таблиця, а база сама розкладає рядки по частинах.

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 - сильний додатковий рівень захисту, але не заміна перевіркам у застосунку.

Докладніше в документації: Політики безпеки рядків