Особливості MVCC у PostgreSQL: глибокий технічний огляд
На цьому відкритому уроці Євген Арістов детально розібрав механізм мультиверсійного контролю конкурентного доступу (MVCC) у PostgreSQL, порівнявши його з класичним підходом інших реляційних СУБД. Спікер наголосив, що в сучасному світі з сотнями й тисячами одночасних користувачів критично важливо забезпечити паралельну роботу з даними, не порушуючи їхньої цілісності, атомарності, консистентності, ізоляції та довговічності (ACID). MVCC — це ключовий інструмент для вирішення цього завдання.
Класичний механізм із сегментом відкату та його недоліки
Автор пояснив, що в традиційних реляційних СУБД (наприклад, Oracle, MySQL з InnoDB) використовується сегмент відкату (undo segment). Коли виконується зміна даних, нова інформація одразу записується на диск, а в пам’яті формується «протилежна» операція для можливого відкату: при видаленні зберігаються блоки даних, при вставці — записи про необхідність видалення, при оновленні — старі значення. Проте цей підхід має суттєві проблеми:
- Ризик переповнення великими транзакціями. Якщо транзакція дуже довга або охоплює величезний обсяг даних, undo-сегмент може не вмістити всю історію змін. У такому випадку СУБД може ініціювати примусовий відкат, що потребує багато часу та ресурсів.
- Втрата даних при збоях. Undo-сегмент знаходиться в оперативній пам’яті. Якщо сервер аварійно перезавантажиться, інформація для відкату може зникнути. Хоча для відновлення використовуються журнали redo/undo, це ускладнює архітектуру.
- Висока вартість операції ROLLBACK. При скасуванні змін потрібно фізично відновити старі версії рядків на диску, що є повільною операцією, особливо при великій кількості змінених даних.
Спікер підкреслив, що в класичній схемі історія всіх змін до рядка зберігається через ланцюжок посилань на записи undo-сегмента, але через обмеження пам’яті ці дані постійно витісняються, що ускладнює аналіз минулих станів.
Як PostgreSQL реалізує MVCC: версії рядків і системні колонки
PostgreSQL пішов принципово іншим шляхом, реалізувавши так званий «версійний MVCC». Ключова ідея: при будь-якій зміні рядка створюється нова версія цього рядка, а стара позначається як застаріла. Для цього використовуються дві приховані системні колонки:
xmin— номер транзакції, яка створила дану версію рядка.xmax— номер транзакції, яка видалила (або зробила неактуальною) дану версію рядка.
Як пояснив автор, при вставці (INSERT) встановлюється xmin поточної транзакції, а xmax стає рівним 0 — сигнал, що запис «жива». При видаленні (DELETE) в оригінальному рядку просто проставляється xmax поточної транзакції. При оновленні (UPDATE) PostgreSQL виконує логічне видалення старої версії (встановлює xmax) і створює нову версію рядка з новими значеннями та xmin, що дорівнює номеру поточної транзакції.
Цей підхід дає важливу перевагу: операція ROLLBACK виконується миттєво. Замість дорогого фізичного відновлення даних, PostgreSQL просто виставляє спеціальний біт (commit/abort) для відповідної транзакції. Ніякі зміни на диску не потрібні. Також стає можливим відновити видалені дані, якщо процес вакууму ще не очистив мертві версії — досить вилучити сирі дані зі сторінок диска за допомогою спеціальних утиліт (наприклад, pageinspect), доки автовакуум не видалив цю інформацію.
Практична демонстрація: як живуть транзакції під капотом
Євген провів детальний практичний огляд. Встановивши PostgreSQL 16, він на прикладі простенької таблиці показав:
- Як кожен запит автоматично (або явно) обгортається транзакцією, і нумерація транзакцій зростає при кожній зміні.
- Після виконання
UPDATEоднієї зі стрічок, у статистиці таблиці з’являється «мертва» запис: стара версія рядка залишається на диску, але вже не видна для звичайних запитів. У демонстрації з чотирма рядками після оновлення однієї з них кількість «живих» рядків залишилася такою самою, але з’явилася одна «мертва» (тобто 20% простору зайняли застарілі дані). - Використовуючи розширення
pageinspect, спікер показав сиру сторінку даних. Окремо було видно мертву версію (xmaxпроставлений), а нова версія рядка з'явилася в кінці сторінки як вставка. - Особливу увагу було приділено колонці
t_ctid— фізичному посиланню на наступну версію рядка. Якщо запис актуальна,t_ctidвказує на себе. Якщо рядок був оновлений,t_ctidмертвої версії вказує на нову версію, утворюючи ланцюжок. Це дозволяє, теоретично, відключивши вакуум, відстежити всю історію змін. - На прикладі операції
ROLLBACKавтор продемонстрував, що бітиxmin_commitedтаxmax_commitedне проставляються, тому PostgreSQL розуміє, що зміна не була зафіксована, і просто ігнорує ці записи. Жодного фізичного відновлення даних не відбувається, що робить відкат надзвичайно швидким.
Проблеми MVCC у PostgreSQL: вакуум, фрагментація та відсутність clustered index
Попри переваги, версійний підхід має і зворотний бік. Спікер виокремив кілька принципових проблем:
- Процес вакууму (autovacuum) є обов’язковим. Накопичення мертвих версій рядків веде до зростання розміру таблиці, зниження продуктивності та навіть до помилок збігу номера транзакції (wrap-around). Вакуум — це безперервне чищення, яке потребує ресурсів і часу.
- Фрагментація даних. Через те, що при оновленні створюється нова версія рядка (не обов'язково поруч зі старою), дані «розмазуються» по сторінках. Це збільшує кількість фізичних блоків, які потрібно прочитати для виконання запиту.
- Складність із clustered index. PostgreSQL не підтримує справжній кластерний індекс (за винятком одноразової операції
CLUSTER, яка вимагає подвійного простору на диску та ексклюзивної блокіровки). Після будь-яких оновлень або видалень дані знову фрагментуються, тому кластеризація втрачає сенс. Для боротьби з цим використовуються утиліти на кшталтpg_repackабоpg_compact, які дозволяють перебудувати таблицю без блокування читання.
Додаткові теми: менторинг, вакансії та новий курс
У другій частині уроку Євген розповів про власні проекти. Він анонсував вихід нової книги з оптимізації PostgreSQL 16 орієнтовно в травні, а також розширення проекту з менторингу: експерти (DBA високого рівня) надають індивідуальні консультації та швидке занурення у складні теми (наприклад, Kafka, реплікація, тунелінг). Спікер наголосив, що через падіння якості масових курсів такий персоналізований підхід стає дедалі популярнішим.
Також він згадав про канал з вакансіями, де наразі є кілька пропозицій з високою зарплатою (450+ тисяч рублів) та низькими вимогами щодо стажу. Для учасників уроку було проведено розіграш знижки 30% на повний курс, який триватиме по суботах і неділях у вигляді інтерактивних лекцій із практикою. Переможцем став учасник Сергій, який отримав сертифікат на знижку.
Підсумки та запрошення до обговорення
Завершив урок Євген пропозицією до аудиторії обрати теми майбутніх занять: міграція баз даних, тунелінг трафіку, поглиблені блокировки. Він закликав підписуватися на YouTube-канал та вступати до Telegram-спільноти, де вже зібралося понад 200 DBA для обміну досвідом. Наступний відкритий урок заплановано на 27 квітня.