PostgreSQL проти MongoDB: гнучка схема чи передбачувані операції — що приносить менше проблем згодом

Було корисно?

Перші місяці — легкі. Ваш додаток запущено, користувачі клікають, дані десь зʼявляються, і всім здається, що справа йде добре.
Потім починається веселощі: дашборд гальмує, інцидент дзвонить о 02:17, а «тимчасова» модель даних стає зобовʼязанням.

PostgreSQL і MongoDB обидва можуть працювати в серйозному продакшні. Але вони ламаються по-різному і вимагають різних видів дисципліни.
Питання не в тому «який кращий», а в тому «який завдає менше болю пізніше для звичок, обмежень і толерантності до ризику саме вашої команди».

Теза: гнучкість не буває безкоштовною

Позиція MongoDB — документна модель, гнучка схема, висока швидкість розробки — може бути правдою. Позиція PostgreSQL — реляційна модель,
сильна узгодженість, зріла операційна екосистема — теж може бути правдою. Пастка в тому, щоб думати, що переваги нічого не коштують.

У продакшні ваш «вибір бази даних» здебільшого стає «вибором операційної поведінки». Postgres зазвичай винагороджує структуру:
явну схему, обмеження, транзакції та нудні повторювані операції. MongoDB зазвичай винагороджує дисципліну, яку маєте забезпечити ви:
послідовність у формі документів, обережну індексацію та сувору операційну гігієну навколо replica set і write concern.

Якщо у вас команда, що любить формальні межі — міграції, обмеження, суворі типи — Postgres пізніше болітиме менше.
Якщо у вас команда, яка може підтримувати консистентну структуру документів без примусу бази даних і ви дійсно
отримуєте вигоду від вкладених документів, MongoDB може боліти менше. Якщо ви оберете MongoDB, бо не хочете думати про схему,
ви обовʼязково будете думати про схему пізніше — але під час інцидентів.

Цитата для нотатки: Надія — це не стратегія. — генерал Гордон Р. Салліван.
Бази даних перетворюють надію на дзвінок о третій ночі.

Цікаві факти й контекст (чому налаштування за замовчуванням такі, як є)

  • PostgreSQL починався як POSTGRES в UC Berkeley у 1980-х, з фокусом на розширюваність; ця ДНК пояснює кастомні типи, розширення та підхід «інструментарію».
  • MongoDB зʼявився в кінці 2000-х як дружнє до розробника документне сховище, коли web-команди втомилися примушувати JSON у жорсткі ORM-моделі.
  • MVCC-модель PostgreSQL (контроль багатьох версій одночасно) — чому читання не блокують запис, але також чому vacuum стає реальною операційною задачею.
  • Рання популярність MongoDB була повʼязана з ерою «web scale», де шардинг здавався неминучістю, а не вибором із гострими наслідками.
  • Postgres отримав JSONB (бінарне зберігання JSON та індексацію), щоб задовольнити сучасні потреби додатків без втрати реляційних сильних сторін; це змінило багато рішень «Mongo за замовчуванням».
  • MongoDB додав транзакції між документами пізніше, що звузило розрив — але також внесло більше нюансів у продуктивність і тонке налаштування транзакційних робочих навантажень.
  • Логічна реплікація Postgres дозріла до практичного інструмента для міграцій, часткової реплікації та оновлень — корисно, коли «downtime неприйнятний» раптово стає вимогою.
  • Операційна історія MongoDB орієнтована на replica-set: вибори, read preferences і write concerns — це ключові концепції, а не необовʼязкові налаштування.

Це не просто цікавинка. Вони пояснюють, чому кожна система «натякає» на певні архітектури — і чому протистояти цим натякам може бути дорого.

Реальність моделювання даних: документи, рядки і брехні, які ми собі розповідаємо

Найбільша перевага MongoDB: локальність і природні агрегати

Коли «сущність» у вашому домені природно ієрархічна — замовлення з позиціями, адресою доставки і переходами станів — документи можуть гарно відображати це.
Одне читання повертає все. Один запис оновлює всю сутність. Це не просто зручність; це менше кругових викликів і менше логіки join.

Але з документами завжди залишається питання: вкладати чи посилатися? Вкладати для локальності; посилатися для спільних сутностей і обмежень зростання.
Якщо вкладати занадто багато, документи роздуваються і оновлення переписують багато байтів. Якщо посилатися занадто часто, ви винаходите join у коді додатка,
зазвичай без транзакційної безпеки між колекціями, якщо ви не платите витратами транзакцій.

Найбільша перевага PostgreSQL: гарантовані інваріанти

Postgres блискуче працює, коли важлива коректність і стосунки. Зовнішні ключі, унікальні обмеження, check-обмеження і тригери — це не «корпоративна бюрократія».
Це спосіб не дозволити вашим даним тихо перетворитися на складу незакінчених обʼєктів.

Postgres також дає JSONB, який дозволяє зберігати гнучкі атрибути без втрати можливості індексувати й запитувати їх. Це практичний компроміс: тримайте ядро реляційним
і дозвольте «мішок атрибутів» для довгого хвоста. У більшості систем є довгий хвіст.

Брехня: «Нормалізуємо пізніше» / «Очистимо документи пізніше»

Пізніше — це коли у вас більше даних, більше залежностей, більше очікувань клієнтів і менше толерантності до простою. «Пізніше» — це коли кожне очищення
стає живою міграцією з реальним ризиком. Робота зі схемою як ниття: пропустити її — економія часу, поки вже не стане ні.

