PostgreSQL: питання на співбесіді рівня Senior
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
39 питань
Частковий індекс (PostgreSQL) індексує лише рядки, що відповідають умові WHERE. Він менший, швидше оновлюється і краще тримається в пам'яті.
1. Запити завжди дивляться на малу частину таблиці:
-- 99% замовлень завершені, але черга обробки дивиться лише на pending
CREATE INDEX orders_pending_idx ON orders (created_at) WHERE status = 'pending';
Індекс займає частку від повного, і запит WHERE status = 'pending' ORDER BY created_at бере з нього кілька рядків.
2. Унікальність лише серед частини рядків - найкорисніше застосування:
-- Email унікальний лише серед не видалених (soft delete)
CREATE UNIQUE INDEX users_email_active_uniq ON users (email) WHERE deleted_at IS NULL;
-- Лише одна активна підписка на користувача
CREATE UNIQUE INDEX subscriptions_one_active ON subscriptions (user_id) WHERE active;
Без часткового індексу видалений користувач «займав» би email назавжди, і зареєструватися знову було б неможливо.
Обмеження:
- Планувальник використає індекс, лише якщо з умови запиту можна вивести умову індексу.
WHERE status = 'pending'- так;WHERE status = $1з параметром, невідомим на етапі планування, - не завжди. - MySQL часткових індексів не має. Альтернативи - згенерована колонка, яка
NULLдля «неактивних» рядків (унікальний індекс пропускаєNULL), або функціональний індекс.
Звичайний CREATE INDEX у PostgreSQL бере блокування, яке забороняє запис у таблицю на весь час побудови. На таблиці в сотні мільйонів рядків це хвилини чи години простою для INSERT/UPDATE.
CREATE INDEX CONCURRENTLY будує індекс без блокування запису:
CREATE INDEX CONCURRENTLY orders_created_at_idx ON orders (created_at);
Ціна й підводні камені:
- Працює довше: два проходи по таблиці й очікування завершення всіх транзакцій, що вже йшли.
- Не можна в транзакції. У Laravel-міграції для цього потрібно вимкнути транзакцію міграції (
public $withinTransaction = false;). - Якщо побудова впала (наприклад, порушено унікальність), лишається індекс у стані
INVALID. Він не використовується для читання, але сповільнює запис. Його треба знайти (pg_index.indisvalid = false), видалитиDROP INDEX CONCURRENTLYі створити знову. - Довга транзакція десь у системі змушує побудову чекати - перед операцією варто перевірити
pg_stat_activity.
MySQL (InnoDB) з версії 5.6 будує більшість індексів online (ALGORITHM=INPLACE, LOCK=NONE), але на початку й наприкінці все одно потрібне коротке метаданих-блокування. Для великих таблиць часто використовують gh-ost чи pt-online-schema-change.
Загальне правило: зміни схеми великих таблиць ганяють окремо від деплою коду, з lock_timeout, щоб операція краще впала, ніж повісила всю базу в черзі за блокуванням.
MVCC (Multi-Version Concurrency Control) - спосіб дати паралельним транзакціям узгоджений знімок даних без блокувань на читання. Читачі не блокують письменників, письменники - читачів.
Як це влаштовано в PostgreSQL: UPDATE не змінює рядок на місці, а створює нову версію рядка, позначаючи стару як застарілу (з якої транзакції вона вже невидима). DELETE лише позначає рядок. Кожна транзакція бачить ті версії, що були актуальні на момент її знімка.
Звідси потреба у VACUUM: старі версії («мертві кортежі») лишаються в таблиці, доки жодна транзакція вже не може їх бачити. VACUUM:
- звільняє місце мертвих кортежів для повторного використання;
- оновлює visibility map (потрібна для index-only scan);
- «заморожує» старі ідентифікатори транзакцій, щоб уникнути переповнення лічильника транзакцій (transaction ID wraparound) - аварійної ситуації, за якої база перестає приймати запис.
Зазвичай усе це робить фоновий autovacuum.
Типові проблеми на проді:
- Роздування (bloat) таблиць з частими
UPDATE: autovacuum не встигає, таблиця й індекси ростуть, запити сповільнюються. Лікують тонким налаштуванням autovacuum для конкретних таблиць. - Довга транзакція (чи забута відкрита сесія
idle in transaction) не дає вакууму прибирати навіть старі версії по всій базі. VACUUM FULLповертає місце ОС, але переписує таблицю під ексклюзивним блокуванням - на проді його уникають.
InnoDB теж використовує MVCC, але зберігає старі версії в undo log, тож окремого вакууму не потребує (його роль виконує фоновий purge).
Advisory-блокування (PostgreSQL) - блокування за довільним числом, яке не прив'язане до жодного рядка чи таблиці. База лише гарантує, що один ключ одночасно тримає лише одна сесія; що цей ключ означає - вирішує застосунок.
-- Чекати, доки звільниться
SELECT pg_advisory_lock(42);
-- ... робота ...
SELECT pg_advisory_unlock(42);
-- Не чекати: true, якщо взяли, false - якщо зайнято
SELECT pg_try_advisory_lock(hashtext('import:prices'));
-- Знімається автоматично в кінці транзакції
SELECT pg_advisory_xact_lock(42);
Коли це доречно:
- Захистити дію, а не рядок: «лише один процес імпорту одночасно», «лише один сервер виконує міграції під час деплою».
- Блокування того, чого ще немає: не можна взяти
FOR UPDATEна рядок, якого ще не створено, а advisory-ключ від, наприклад, email - можна. - Без зайвої інфраструктури: коли Redis для розподілених блокувань немає, а PostgreSQL уже є.
Пастки:
- Сесійні блокування живуть, поки живе з'єднання. Якщо процес «забув» зняти блокування, але з'єднання лишилося в пулі, ключ буде зайнятий безстроково. Транзакційний варіант (
_xact_) безпечніший. - З PgBouncer у режимі transaction pooling сесійні блокування ламаються: наступний запит може піти іншим з'єднанням.
- Ключ - число; рядкові ідентифікатори перетворюють хешем, і про можливі колізії варто пам'ятати.
MySQL має схожі GET_LOCK('name', timeout) / RELEASE_LOCK('name') з рядковими іменами.
Повільний запит - не лише той, що виконується 5 секунд один раз. Часто більше шкодить запит на 20 мс, який виконується мільйон разів на день. Тому дивляться на сумарний час.
PostgreSQL - pg_stat_statements. Розширення збирає статистику по кожному нормалізованому запиту (параметри замінено на $1): кількість викликів, сумарний і середній час, прочитані блоки.
-- shared_preload_libraries = 'pg_stat_statements' у postgresql.conf, потім:
CREATE EXTENSION pg_stat_statements;
SELECT calls, round(total_exec_time) AS total_ms, round(mean_exec_time, 1) AS mean_ms, query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
Доповнення:
log_min_duration_statement = 500- записувати в лог усі запити довші за 500 мс, з параметрами.auto_explain- автоматично логувати плани повільних запитів. Без цього план на проді, де дані й статистика інші, ніж локально, часто не відтворити.pg_stat_activity- що виконується просто зараз, хто кого чекає.
MySQL: slow query log (long_query_time), performance_schema і sys.statement_analysis - аналог pg_stat_statements.
На рівні застосунку APM (Sentry Performance, New Relic, Telescope локально) пов'язує запит з місцем у коді, яке його породило. Часто проблема - не сам запит, а N+1: сто швидких запитів там, де мав бути один.
Порядок дій: знайти топ за сумарним часом → EXPLAIN (ANALYZE, BUFFERS) на реальних параметрах → індекс, переписаний запит чи кеш → порівняти статистику після змін.
Кожне з'єднання з PostgreSQL - це окремий процес сервера з власною пам'яттю. Сотні з'єднань споживають багато ресурсів, а встановлення нового з'єднання помітно повільне. Тим часом PHP-FPM з 200 воркерами на кількох серверах легко відкриває тисячу з'єднань, більшість з яких простоює.
PgBouncer стоїть між застосунком і базою: тисячі клієнтських з'єднань він обслуговує невеликим пулом серверних.
Режими:
- Session pooling - серверне з'єднання видається клієнту на всю сесію. Безпечно, але виграш малий.
- Transaction pooling - з'єднання видається лише на час транзакції (або окремого запиту поза транзакцією). Головна економія - і головні сюрпризи.
- Statement pooling - на окремий запит; транзакції з кількох запитів неможливі.
Що ламається в transaction pooling: усе, що прив'язане до сесії, бо наступний запит може піти іншим серверним з'єднанням:
SET(часовий пояс,search_path,statement_timeout) безLOCAL- налаштування «перетече» до іншого клієнта.- Сесійні advisory-блокування,
LISTEN/NOTIFY, тимчасові таблиці. - Підготовлені запити (prepared statements) - PgBouncer підтримує їх на рівні протоколу лише з версії 1.21 і з налаштуванням
max_prepared_statements. До того PDO з емуляцією вимкненою отримував помилки на кшталтprepared statement does not exist.
Альтернативи й доповнення: керовані пули (RDS Proxy, Supavisor у Supabase), довгоживучі процеси (Octane), що тримають сталі з'єднання, і просто обмеження кількості PHP-воркерів під реальну ємність бази.
Головна небезпека не в тривалості самої зміни, а в блокуваннях. 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-партицію.
Партиціонування ≠ шардинг: частини живуть на одному сервері. Розподіл по серверах - окреме, набагато складніше рішення.
BRIN (Block Range INdex) зберігає не кожне значення, а підсумок для діапазону сторінок таблиці: мінімум і максимум значення колонки в кожних, скажімо, 128 сторінках.
Запит WHERE created_at >= '2026-10-01' перевіряє підсумки й читає лише ті діапазони сторінок, де значення можуть бути, - решту пропускає.
CREATE INDEX events_created_brin ON events USING brin (created_at);
CREATE INDEX events_created_brin ON events USING brin (created_at) WITH (pages_per_range = 32);
Головна перевага - розмір. На таблиці в сотні гігабайтів B-tree займає десятки гігабайтів, а BRIN - мегабайти. Він майже не впливає на швидкість вставки.
Коли BRIN працює: значення корелюють з фізичним розташуванням рядків на диску. Ідеальний випадок - журнали, події, метрики, де рядки лише дописуються в кінець, а час вставки зростає. Тоді кожен діапазон сторінок містить вузький проміжок часу.
Коли не працює:
- Значення розкидані по таблиці випадково (наприклад,
user_idу журналі подій) - кожен діапазон містить майже всі значення, і BRIN нічого не відсікає. - Часті
UPDATEчи видалення з подальшими вставками на звільнене місце руйнують кореляцію. - Потрібен точковий пошук одного рядка - BRIN поверне діапазони сторінок, які доведеться перечитати.
Як перевірити придатність: correlation у pg_stats для колонки - близько до 1 чи -1 означає, що BRIN підійде.
Обслуговування: нові сторінки підсумовуються вакуумом або автоматично (autosummarize = on), або вручну brin_summarize_new_values(). Поки діапазон не підсумовано, його доводиться читати завжди.
Типова архітектура: BRIN за часом на великій таблиці подій + партиціонування за місяцями + B-tree лише на тих колонках, за якими справді шукають окремі записи.
Через MVCC UPDATE у PostgreSQL створює нову версію рядка. Звичайно нова версія лягає в інше місце таблиці, і кожен індекс таблиці отримує новий запис, що на неї вказує, - навіть індекси на колонках, які не змінювалися.
HOT (Heap-Only Tuple) - оптимізація, коли оновлення не чіпає індекси взагалі. Нова версія кладеться на ту саму сторінку, а стара посилається на неї ланцюжком. Індекси продовжують вказувати на стару позицію й знаходять нову через ланцюжок.
Умови HOT:
- Жодна індексована колонка не змінилася.
- На тій самій сторінці є вільне місце для нової версії.
Чому зайвий індекс шкодить UPDATE: індекс на колонці updated_at чи views_count, яку змінює майже кожне оновлення, робить HOT неможливим для всіх таких оновлень. Кожен UPDATE ... SET views_count = views_count + 1 тепер пише в усі індекси таблиці й більше роздуває їх.
Як збільшити частку HOT:
- Не індексувати «гарячі» колонки, які часто змінюються, без явної потреби.
fillfactorменше 100 - залишати на сторінках вільне місце для нових версій:
ALTER TABLE counters SET (fillfactor = 80);
(діє для нових сторінок; для наявних - після VACUUM FULL чи перебудови).
- Винести часто змінювані лічильники в окрему вузьку таблицю.
Як виміряти:
SELECT relname, n_tup_upd, n_tup_hot_upd,
round(100.0 * n_tup_hot_upd / nullif(n_tup_upd, 0), 1) AS hot_percent
FROM pg_stat_user_tables
ORDER BY n_tup_upd DESC;
Низький відсоток HOT на таблиці з багатьма оновленнями - сигнал переглянути індекси й fillfactor.
Кожен індекс уповільнює запис, займає пам'ять і диск, подовжує бекапи й вакуум. З роками в базі накопичуються індекси, створені «про всяк випадок» чи для запитів, яких уже немає.
1. Невикористані:
SELECT s.relname AS table, s.indexrelname AS index, s.idx_scan,
pg_size_pretty(pg_relation_size(s.indexrelid)) AS size
FROM pg_stat_user_indexes s
JOIN pg_index i ON i.indexrelid = s.indexrelid
WHERE s.idx_scan = 0
AND NOT i.indisunique -- унікальні тримають цілісність
ORDER BY pg_relation_size(s.indexrelid) DESC;
2. Дублікати - однакові колонки в тому самому порядку:
SELECT indrelid::regclass AS table, array_agg(indexrelid::regclass) AS indexes
FROM pg_index
GROUP BY indrelid, indkey, indexprs::text, indpred::text
HAVING count(*) > 1;
3. Надлишкові за лівим префіксом. Індекс (user_id) зайвий, якщо є (user_id, created_at): другий обслуговує ті самі запити. Виняток - коли перший унікальний чи значно менший і використовується для гарячого запиту.
Перш ніж видаляти:
- Переконатися, що статистика збиралася достатньо довго (щомісячні звіти, сезонні задачі).
- Перевірити всі репліки - статистика окрема на кожному сервері.
- Перевірити, чи індекс не підтримує зовнішній ключ (каскадне видалення без нього стане повним переглядом таблиці).
- Видаляти через
DROP INDEX CONCURRENTLYі мати скрипт для швидкого відновлення.
Інструменти: запити з PostgreSQL Wiki, pgstattuple, звіти в pganalyze, Percona Monitoring. Аналіз варто повторювати регулярно, а не раз на кілька років після проблем зі швидкістю.
Планувальник оцінює кількість рядків для умов окремо по кожній колонці й припускає, що колонки незалежні: вибірковість A AND B = вибірковість A × вибірковість B.
Проблема - корельовані колонки:
SELECT * FROM addresses WHERE city = 'Львів' AND region = 'Львівська';
Якщо city = 'Львів' дає 2% рядків, а region = 'Львівська' - 5%, планувальник очікує 0.1%. Насправді ж майже всі Львови - у Львівській області, і результат близький до 2% - у 20 разів більше. З такою оцінкою він обирає Nested Loop там, де потрібен Hash Join, і запит сповільнюється на порядки.
Розширена статистика (PostgreSQL 10+) дозволяє зібрати статистику по комбінації колонок:
CREATE STATISTICS addresses_city_region (dependencies, ndistinct, mcv)
ON city, region FROM addresses;
ANALYZE addresses;
Види:
dependencies- функціональні залежності (місто визначає область).ndistinct- кількість унікальних комбінацій значень; важливо для оцінкиGROUP BY city, region.mcv(PostgreSQL 12+) - найчастіші комбінації значень з частотами; найточніша для конкретних пар значень.
З PostgreSQL 14 статистику можна збирати й для виразів: CREATE STATISTICS ... ON (lower(email)) FROM users.
Як зрозуміти, що це потрібно: EXPLAIN ANALYZE показує оцінку rows, що в десятки разів відрізняється від actual rows, на умові з кількома колонками однієї таблиці, а статистика окремих колонок свіжа.
Обмеження: допомагає лише для колонок однієї таблиці і лише для умов рівності й групування. Статистика займає місце і збирається під час ANALYZE, тож створюють її точково - для проблемних запитів.
Навіть транзакція, що лише читає і не тримає конфліктних блокувань, впливає на всю базу через MVCC.
Горизонт вакууму. VACUUM може прибрати мертву версію рядка, лише якщо вона невидима жодній активній транзакції. Найстаріша відкрита транзакція (чи знімок) встановлює горизонт: усі версії, що з'явилися після її початку, захищені від очищення - у всіх таблицях бази, не лише тих, що вона читала.
Що відбувається, поки транзакція висить годинами:
- Мертві версії накопичуються в таблицях з частими оновленнями (черги, лічильники, сесії).
- Таблиці й індекси роздуваються, запити сповільнюються - бо читають сторінки, повні мертвих рядків.
- Autovacuum працює, але марно: «не може видалити, бо ще потрібні».
- Після закриття транзакції роздуття саме не зникає - місце повторно використовується, але файли лишаються великими.
Джерела довгих транзакцій:
idle in transaction- застосунок відкрив транзакцію й робить щось повільне поза базою (HTTP-запит, обробка файлу) або впав, не закривши її.- Довгі звіти й аналітика на primary.
- Репліка з
hot_standby_feedback = on, де йде довгий запит, - вона теж тримає горизонт на primary. - Забуті prepared transactions і неактивні слоти реплікації (утримують WAL і горизонт).
Як знайти:
SELECT pid, state, now() - xact_start AS duration, query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start
LIMIT 10;
Захист:
idle_in_transaction_session_timeout- розривати сесії, що зависли в транзакції.transaction_timeout(PostgreSQL 17+) - обмеження на тривалість усієї транзакції.- Аналітику - на репліку без
hot_standby_feedbackчи в окреме сховище. - У коді - не робити повільних зовнішніх дій усередині
DB::transaction().
Докладніше в документації: Налаштування клієнтських з'єднань
За замовчуванням PostgreSQL чекає безкінечно: запит може виконуватися годинами, а очікування блокування - тривати вічно. Таймаути перетворюють «завис» на зрозумілу помилку.
statement_timeout- максимальний час виконання однієї команди. Після нього запит скасовується з помилкою.lock_timeout- максимальний час очікування блокування. Сам запит може йти довго, але чекати на чужі блокування - не довше вказаного.idle_in_transaction_session_timeout- розірвати сесію, що простоює всередині відкритої транзакції.transaction_timeout(PostgreSQL 17+) - ліміт на всю транзакцію.
Де налаштовувати - на різних рівнях:
-- для ролі застосунку
ALTER ROLE app_user SET statement_timeout = '30s';
ALTER ROLE app_user SET idle_in_transaction_session_timeout = '60s';
-- для окремої сесії чи транзакції
SET lock_timeout = '3s'; -- для сесії
SET LOCAL statement_timeout = '5min'; -- лише до кінця поточної транзакції
Типові значення:
- Веб-запити:
statement_timeoutкілька секунд - веб-запит, що чекає базу хвилину, однаково нікому не потрібен, а тримає з'єднання з пулу. - Міграції:
lock_timeout2-5 секунд, щобALTER TABLEне вишикував чергу з усіх запитів;statement_timeout- побільше або вимкнений для довгих операцій на зразокCREATE INDEX CONCURRENTLY. - Звіти й фонові задачі: окрема роль чи
SET LOCALз більшими лімітами.
Застереження:
- Глобальний
statement_timeoutуpostgresql.confзачепить і адміністративні операції -pg_dump, вакуум вручну. Краще налаштовувати на роль застосунку. - З PgBouncer у режимі transaction pooling
SETбезLOCAL«протікає» в інші сесії - використовуватиSET LOCALабо налаштування ролі. - Помилку таймауту застосунок має обробляти: логувати з текстом запиту й показувати користувачу зрозуміле повідомлення, а не 500.
PostgreSQL можна використати як надійну чергу без окремого брокера - так працюють драйвер database у Laravel, good_job у Rails, pg-boss, River.
Основа - FOR UPDATE SKIP LOCKED:
BEGIN;
SELECT id, payload
FROM jobs
WHERE queue = 'default' AND available_at <= now()
ORDER BY id
LIMIT 1
FOR UPDATE SKIP LOCKED;
-- обробили завдання
DELETE FROM jobs WHERE id = $1;
COMMIT;
Кілька воркерів одночасно беруть різні рядки: заблокований іншим воркером рядок просто пропускається. Якщо воркер впав, транзакція відкотиться, блокування зніметься - завдання повернеться в чергу автоматично.
LISTEN / NOTIFY - щоб воркери не опитували таблицю постійно:
-- воркер
LISTEN new_job;
-- при додаванні завдання (можна тригером)
NOTIFY new_job;
Повідомлення доставляються після коміту транзакції-відправника, тож воркер не прокинеться раніше, ніж завдання стане видимим.
Переваги: транзакційність - завдання створюється в тій самій транзакції, що й дані (немає завдань для незбережених замовлень і загублених завдань), одна система замість двох, звичні бекапи й моніторинг.
Межі:
- Навантаження: тисячі завдань на секунду - уже відчутно для бази: кожне завдання - вставка, оновлення, видалення, мертві версії й вакуум. Таблиця черги роздувається й потребує агресивного autovacuum.
NOTIFYне надійний як черга: повідомлення не зберігаються, якщо слухача немає; розмір payload обмежений (8000 байтів). Це лише «будильник», джерело правди - таблиця.- Конкуренція з основним навантаженням бази за ресурси.
Коли брати Redis/RabbitMQ/SQS: високий потік завдань, потреба в pub/sub, затримках і пріоритетах без навантаження на основну базу. Для типового застосунку з десятками-сотнями завдань на хвилину черга в PostgreSQL - цілком розумний вибір.
Питання рівня Senior з реальних технічних співбесід - 39 питань у 9 темах, розібраних із відповідями. Нижче - теми цього рівня та сусідні рівні, якщо хочете звузити або розширити підготовку.
Готуєтесь до співбесіди не просто так: зараз на сайті 47 відкритих вакансій рівня Senior. Переглянути вакансії