💳 Розділ 11 · Питання #3

Що таке Read Uncommitted

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


🟢 Junior Level

Read Uncommitted (Читання незафіксованих даних) — це найнижчий і найменш суворий рівень ізоляції транзакцій у стандарті SQL.

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

Головна проблема: Брудне читання (Dirty Read)

Якщо транзакція прочитала незафіксовані дані, а транзакція-автор потім скасувала свої зміни (ROLLBACK), прочитані дані виявляються хибними («фантомними»), оскільки в базі даних їх більше не існує.

Час    Транзакція А (Списання)           Транзакція Б (Read Uncommitted)
  |    -----------------------           -------------------------------
  |  T1: BEGIN;
  |  T2: UPDATE accounts
  |      SET balance = 500 WHERE id = 1;
  |  T3:                                   SELECT balance FROM accounts WHERE id = 1;
  |                                        -- Результат: 500 (БРУДНЕ ЧИТАННЯ!)
  |  T4: ROLLBACK; -- Відкат змін!
  |  T5:                                   Транзакція Б впевнена, що баланс 500,
  v                                        хоча фактично він залишився рівним 100!
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

@Service
public class AnalyticsService {

    // У Spring рівень ізоляції можна оголосити явно:
    @Transactional(isolation = Isolation.READ_UNCOMMITTED, readOnly = true)
    public long getApproximateActiveUsersCount() {
        // Допустимо лише для приблизної фонової телеметрії,
        // де похибка у кілька відсотків не відіграє ролі.
        return userRepository.countActiveUsers();
    }
}

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


🟡 Middle Level

Особливості поведінки в різних СУБД

  1. PostgreSQL та Oracle: У цих СУБД рівень Read Uncommitted фізично відсутній. Якщо в PostgreSQL виконати:
    SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
    

    СУБД прийме команду без синтаксичної помилки (задля сумісності зі стандартом SQL), але внутрішньо виконає її на рівні Read Committed. Механізм MVCC у PostgreSQL архітектурно відсікає незакомічені кортежі за картою статусів транзакцій (pg_xact).

  2. MySQL (InnoDB): Реально підтримує Read Uncommitted. У цьому режимі InnoDB читає останню версію рядка безпосередньо з буферного пулу оперативної пам’яті, не переходячи ланцюжками Undo Log (відкатних сегментів).
  3. Microsoft SQL Server: Поведінка Read Uncommitted еквівалентна використанню табличного хінта WITH (NOLOCK):
    SELECT * FROM Orders WITH (NOLOCK);
    

    Запит читає сторінки без запиту розділюваних блокувань (Shared Locks / S-locks), ігноруючи накладені іншими потоками монопольні блокування (Exclusive Locks / X-locks).

Допустимі аномалії

Аномалія Допускається на Read Uncommitted?
Брудне читання (Dirty Read) ⚠️ Так
Неповторюване читання (Non-Repeatable Read) ⚠️ Так
Фантомне читання (Phantom Read) ⚠️ Так
Втрачене оновлення (Lost Update) ⚠️ Так
Брудний запис (Dirty Write) ❌ НІ (Заборонений завжди)

🔴 Senior Level

Чому Dirty Write ($G_0$) заборонений навіть на Read Uncommitted?

Стандарт ANSI SQL допускає на рівні Read Uncommitted усі види аномалій читання, але категорично забороняє аномалію Dirty Write (Брудний запис):

\[\text{Dirty Write: } T_1 \text{ modifies } X \implies T_2 \text{ modifies } X \implies T_1 \text{ or } T_2 \text{ aborts}\]

Якби СУБД дозволяла транзакції $T_2$ перезаписати дані, змінені незафіксованою транзакцією $T_1$, під час подальшого відкату $T_1$ механізм Undo Log не зміг би визначити, яке значення відновлювати, що призвело б до фізичного пошкодження бази даних (Database Corruption). Тому в усіх СУБД (зокрема в MySQL та SQL Server) будь-які операції модифікації (UPDATE, DELETE, INSERT) завжди накладають монопольні блокування (Exclusive X-Locks) до кінця транзакції, унеможливлюючи одночасний несинхронізований запис в один рядок навіть на Read Uncommitted.

Апаратні аномалії SQL Server: WITH (NOLOCK) та Page Splitting

У дискових сторінкових рушіях із блокуваннями (SQL Server) читання на рівні Read Uncommitted / NOLOCK загрожує не просто логічними помилками бізнес-даних, а й структурними аномаліями вибірки (Allocation Order Scans):

  1. Пропуск рядків (Skipped Rows): Поки транзакція-читач сканує сторінку за порядком фізичного розміщення на диску, паралельна транзакція вставляє запис. Відбувається розщеплення сторінки (B-Tree Page Split): половина рядків переноситься на нову сторінку, яка вже залишилася позаду вказівника сканування. Читач пропускає частину наявних рядків.
  2. Дублювання рядків (Duplicate Rows): Якщо під час Page Split дані переносяться на сторінку, до якої сканер ще не дійшов, читач поверне один і той самий запис двічі.
  3. Помилка 601 (Index Corruption Exception): Якщо паралельна транзакція ініціює видалення або реорганізацію сторінок індексу, сканувальний запит аварійно завершиться з системною помилкою доступу до сторінок пам’яті.