Жарт №1: Ваша схема як квитанції по податках — ігнорувати їх приємно до моменту аудиту.

Транзакції та узгодженість: на що ваша аплікація непомітно ставить

Саме тут народжуються інциденти в продакшні. Не тому, що люди не знають, що таке ACID, а тому, що вони припускають, що система поводиться як остання база даних, з якою працювали.

PostgreSQL: сильні налаштування за замовчуванням, чіткі інструменти

Postgres за замовчуванням дає сильні транзакційні гарантії. Якщо ви оновлюєте дві таблиці в одній транзакції, ви або фіксуєте обидві, або жодну.
Обмеження спрацьовують на межі бази даних. Ви можете обирати рівні ізоляції; ви також можете зашкодити собі довгими транзакціями, які роздувають MVCC-історію і гальмують vacuum.
Postgres дозволяє бути винахідливим. Інколи цього робити не варто.

MongoDB: оберіть модель узгодженості явно

MongoDB може бути сильно узгодженою на практиці, якщо ви правильно налаштуєте: majority write concern, відповідний read concern і адекватні read preferences.
Може також бути «швидкою, але дивною», якщо ви читаєте з вторинних вузлів, дозволяєте застарілі читання або приймаєте записи, які ще не реплікувалися.
Це не моральна вада; це вибір. Проблема — коли це випадковий вибір.

Якщо ваша бізнес-логіка вимагає «гроші переміщено точно один раз», вам потрібна система, яка робить порушення інваріантів важким.
Postgres це робить за замовчуванням. MongoDB може це забезпечити, але ви маєте для цього спроектувати: idempotency keys, транзакційні сесії там, де потрібно,
і операційні налаштування, що відповідають вимогам коректності.

Форми продуктивності: що стає швидким, а що дивним

MongoDB: швидкі читання, поки індекси не перестануть відповідати реальності

MongoDB може бути надзвичайно швидким, коли запити збігаються з індексами і документи правильно сформовані. Біль приходить, коли команди додають нові шаблони запитів щотижня, і стратегія індексації стає реактивною.
Гірше: гнучка схема означає, що запит може обробляти кілька форм документів і відсутні поля, що веде до селективних індексів, які поводяться не так, як ви думаєте.

Ви також зустрінете «податок на ріст документів». Якщо документи ростуть з часом — додаються масиви, вкладені поля — оновлення стають важчими.
Починають зʼявлятися фрагментація зберігання і збільшення записів на диск. Це не теоретично; це те, що відбувається, коли «стрічка профілю»
переходить у «стрічку профілю плюс стрічка активності плюс налаштування плюс все інше».

PostgreSQL: join-и працюють; погані плани — ні

Postgres може обробляти join-и в масштабі, але лише якщо статистика здорова і запити адекватні. Коли продуктивність падає, часто це через:
відсутні індекси, неправильний порядок join через застарілу статистику або параметризовані запити, які створюють загальні (generic) плани,
що погано працюють для деяких значень. Виправлення зазвичай видно в EXPLAIN (ANALYZE, BUFFERS). Postgres чесний, якщо ви ставите запит правильно.

Різниця у «формі», що бʼє по операціях

Проблеми MongoDB часто виглядають як «CPU на первинному вузлі зашкалює + пропуски кешу + відставання реплікації». Проблеми Postgres часто виглядають як
«I/O насичений + autovacuum відстає + один запит робить щось дуже невдале». Обидва можна швидко діагностувати,
але ментальні моделі різні.

Індексація: мовчазний пункт бюджету

Індекси — це спосіб купити продуктивність за рахунок зберігання і вартості запису. Обидві бази карають за надмірну індексацію. Обидві ще сильніше карають за її відсутність.
Різниця — наскільки легко випадково засадити себе операційними проблемами індексами.

Підводні камені індексації в PostgreSQL

  • Додавати індекси «бо читання повільне», не перевіряючи витрати на запис або блоут.
  • Не використовувати partial indexes де доцільно, що веде до масивних індексів, які обслуговують лише частину запитів.
  • Ігнорувати fillfactor і HOT-оновлення, що збільшує блоут і навантаження на vacuum.
  • Забувати, що індекс — це також обʼєкт, який треба вакуумити і підтримувати.

Підводні камені індексації в MongoDB

  • Складні (compound) індекси, які не збігаються з порядком сортування + фільтра, що призводить до сканувань.
  • Індексація полів, яких бракує в багатьох документах, створює індекси з низькою селективністю.
  • Дозволяти TTL-індексам діяти як «безкоштовна задача видалення», а потім виявляти, що під час вікон прибирання вони створюють навантаження запису й відставання реплікації.
  • Будувати великі індекси на зайнятому первинному вузлі без плану, а потім дивуватися спайкам латентності.

Реплікація та відновлення: передбачуваний біль проти сюрпризного болю

PostgreSQL: реплікація проста; failover — це ваша робота

Фізична реплікація Postgres перевірена в бою. Але автоматичний failover не є єдиною вбудованою функцією; це вибір екосистеми
(Patroni, repmgr, Pacemaker, керовані сервіси). Коли люди кажуть «failover в Postgres складний», вони зазвичай мають на увазі «ми не вирішили,
не протестували і не відрепетирували, як працює failover».

Реплікація Postgres також змушує стикатися з утриманням WAL, replication slots і зростанням диску. Ігноруйте це — і ви дізнаєтеся, як швидко
диск може заповнитися о 03:00.

MongoDB: failover вбудований; семантика — ваша робота

Replica set обирає primary. Це добре. Але ваш додаток має обробляти транзієнтні помилки, retryable writes і реальність, що «первинний» може переміститися.
Також політика read preference визначає, чи бачать користувачі застарілі дані в певних режимах відмови.

Операційний біль MongoDB зазвичай зʼявляється, коли люди сприймають вибори як рідкісні події. Вони не рідкісні. Мережа флапає. Ноди перезавантажуються.
Оновлення ядра трапляються. Якщо поведінка клієнта не протестована під час stepdown-ів, у вас не HA, а оптимізм.

Бекапи та відновлення: ваше єдине справжнє SLA

Бекапи — це не файли, які ви копіюєте. Бекапи — це відновлення, які ви протестували. Все інше — рукоділля.

PostgreSQL

Золотий стандарт — base backups плюс WAL-архівація (point-in-time recovery). Логічні дампи підходять для менших систем або міграцій,
але вони не машина часу. Операційне питання: чи можете ви відновити в новий кластер, перевірити консистентність і переключитися без імпровізації?

MongoDB

Можна робити бекапи знімків (snapshots), знімки на рівні файлової системи (обережно, з гарантіями консистентності) або логічні бекапи на зразок mongodump.
Критично зрозуміти, чи ваш бекап захоплює консистентний знімок між шардами і replica set (якщо застосовано).
Шардовані кластери ускладнюють усе. Вони завжди ускладнюють.

Зміни схеми: міграції проти «просто відправити»

Міграції в Postgres явні й отже керовані

У Postgres зміни схеми — це робочий процес першого класу. Це означає, що ви можете переглянути їх, підготувати і виконати свідомо.
Ви все одно можете зробити помилку (блокуючий DDL у години піку — класика), але принаймні робота видима.

Зміни схеми в MongoDB неявні і тому підступні

У MongoDB зміни схеми часто відбуваються як побічний ефект деплою коду. Старі документи лишаються в старих формах, поки їх не торкнуться або не заповнять.
Це може бути перевагою — поступова міграція без великого DDL-блокування. Але це також може стати довготривалою непослідовністю, що просочується в аналітику,
пошукові індекси та поведінку, видиму клієнтам.

Операційне питання просте: ви віддаєте перевагу одній великій контрольованій міграції чи багатьом маленьким частковим міграціям з довшим періодом змішаної реальності?
Обидва підходи можуть працювати. Мішана реальність має тенденцію ставати постійною, якщо ви не встановите дедлайн очищення.

Практичні завдання з командами: що запускати, що це означає, що робити далі

Це не «навчальні команди». Це те, що ви запускаєте, коли графік виглядає неправильно і треба вирішити, що робити наступні 15 хвилин.
Кожне завдання включає: команду, що означає вивід, і рішення, яке ви приймаєте.

Завдання для PostgreSQL

1) Перевірити активні запити і чи є блокування

cr0x@server:~$ psql -h pg01 -U postgres -d appdb -c "select pid, usename, state, wait_event_type, wait_event, now()-query_start as age, left(query,120) as q from pg_stat_activity where state <> 'idle' order by age desc;"
 pid  | usename | state  | wait_event_type |  wait_event   |   age    | q
------+--------+--------+-----------------+---------------+----------+------------------------------------------------------------
 8421 | app    | active |                 |               | 00:02:14 | SELECT ... FROM orders JOIN customers ...
 9110 | app    | active | Lock            | relation      | 00:01:09 | ALTER TABLE orders ADD COLUMN ...
 8788 | app    | active | Lock            | transactionid | 00:00:57 | UPDATE orders SET ...

Значення: Довготривалий DDL чекає на блоки, плюс запити чекають на транзакційні блоки.
Рішення: Якщо DDL блокує бізнес-трафік, скасуйте DDL (або блокер), перенесіть у безпечнішу стратегію міграції.

2) Знайти, хто кого блокує

cr0x@server:~$ psql -h pg01 -U postgres -d appdb -c "select blocked.pid as blocked_pid, blocked.query as blocked_query, blocking.pid as blocking_pid, blocking.query as blocking_query from pg_locks blocked_locks join pg_stat_activity blocked on blocked.pid=blocked_locks.pid join pg_locks blocking_locks on blocking_locks.locktype=blocked_locks.locktype and blocking_locks.database is not distinct from blocked_locks.database and blocking_locks.relation is not distinct from blocked_locks.relation and blocking_locks.page is not distinct from blocked_locks.page and blocking_locks.tuple is not distinct from blocked_locks.tuple and blocking_locks.virtualxid is not distinct from blocked_locks.virtualxid and blocking_locks.transactionid is not distinct from blocked_locks.transactionid and blocking_locks.classid is not distinct from blocked_locks.classid and blocking_locks.objid is not distinct from blocked_locks.objid and blocking_locks.objsubid is not distinct from blocked_locks.objsubid and blocking_locks.pid != blocked_locks.pid join pg_stat_activity blocking on blocking.pid=blocking_locks.pid where not blocked_locks.granted;"
 blocked_pid |   blocked_query    | blocking_pid |     blocking_query
------------+---------------------+--------------+-------------------------
 8788       | UPDATE orders SET.. | 6502         | BEGIN; SELECT ...;

Значення: Транзакція, що тримає блоки, блокує оновлення.
Рішення: Убити блокуючу сесію, якщо це безпечно, або виправити патерн у додатку (наприклад, довгі транзакції).

3) Перевірити відставання реплікації