4 Tricky Questions

1. Чому в PostgreSQL встановлення SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; не дозволяє прочитати «брудні» дані?

Відповідь: У рушії PostgreSQL механізм ізоляції повністю базується на MVCC (Multi-Version Concurrency Control). Кожен рядок (Tuple) у файлі таблиці містить заголовок із полями xmin (ID транзакції, що створила рядок) та xmax (ID транзакції, що видалила рядок). Під час будь-якого читання PostgreSQL звіряє xmin рядка з бітовою картою статусів транзакцій в оперативній пам’яті (pg_xact). Якщо транзакція xmin має статус IN_PROGRESS (ще не зафіксована) або ABORTED, ядро СУБД автоматично вважає цю версію рядка невидимою. Щоб реалізувати брудне читання, розробникам PostgreSQL довелося б навмисно ускладнити код перевірки видимості заради сумнівного антипатерну. Тому в PostgreSQL рівень Read Uncommitted синтаксично приймається, але працює строго як Read Committed.


2. Чи дає READ UNCOMMITTED відчутний виграш у швидкості в MySQL InnoDB порівняно з READ COMMITTED?

Відповідь: Практично ні. У MySQL InnoDB читання на рівні Read Committed не вимагає встановлення shared-блокувань — воно також є неблокувальним і звертається до кешу пам’яті (Buffer Pool). Єдина мікроскопічна різниця: на рівні Read Committed рушій у разі виявлення незафіксованого рядка розгортає ланцюжок Undo Log до останньої зафіксованої версії (створює Read View), а на Read Uncommitted читає поточні байти сторінки без розгортання Undo-логу. На сучасних багатоядерних серверах різниця у продуктивності становить частки відсотка, що абсолютно не виправдовує колосальний ризик спотворення бізнес-логіки.


3. Чи дозволяє JPA / Hibernate використовувати рівень ізоляції Read Uncommitted у методах репозиторію?

Відповідь: Так, анотація @Transactional(isolation = Isolation.READ_UNCOMMITTED) підтримується Spring і транслюється в JDBC-драйвер через виклик connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED). Проте використання цього рівня в застосунках із Hibernate несе фатальну небезпеку для кешу першого рівня (Persistence Context). Якщо Hibernate прочитає «брудну» сутність, яка згодом буде відкочена в БД, цей брудний об’єкт осяде в кеші першого рівня поточної сесії EntityManager. Будь-які подальші запити або автоматичний flush у цій сесії збережуть некоректні дані назад у базу даних.


4. У яких рідкісних production-сценаріях використання READ UNCOMMITTED (або NOLOCK) усе ще вважається виправданим?

Відповідь: Існує лише три легітимні сценарії:

  1. Приблизні моніторингові дашборди (Telemetry): Збір метрик типу «скільки приблизно повідомлень перебуває в черзі брокера» або «приблизна кількість активних користувачів онлайн за добу», де допустима похибка в $\pm 5\%$, але критично важливо не заблокувати транзакції запису на старих СУБД із блокувальником (SQL Server).
  2. Екстрена діагностика на проді (DBA Troubleshooting): Коли база даних заблокована довгими транзакціями, адміністратор виконує швидкий SELECT ... WITH (NOLOCK) для з’ясування стану завислих процесів, щоб сам діагностичний запит не став у кінець черги на блокування.
  3. Логи та аудит-архіви: Аналітичні звіти за таблицями аудиту (Append-Only), де операції UPDATE та DELETE заборонені тригерами, і нові рядки лише додаються.

🎯 Шпаргалка для інтерв’ю

  • Сутність Read Uncommitted: Найслабший рівень; дозволяє читання незафіксованих даних інших транзакцій (Dirty Read).
  • Де відсутній: У PostgreSQL та Oracle цього рівня немає (автоматично підвищується до Read Committed без попереджень).
  • Аномалії: Дозволені Dirty Read, Non-Repeatable Read, Phantom Read, Lost Update.
  • Dirty Write ($G_0$): Суворо заборонений навіть на Read Uncommitted (монопольні X-блокування на запис беруться завжди в усіх СУБД).
  • Небезпека NOLOCK у SQL Server: Читання без Shared Locks може призводити до дублювання або пропуску рядків під час розщеплення сторінок (Page Splits).
  • Production правило: Не використовувати в бізнес-логіці та транзакційних сервісах.

Червоні прапорці (чого уникати)

  • Твердження, що Read Uncommitted суттєво прискорює запити в MVCC-базах (у Postgres його немає, а в MySQL різниця незначна).
  • Використання Read Uncommitted у бізнес-логіці, фінансових розрахунках або замовленнях.
  • Незнання того, що брудний запис (Dirty Write) заборонений на всіх рівнях ізоляції без винятку.
  • Твердження, що WITH (NOLOCK) у SQL Server абсолютно безпечний для звичайних SELECT-запитів.

Пов’язані теми