cr0x@server:~$ psql -h pg01 -U postgres -d appdb -c "select application_name, state, sync_state, write_lag, flush_lag, replay_lag from pg_stat_replication;"
 application_name |   state   | sync_state | write_lag | flush_lag | replay_lag
------------------+-----------+------------+-----------+-----------+------------
 pg02             | streaming | async      | 00:00:02  | 00:00:03  | 00:00:05

Значення: Репліка відстає на кілька секунд.
Рішення: Якщо відставання зростає — зменште пікові записи, перевірте I/O на репліці або обережно перемістіть важкі читання з первинного.

4) Виявити повільні запити за загальним часом

cr0x@server:~$ psql -h pg01 -U postgres -d appdb -c "select queryid, calls, total_exec_time::int as total_ms, mean_exec_time::int as mean_ms, rows, left(query,120) as q from pg_stat_statements order by total_exec_time desc limit 5;"
 queryid  | calls | total_ms | mean_ms | rows  | q
----------+-------+----------+---------+-------+------------------------------------------------------------
 91233123 | 18000 | 941200   | 52      | 18000 | SELECT * FROM events WHERE user_id=$1 ORDER BY ts DESC LIMIT 50

Значення: Частий запит домінує загальний час.
Рішення: Додайте потрібний індекс, переробіть запит або кешуйте на межі додатка — залежно від аналізу плану.

5) Explain запиту з буферами, щоб побачити I/O

cr0x@server:~$ psql -h pg01 -U postgres -d appdb -c "explain (analyze, buffers) select * from events where user_id=42 order by ts desc limit 50;"
 Limit  (cost=0.43..12.77 rows=50 width=128) (actual time=0.212..24.981 rows=50 loops=1)
   Buffers: shared hit=120 read=1800
   ->  Index Scan Backward using events_user_id_ts_idx on events  (cost=0.43..4212.10 rows=17000 width=128) (actual time=0.211..24.964 rows=50 loops=1)
         Index Cond: (user_id = 42)
 Planning Time: 0.188 ms
 Execution Time: 25.041 ms

Значення: Багато читань з буфера вказують на дискові I/O; індекс є, але все одно читає багато сторінок.
Рішення: Розгляньте покривний індекс, зменште ширину рядка або покращіть кеш (RAM), якщо робочий набір більший за памʼять.

6) Перевірити сигнали схожі на блоут і стан vacuum

cr0x@server:~$ psql -h pg01 -U postgres -d appdb -c "select relname, n_live_tup, n_dead_tup, round(100.0*n_dead_tup/greatest(n_live_tup,1),2) as dead_pct, last_vacuum, last_autovacuum from pg_stat_user_tables order by n_dead_tup desc limit 5;"
 relname | n_live_tup | n_dead_tup | dead_pct |     last_vacuum     |   last_autovacuum
---------+------------+------------+----------+---------------------+---------------------
 events  | 94000000   | 21000000   | 22.34    |                     | 2025-12-30 01:02:11

Значення: Багато «мертвих» кортежів; autovacuum запускався, але може відставати від інтенсивності змін.
Рішення: Налаштуйте autovacuum для гарячих таблиць, розгляньте партиціонування та дослідіть довгі транзакції, які заважають очищенню.

7) Перевірити ризики WAL і replication slot

cr0x@server:~$ psql -h pg01 -U postgres -d appdb -c "select slot_name, active, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) as retained from pg_replication_slots;"
 slot_name | active | retained
-----------+--------+----------
 wal_slot  | f      | 128 GB

Значення: Неактивний слот утримує 128 GB WAL. Ризик заповнення диска.
Рішення: Якщо споживач відсутній — видалити слот; якщо ні — відновити споживача або збільшити диск/ретеншен.

8) Перевірити тиск чекпоінтів

cr0x@server:~$ psql -h pg01 -U postgres -d appdb -c "select checkpoints_timed, checkpoints_req, round(100.0*checkpoints_req/greatest(checkpoints_timed+checkpoints_req,1),2) as req_pct, buffers_checkpoint, buffers_backend from pg_stat_bgwriter;"
 checkpoints_timed | checkpoints_req | req_pct | buffers_checkpoint | buffers_backend
------------------+-----------------+---------+--------------------+----------------
 120              | 98              | 44.96   | 81234012           | 12999876

Значення: Багато запитуваних чекпоінтів; бекендові буфери скидаються драйверами — ймовірні спайки латентності.
Рішення: Налаштуйте параметри чекпоінтів, оцініть обсяг WAL і розгляньте швидше сховище або пакетування записів.

Завдання для MongoDB

9) Перевірити стан replica set і хто первинний

cr0x@server:~$ mongosh --host mongo01:27017 --quiet --eval 'rs.status().members.map(m => ({name:m.name,stateStr:m.stateStr,health:m.health,uptime:m.uptime,lag:m.optimeDate}))'
[
  { name: 'mongo01:27017', stateStr: 'PRIMARY', health: 1, uptime: 90233, lag: ISODate('2025-12-30T02:10:01.000Z') },
  { name: 'mongo02:27017', stateStr: 'SECONDARY', health: 1, uptime: 90110, lag: ISODate('2025-12-30T02:09:58.000Z') },
  { name: 'mongo03:27017', stateStr: 'SECONDARY', health: 1, uptime: 89987, lag: ISODate('2025-12-30T02:09:57.000Z') }
]

Значення: Кластер здоровий; вторинні вузли відстають на кілька секунд.
Рішення: Якщо здоровʼя погіршується або відставання зростає — припиніть важкі читання з вторинних, перевірте диск і мережу, та перевірте налаштування write concern.

10) Перевірити поточні операції на предмет блокувань чи повільної роботи

cr0x@server:~$ mongosh --host mongo01:27017 --quiet --eval 'db.currentOp({active:true, secs_running: {$gte: 5}}).inprog.map(op => ({opid:op.opid,secs:op.secs_running,ns:op.ns,command:Object.keys(op.command||{}),waitingForLock:op.waitingForLock}))'
[
  { opid: 12345, secs: 22, ns: 'app.events', command: [ 'aggregate' ], waitingForLock: false },
  { opid: 12388, secs: 9, ns: 'app.orders', command: [ 'update' ], waitingForLock: true }
]

Значення: Оновлення чекає на блок — може бути «гарячий» документ або контеншин на рівні колекції.
Рішення: Виявити проблемний патерн (лічильник в одному документі, оновлення невпорядкованих масивів), переробити, щоб зменшити контеншн.

11) Проінспектувати профіль повільних запитів (якщо увімкнено) або використати explain

cr0x@server:~$ mongosh --host mongo01:27017 --quiet --eval 'db.events.find({userId:42}).sort({ts:-1}).limit(50).explain("executionStats").executionStats'
{
  nReturned: 50,
  totalKeysExamined: 18050,
  totalDocsExamined: 18050,
  executionTimeMillis: 84
}

Значення: Переглянуто 18k документів, щоб повернути 50. Індекс не відповідає формі запиту.
Рішення: Створити compound-індекс типу {userId:1, ts:-1} і перевірити статистику; уникати дублюючих індексів.

12) Перевірити відставання реплікації більш прямо

cr0x@server:~$ mongosh --host mongo01:27017 --quiet --eval 'db.adminCommand({replSetGetStatus:1}).members.map(m => ({name:m.name,state:m.stateStr,lagSeconds: (new Date()-m.optimeDate)/1000}))'
[
  { name: 'mongo01:27017', state: 'PRIMARY', lagSeconds: 0 },
  { name: 'mongo02:27017', state: 'SECONDARY', lagSeconds: 3.2 },
  { name: 'mongo03:27017', state: 'SECONDARY', lagSeconds: 4.1 }
]

Значення: Вторинні вузли відстають на кілька секунд.
Рішення: Якщо відставання перевищує вашу толерантність — зменшіть навантаження записів, збільшіть IOPS, налаштуйте журнали/чекпоінти (обережно) або масштабуйтесь/шардуйте з наміром.

13) Перевірити тиск кешу WiredTiger (поширений ліміт продуктивності)

cr0x@server:~$ mongosh --host mongo01:27017 --quiet --eval 'var s=db.serverStatus(); ({cacheBytesUsed:s.wiredTiger.cache["bytes currently in the cache"], cacheMax:s.wiredTiger.cache["maximum bytes configured"], evicted:s.wiredTiger.cache["pages evicted by application threads"]})'
{
  cacheBytesUsed: 32212254720,
  cacheMax: 34359738368,
  evicted: 1902231
}

Значення: Кеш майже повний, велика кількість витіснень. Ви обмежені I/O або маєте брак памʼяті.
Рішення: Додати RAM, зменшити робочий набір (індекси, проекції) або переробити запити, щоб вони були селективнішими.

14) Перевірити розміри індексів по колекції (перевірка бюджету)

cr0x@server:~$ mongosh --host mongo01:27017 --quiet --eval 'db.events.stats().indexSizes'
{
  _id_: 2147483648,
  userId_1_ts_-1: 4294967296,
  type_1_ts_-1: 3221225472
}

Значення: Індекси займають кілька ГБ. Робочий набір може не вміщатися в кеш.
Рішення: Видалити невикористовувані індекси, консолідувати або перемістити холодні запити в сховище аналітики. Індекси не безкоштовні.

15) Перевірити розподіл шардінгу (якщо шардовано) для виявлення «гарячих» частин

cr0x@server:~$ mongosh --host mongos01:27017 --quiet --eval 'db.getSiblingDB("config").chunks.aggregate([{$match:{ns:"app.events"}},{$group:{_id:"$shard",chunks:{$sum:1}}}]).toArray()'
[
  { _id: 'shard01', chunks: 412 },
  { _id: 'shard02', chunks: 398 },
  { _id: 'shard03', chunks: 401 }
]

Значення: Кількість чанків виглядає збалансовано, але баланс не те саме, що баланс навантаження.
Рішення: Якщо один шард гарячий — перегляньте shard key і маршрутизацію запитів; баланс чанків сам по собі може бути оманливим.

16) Швидко перевірити використання диска Postgres (бо сховище завжди підозрюване)

cr0x@server:~$ df -h /var/lib/postgresql
Filesystem      Size  Used Avail Use% Mounted on
/dev/nvme0n1p2  1.8T  1.6T  150G  92% /var/lib/postgresql

Значення: Ви близькі до заповнення. Postgres ненавидить повні диски; тоді починається найгірше.
Рішення: Виявити джерело росту (WAL, таблиці, тимчасові файли), негайно помʼякшити та додати оповіщення, щоб це не повторилося.

Жарт №2: Єдина річ, що масштабується безкінечно — це кількість індексів, які хтось запропонує під час простою.

Швидкий план діагностики (тріаж вузьких місць)

Коли латентність зростає, не починайте з дебатів про архітектуру. Почніть з локалізації вузького місця. Мета — визначити,
чи ви обмежені CPU, I/O, блокуваннями або мережею, і чи проблема — один поганий запит або системний тиск.

Перше: підтвердити радіус ураження

  • Це один endpoint чи все?
  • Це лише записи, лише читання чи обидва?
  • База даних повільна, чи додаток повільний при нормальній БД (вичерпання пулу зʼєднань, повтори, таймаути)?

Друге: перевірити сигнали насичення

  • CPU: Якщо CPU БД завантажений і середнє навантаження слідує — шукайте декілька дорогих запитів, відсутні індекси або runaway-агрегації.
  • I/O: Висока латентність читання, високе використання диска, спайки витіснення кешу (WiredTiger) або буферні читання (Postgres) вказують, що робочий набір не вміщується в памʼять.
  • Блокування: Багато сесій, що чекають блоків, або довгі транзакції — ознака контеншну або невдалого часу міграції.
  • Мережа: Різкі таймаути, вибори реплік або між-AZ-чаттер можуть маскуватися під проблеми бази даних.

Третє: знайти головного винуватця

  • Postgres: Перевірте pg_stat_activity на очікування і pg_stat_statements на важкі запити; підтвердіть за допомогою EXPLAIN (ANALYZE, BUFFERS).
  • MongoDB: Перевірте currentOp, повільні запити (профайлер/метрики) і explain("executionStats"); перевірте витіснення кешу і відставання реплікації.

Четверте: обрати найнебезпечніше помʼякшення

  • Скасувати/убити один найгірший запит або роботу, якщо він явно патологічний.
  • Тимчасово збільшити ресурси (CPU/RAM/IOPS), щоб отримати перепочинок.
  • Зменшити навантаження: rate limit, відключити важкі endpoint-і, призупинити батчеві задачі.
  • Зробити найменшу зміну індексації, яка виправляє домінуючий шаблон запитів (і запланувати подальші роботи).

Пʼяте: записати «чому» поки свіжо

Не довіряйте собі в майбутньому. Занотуйте запит, план, тип очікування і помʼякшення. Різниця між командою, що покращується,
і командою, що переживає ті ж самі інциденти — у здатності навмисно відтворити режим відмови.

Три міні-історії з корпоративного фронту

1) Інцидент через неправильне припущення: «Другорядні вузли безпечні для читання»

Середня SaaS-компанія використовувала MongoDB зі стандартним replica set. Команда додатка хотіла зменшити навантаження на первинний,
тож переключили читання для певних «некритичних» endpoint-ів на вторинні вузли через read preference. Ці endpoint-і були «тільки дашборди», а дашборди — «не продакшн».
Усі казали таке. Усі помилялися.

Інцидент почався з скарги клієнта: «Мої цифри відмотуються назад». Це було не лише UX-питання; це запустило автосповіщення і спричинило сплеск тикетів підтримки.
Дашборди підживлювали інші системи: робочі процеси продовження, ліміти використання та внутрішні прогнози. Ста́рий читання стало вхідними даними для бізнес-логіки,
що призвело до критичних дій.

Корінна причина не в тому, що MongoDB «загубив дані». Він робив те, на що був налаштований: віддавав читання з вторинних, які можуть відставати.
Під час періоду великої кількості записів відставання збільшилось. Дашборди читали старі дані, потім оновлення читало нові — і користувачі бачили подорож у часі.
Ніхто не описав допустимий інтервал застарілості й не тестував поведінку під час відставання.

Виправлення не було героїчним. Вони повернули read preference на primary для тих endpoint-ів і ввели явне кешування з чітким TTL і семантикою «дані можуть бути до N хвилин старими».
Також додали моніторинг відставання реплікації з порогами оповіщення, привʼязаними до бізнес-толерантності.
Результат: менше сюрпризів, і дашборди перестали викликати сварки.

2) Оптимізація, що обернулась проти: «Денормалізувати все в один документ»

Інша компанія використовувала MongoDB для профілів користувачів. Перевірка продуктивності показала занадто багато кругових викликів для складання «вигляду користувача»:
профіль, налаштування, стан підписки і список недавніх подій. Хтось запропонував очевидну оптимізацію: вкладати все в документ користувача і «просто оновлювати на записах».
Одне читання — і готово.

Вона працювала в стейджингу. Працювала навіть у продакшні деякий час. Потім додали більше «недавніх подій», потім більше історії, потім налаштування для фіч.
Документ користувача зростав. Оновлення почали торкатися дедалі більших документів. Латентність повзла, потім різко стрибнула. Відставання реплікації зросло під навантаженням,
і вибори стали частішими, бо первинний був під тривалим тиском.

Справжній відкат був не лише в розмірах. Це була ампліфікація записів і контеншн. Один гарячий документ користувача оновлювали кілька конкурентних процесів (події, білінг, feature flags),
створюючи послідовну пляшку. Деякі оновлення повторювались після тимчасових помилок, підвищуючи навантаження ще більше.
Система стала «швидкі читання, повільне все інше» — або, простіше, «пейджер».

Вони розгорнули гібрид: ключові поля профілю лишили вкладеними, але швидкозмінні та необмежені масиви винесли в окремі колекції з чіткою індексацією.
Також додали підхід append-only для «недавніх подій», замість багаторазового переписування зростаючого масиву.
Читання стало трохи складнішим, але система перестала «єсти себе».

3) Нудна, але правильна практика, що врятувала ситуацію: «Репетиції відновлення як пожежні тренування»

Регульована організація використовувала Postgres для транзакційних даних. Вони не були гламурні. Мали вікна змін, рукописи і щотижневу «репетицію відновлення»
в стейджингу, що дзеркалив продакшн настільки, щоб це було дратівливо. Інженери нарікали на витрачений час. Менеджери — на витрати. Безпека — на все.
Нормально.

Одного дня на репліці пошкодився том на рівні сховища. Failover пройшов чисто, але команда виявила, що їхні «відомі добрі» бекапи пропускали невелику, але критичну частину:
недавню зміну в логіці ретеншну WAL-архіву. Вони не виявили цього під час інциденту — виявили, бо минулотижнева репетиція відновлення вже провалилась і була виправлена.
Вони мали протестований шлях, перевірене RPO і документований запуск.

Інцидент все одно болів, бо інциденти завжди болять. Але він залишився в межах бізнес-толерантності. Постмортем не був «у нас не було бекапів».
Це було «ми практикували відновлення, тож бекапи були реальними». Така різниця — між аварією і карʼєрною подією.

Типові помилки: симптом → корінна причина → виправлення

1) Симптом: CPU Postgres в нормі, але все повільно і диски гарячі

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

Виправлення: Виявити топ-запити з pg_stat_statements, виконати EXPLAIN (ANALYZE, BUFFERS), додати таргетовані індекси і налаштувати autovacuum для гарячих таблиць.

2) Симптом: «Випадковий» блоут і зростання диска у Postgres, потім раптовий крах продуктивності

Корінна причина: Довгі транзакції перешкоджають cleanup vacuum, накопичується мертві кортежі і індексовий блоут.

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

3) Симптом: Первинний MongoDB завантажений CPU під піком, відставання реплікації зростає, потім вибори

Корінна причина: Робочий набір не вміщується в кеш + неефективні запити, що сканують занадто багато + write-heavy патерни по гарячих документах.

Виправлення: Використати explain("executionStats"), поправити compound-індекси, зменшити ріст документів і додати RAM/IOPS, коли це виправдано.

4) Симптом: Читання MongoDB «непослідовні» між запитами

Корінна причина: Read preference вказує на вторинні і є відставання, або write concern не majority і відкат стався після failover.

Виправлення: Узгодьте read/write concern з вимогами коректності; уникайте читань з вторинних для всього, що живить бізнес-логіку, якщо застарілість не прийнятна.

5) Симптом: Failover Postgres спрацював, але помилки аплікації зросли на хвилини

Корінна причина: Налаштування клієнтського зʼєднання та оновлення DNS/endpoint не налаштовані; пули зʼєднань не відновлюються коректно.

Виправлення: Використовуйте стабільну проксі/endpoint-стратегію, впровадьте безпечну логіку повторів і тестуйте failover з продакшн-подібними пулами.

6) Симптом: Шард MongoDB виглядає «збалансованим», але один шард горить

Корінна причина: Шард-ключ маршрутизує гарячі запити на один шар; кількість чанків не відображає розподіл трафіку.

Виправлення: Перегляньте shard key на основі шаблонів запитів; перевіряйте метрики по шардах і робіть таргетовану вибірку маршрутизації запитів.

7) Симптом: Диск Postgres швидко заповнюється, хоч таблиці не сильно виросли

Корінна причина: Ретеншен WAL через replication slots або некоректна архівація, або неконтрольовані тимчасові файли від сортувань/hash join-ів.

Виправлення: Перевірити розмір, утриманий replication slots, pipeline архівації і використання тимчасових файлів; додати оповіщення щодо диска з реальним запасом.

8) Симптом: «Гнучкість схеми» стала аналітичною хаотичністю

Корінна причина: Багато форм документів з часом без очищення; downstream-системи не можуть покладатися на наявність полів або збіг типів.

Виправлення: Встановити контракт схеми на межі додатка, робити бекфіл старих даних за графіком і застосовувати валідаційні правила де можливо.

Контрольні списки / покроковий план

Вибираючи Postgres без жалю

  1. Спроектуйте інваріанти насамперед: Що ніколи не повинно статися? Закодуйте це обмеженнями (unique, foreign keys, check constraints).
  2. Плануйте vacuum: Визначте найгарячіші таблиці, налаштуйте autovacuum і моніторте мертві кортежі.
  3. Використовуйте JSONB цілеспрямовано: Тримайте ядро реляційним; зберігайте довгий хвіст атрибутів у JSONB з таргетованими GIN-індексами там, де потрібно.
  4. Зробіть міграції нудними: Уникайте довгих блокувань; віддавайте перевагу патернам backfill + swap; тестуйте на даних, схожих на продакшн.
  5. Описати бекап/відновлення: Base backup + WAL-архівація; репетируйте відновлення; документуйте RPO/RTO.
  6. Визначитися з HA: Обрати підхід до failover, тестувати щоквартально і підготувати поведінку підключення аплікації.

Вибираючи MongoDB, щоб не накопичити «борг гнучкої схеми»

  1. Напишіть контракт схеми все одно: Визначте обовʼязкові поля, типи і стратегію версіонування документів.
  2. Вкладати з обмеженням: Уникайте необмежених масивів і постійно зростаючих документів; віддавайте перевагу append-only колекціям для історії подій.
  3. Індексувати з шаблонів запитів: Для кожного критичного endpoint: фільтри, порядок сортування і проекція; будувати compound-індекси відповідно.
  4. Явно налаштуйте read/write concerns: Визначте допустиму застарілість і довговічність; не лишайте це за замовчуванням або на відчуття.
  5. Репетируйте вибори: Тестуйте поведінку клієнта під час stepdown-ів; перевірте, що таймаути реалістичні і операції ідемпотентні.
  6. Бюджетуйте кеш: Моніторте витіснення WiredTiger; розміряйте RAM під робочий набір, а не надію.

Гібридний підхід, що часто перемагає: Postgres + JSONB (і дисципліна)

  1. Розміщуйте транзакційну істину в реляційних таблицях з обмеженнями.
  2. Використовуйте JSONB для еволюційних атрибутів і рідкісних опцій.
  3. Індексуйте лише те, що запитується; погодьтеся, що не всі поля JSON заслуговують індексації.
  4. Тримайте аналітику окремо, якщо шаблони запитів стають несумісними з OLTP.

Часті запитання

1) Чи стартапам варто за замовчуванням брати MongoDB за швидкість?

Обирайте те, чим ваша команда може оперувати. Якщо у вас немає сильної дисципліни щодо форми документів і індексації, Postgres буде швидшим у єдиному сенсі, що має значення:
менше дзвінків на 2 ранку.

2) Чи Postgres «повільний у масштабі», бо join-и дорогі?

Ні. Погані плани дорогі. З правильними індексами і статистикою Postgres може справлятися з великими join-навантаженнями.
Коли він підводить, зазвичай причина — зміна форми запиту і те, що ніхто не перевірив індексацію і vacuum.

3) Чи транзакції MongoDB «такі ж хороші, як у Postgres» зараз?

Вони можуть забезпечувати сильні гарантії, але ви платите ускладненням і іноді пропускною здатністю, особливо якщо широко використовувати multi-document транзакції.
Якщо вам потрібні транзакції скрізь, Postgres — простіша ставка.

4) Чи Postgres може обробляти гнучкі схеми як MongoDB?

Postgres може ефективно зберігати і запитувати JSONB, і часто цього достатньо. Але це не безкоштовно: потрібна структура, а важкий JSON-запит може створити історію індексації і блоуту.
Використовуйте JSONB для довгого хвоста, а не як філософію всієї бази.

5) Коли MongoDB явно перемагає?

Коли ваші обʼєкти домену природно документні, ви в основному звертаєтесь до них як до агрегатів цілком, і ви можете тримати структуру документів консистентною.
Також коли горизонтальне масштабування через шардинг — відома вимога і ви готові проектувати під це з першого дня.

6) Коли Postgres явно перемагає?

Коли вам потрібна сувора коректність, складні реляційні запити, обмеження як запобіжники і зрілий набір операційних інструментів.
Якщо ви будуєте білінг, інвентар, права доступу або щось, що привертає юристів — Postgres спокійніший вибір.

7) Яка найпоширеніша причина жалю після вибору MongoDB?

Сприйняття «безсхемності» як «відсутність структури». Борг проявляється як непослідовні документи, непередбачувана продуктивність запитів і аналітичні канали, які не довіряють даним.
MongoDB цього не спричиняє; команди це роблять — не навівши контракти.

8) Яка найпоширеніша причина жалю після вибору Postgres?

Недооцінка операційної роботи навколо vacuum, блоуту і блокувань під час міграцій — або використання як іграшки до того, як вона стане критичною.
Postgres стабільний, але очікує рутинного обслуговування і планування ємності.

9) Чи варто запускати обидві бази?

Тільки якщо у вас є чіткий розподіл обовʼязків і операційна зрілість для двох систем. Керувати двома базами «бо кожна краща в чомусь» — це валідно.
Керувати двома, бо не могли вирішити — це як подвоїти навантаження на on-call.

10) Керований сервіс чи self-host?

Якщо час безвідмовної роботи важливий і команда мала — керований сервіс зазвичай виграє. Self-host підходить, коли потрібен глибокий контроль і є люди, що люблять оновлення ядра і розмови про page cache.

Наступні кроки, які ви дійсно можете зробити

Якщо ви обираєте між PostgreSQL і MongoDB для нової системи, не починайте з ідеології. Почніть зі сценаріїв відмов і операційних звичок.
Потім вирішіть, що ви можете забезпечити технологією, а що очікуєте, що люди запамʼятають.

  1. Випишіть ваші інваріанти: що ніколи не повинно трапитися (подвійні списання, сирітські записи, застарілі читання у прийнятті рішень).
  2. Перерахуйте ваші топ-10 запитів: форма, фільтри, сортування, очікувана латентність і очікування росту.
  3. Визначте позицію щодо узгодженості: опишіть допустиму застарілість і довговічність; в MongoDB закодуйте це в read/write concern; в Postgres — у транзакціях і обмеженнях.
  4. Вирішіть історію бекапів: як відновлювати, скільки часу це займає, хто виконує і як часто ви репетируєте.
  5. Запустіть load-тест з даними, схожими на продакшн: не щоб отримати число бенчмарку, а щоб виявити шаблони запитів, що стають операційними мінами.
  6. Обрати опцію «що болить менше пізніше»: ту, що відповідає реальній поведінці вашої команди у вівторок вдень і під час інциденту в суботу вночі.

Мій упереджений дефолт: якщо у вас немає сильної причини для документної моделі MongoDB (і плану зберігати документи консистентними), використовуйте PostgreSQL.
Він не ідеальний, але передбачувано неідеальний — а це те, що потрібно, коли дзвонить пейджер.

← Попередня
VPN повна сітка для трьох офісів: коли вона потрібна і як її підтримувати керованою
Наступна →
Вимоги до GPU для VR-геймінгу зовсім інші: посібник інженера з продакшну

Залишити коментар