Sql

Транзакції в SQL

Детальний розбір транзакцій - ACID властивості, BEGIN/COMMIT/ROLLBACK, savepoints, рівні ізоляції, практичні сценарії

Транзакції в SQL

Проблема: Купівля останнього квитка

Уявіть, що ви купуєте останній квиток на концерт улюбленого гурту. Ваша покупка складається з двох кроків:

  1. 💸 Списати гроші з вашого балансу.
  2. 🎟️ Забронювати місце в залі.

Сценарій жаху: Гроші списалися, але перед бронюванням місця сервер вимкнувся (зникло світло, помилка мережі). Результат: Ви без грошей і без квитка. 😱

Loading diagram...
sequenceDiagram
participant User as Користувач
participant DB as База Даних
participant Wallet as Гаманець
participant Seats as Місця

    User->>DB: BEGIN TRANSACTION
    DB->>Wallet: UPDATE: -2000₴
    Wallet-->>DB: ✅ OK

    critical 💥 АВАРІЯ ТУТ!
        DB->>DB: Система впала!
        DB->>DB: ROLLBACK (АВТОМАТИЧНО)
        DB->>Wallet: 🔙 Повернути гроші
    end

    DB->>Seats: UPDATE: Зайняти місце 1A
    Seats-->>DB: ✅ OK
    DB->>User: COMMIT - Успішна покупка!

Транзакція — це група операцій, які виконуються як одне ціле: або всі успішно, або жодна.

Реальні приклади транзакцій:
  • Переказ грошей між рахунками
  • Оформлення замовлення (створити Order + OrderItems + зменшити Stock)
  • Реєстрація користувача (створити User + Profile + відправити Email)

ACID властивості

ACID властивості

Щоб гарантувати надійність, кожна транзакція повинна відповідати 4 принципам (ACID).

Уявіть, що ви замовляєте лате в кав'ярні:

A - Atomicity (Атомарність)

Принцип: "Все або нічого".

Аналогія: Не можна "трошки купити каву". Або ви платите і отримуєте напій, або ви не платите і не отримуєте нічого. Якщо кавомашина зламалася посеред приготування, бариста повертає гроші.

В SQL:

BEGIN TRANSACTION;
    -- Спроба створити студента
    INSERT INTO Students (LastName) VALUES ('Test'); -- Помилка! (NOT NULL)
    -- Якщо одна частина впала, скасовується ВСЕ
ROLLBACK;

C - Consistency (Узгодженість)

Принцип: "Тільки правильні стани".

Аналогія: Кава не може коштувати -50 грн. Кількість молока не може бути "синім". Транзакція переводить систему з одного правильного стану в інший.

В SQL:

-- `Grants` (Стипендія) не може бути від'ємною (CHECK constraint)
BEGIN TRANSACTION;
    UPDATE Students SET Grants = -100 WHERE Id = 1; -- ❌ Помилка БД!
    -- Транзакція не дозволить порушити правила
ROLLBACK;

I - Isolation (Ізоляція)

Принцип: "Ніхто не підглядає".

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

В SQL:

-- Транзакція 1 змінює прізвище студента
UPDATE Students SET LastName = 'Нове' WHERE Id = 1;

-- Транзакція 2 (паралельно) читає Id = 1
-- Вона НЕ побачить 'Нове', поки Транзакція 1 не скаже COMMIT

D - Durability (Довговічність)

Принцип: "Записано — значить записано".

Аналогія: Якщо чек надруковано (COMMIT), то навіть якщо в кав'ярні вимкнеться світло через секунду, ваша покупка зафіксована. Коли світло увімкнуть, запис про продаж все одно буде в системі.

В SQL:

COMMIT;
-- ✅ Все. Навіть якщо сервер згорить, дані на диску.

Базовий синтаксис транзакцій

Структура транзакції

BEGIN TRANSACTION;  -- Або просто BEGIN TRAN

    -- SQL операції (INSERT, UPDATE, DELETE)

    IF <умова_успіху>
        COMMIT;  -- Зберегти всі зміни
    ELSE
        ROLLBACK;  -- Скасувати всі зміни

Приклад 1: Успішна транзакція

BEGIN TRANSACTION;

    INSERT INTO Students (FirstName, LastName, BirthDate)
    VALUES ('Тест', 'Тестов', '2000-01-01');

    UPDATE Students SET Grants = Grants * 1.1
    WHERE LastName = 'Петренко';

COMMIT;  -- ✅ Обидві операції збережено

Приклад 2: Скасування транзакції

BEGIN TRANSACTION;

    DELETE FROM Students WHERE Id = 1;

    -- Помилка! Передумали

ROLLBACK;  -- ❌ DELETE скасовано, студент залишився

Практичний приклад: Індексація стипендій

Уявіть ситуацію: ректор наказав підняти всім стипендію на 10%. Але є умова: максимальна стипендія не може перевищувати 3000₴.

Якщо хоча б один студент після підвищення отримає > 3000₴ — скасувати підвищення для ВСІХ.

use University;

-- Перевіримо поточні дані
SELECT * FROM Students;

BEGIN TRANSACTION;

    DECLARE @MaxGrantAllowed DECIMAL(10, 2) = 3000.00;

    -- Крок 1: Піднімаємо стипендію всім на 10%
    UPDATE Students
    SET Grants = Grants * 1.10
    WHERE Grants IS NOT NULL;

    -- Крок 2: Перевірка "Чи не порушили ми ліміт?"
    IF EXISTS (SELECT 1 FROM Students WHERE Grants > @MaxGrantAllowed)
    BEGIN
        -- О ні! Хтось отримав забагато.
        ROLLBACK;
        PRINT '❌ Помилка: Індексацію скасовано. Перевищено ліміт бюджету.';
    END
    ELSE
    BEGIN
        -- Все, добре, ліміт не порушено
        COMMIT;
        PRINT '✅ Успіх: Стипендії проіндексовано.';
    END

-- Перевіримо результат (змінилось або ні)
SELECT * FROM Students;

TRY...CATCH з транзакціями

Best Practice: Завжди використовуйте TRY...CATCH для обробки помилок в транзакціях.

Синтаксис

BEGIN TRY
    BEGIN TRANSACTION;

        -- SQL операції

    COMMIT;
END TRY
BEGIN CATCH
    -- Якщо сталася помилка
    IF @@TRANCOUNT > 0
        ROLLBACK;  -- Скасувати транзакцію

    -- Показати помилку
    PRINT ERROR_MESSAGE();
END CATCH

Приклад: Безпечна вставка

BEGIN TRY
    BEGIN TRANSACTION;

        -- Додати студента
        INSERT INTO Students (FirstName, LastName, BirthDate, Email)
        VALUES ('Новий', 'Студент', '2000-01-01', 'test@example.com');

        -- Додати зарахування
        INSERT INTO Enrollments (StudentId, CourseId, EnrollmentDate)
        VALUES (SCOPE_IDENTITY(), 1, GETDATE());

        -- Якщо обидві операції успішні
    COMMIT;
    PRINT 'Студент успішно зареєстрований';
END TRY
BEGIN CATCH
    -- Якщо щось пішло не так (duplicate email, неіснуючий CourseId тощо)
    IF @@TRANCOUNT > 0
        ROLLBACK;

    PRINT 'Помилка: ' + ERROR_MESSAGE();
END CATCH

Корисні функції помилок

BEGIN CATCH
    SELECT
        ERROR_NUMBER() AS ErrorNumber,
        ERROR_MESSAGE() AS ErrorMessage,
        ERROR_LINE() AS ErrorLine,
        ERROR_PROCEDURE() AS ErrorProcedure,
        ERROR_SEVERITY() AS ErrorSeverity,
        ERROR_STATE() AS ErrorState;

    ROLLBACK;
END CATCH

SAVEPOINT: Часткове відкочування

SAVEPOINT (або SAVE TRANSACTION) дозволяє створити "checkpoint" всередині транзакції.

Синтаксис

BEGIN TRANSACTION;

    -- Операція 1
    INSERT INTO Table1 VALUES (...);

    SAVE TRANSACTION SavePoint1;  -- Checkpoint

    -- Операція 2
    UPDATE Table2 SET ...;

    -- Щось пішло не так з Операцією 2
    ROLLBACK TRANSACTION SavePoint1;  -- Відкотити до checkpoint
    -- Операція 1 залишається, Операція 2 скасована

COMMIT;

Приклад: Пакетна вставка з частковим откатом

BEGIN TRANSACTION;

    -- Вставка студента 1
    INSERT INTO Students (FirstName, LastName, BirthDate)
    VALUES ('Студент1', 'Прізвище1', '1998-01-01');

    SAVE TRANSACTION AfterStudent1;

    -- Спроба вставки студента 2
    BEGIN TRY
        INSERT INTO Students (FirstName, LastName, BirthDate)
        VALUES ('Студент2', 'Прізвище2', 'INVALID_DATE');  -- Помилка!
    END TRY
    BEGIN CATCH
        -- Відкотити тільки студента 2
        ROLLBACK TRANSACTION AfterStudent1;
        PRINT 'Студент 2 не додано через помилку';
    END CATCH

    -- Вставка студента 3
    INSERT INTO Students (FirstName, LastName, BirthDate)
    VALUES ('Студент3', 'Прізвище3', '1999-03-03');

COMMIT;

-- Результат: Студенти 1 та 3 додані, студент 2 - ні

Рівні ізоляції (Isolation Levels)

Рівень ізоляції відповідає на питання: "Наскільки ми дозволяємо іншим транзакціям заважати нам?"

Уявіть, що ви читаєте книгу у бібліотеці, а хтось інший намагається виправити в ній текст олівцем.

4 рівні ізоляції (від хаосу до порядку)

Аналогія: Ви читаєте статтю в блозі, яку автор прямо зараз пише. Ви бачите помилки, недописані речення. Автор може все стерти (ROLLBACK), а ви вже прочитали і повірили.

В SQL: Дозволяє "брудне читання" (Dirty Read). Найшвидший, але найнебезпечніший.

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

BEGIN TRANSACTION;
    -- Ми бачимо зміни інших транзакцій, навіть якщо вони ще не COMMIT!
    -- Наприклад, хтось змінив стипендію на 99999, але ще не зберіг.
    SELECT * FROM Students;
COMMIT;

Коли використовувати: Для приблизної аналітики (наприклад, "Скільки приблизно людей зараз на сайті?"), де точність 100% не критична.

Підсумок ризиків

РівеньDirty Read
(Читання сміття)
Non-Repeatable Read
(Зміна даних під попою)
Phantom Read
(Поява нових даних)
Швидкість
READ UNCOMMITTED🔴 Так🔴 Так🔴 Так🚀 Макс
READ COMMITTED🟢 Ні🔴 Так🔴 Так⚡ Швидко
REPEATABLE READ🟢 Ні🟢 Ні🔴 Так🐢 Повільніше
SERIALIZABLE🟢 Ні🟢 Ні🟢 Ні🐌 Дуже повільно

@@TRANCOUNT: Перевірка активних транзакцій

@@TRANCOUNT показує кількість активних (незакритих) транзакцій.

PRINT @@TRANCOUNT;  -- 0 (немає активних)

BEGIN TRANSACTION;
PRINT @@TRANCOUNT;  -- 1

    BEGIN TRANSACTION;  -- Вкладена
    PRINT @@TRANCOUNT;  -- 2

    COMMIT;
    PRINT @@TRANCOUNT;  -- 1

COMMIT;
PRINT @@TRANCOUNT;  -- 0

Використання в помилках

BEGIN TRY
    BEGIN TRANSACTION;
        -- Операції
    COMMIT;
END TRY
BEGIN CATCH
    -- Перевірити, чи є активна транзакція
    IF @@TRANCOUNT > 0
        ROLLBACK;

    PRINT ERROR_MESSAGE();
END CATCH

Deadlocks (Взаємні блокування)

Deadlock — це ситуація "клінч", коли дві транзакції чекають одна на одну і жодна не може продовжити.

Приклад із життя

Два студенти хочуть обмінятися місцями в гуртожитку:

  • Студент А зайняв кімнату 101 і чекає, поки звільниться 102.
  • Студент Б зайняв кімнату 102 і чекає, поки звільниться 101. Результат: Вони чекатимуть вічно.

Приклад в SQL (з таблицею Students)

BEGIN TRANSACTION;
    -- 1. Блокуємо Студента 1
    UPDATE Students SET Grants = 2000 WHERE Id = 1;

    WAITFOR DELAY '00:00:05'; -- Імітація думки...

    -- 2. Хочемо заблокувати Студента 2 (але він зайнятий Транзакцією 2!)
    UPDATE Students SET Grants = 2000 WHERE Id = 2;
COMMIT;

Як уникнути deadlocks

1. Однаковий порядок

Правило: Завжди блокуйте ресурси в одному порядку (наприклад, за зростанням ID).

-- ✅ Завжди спочатку Id=1, потім Id=2
BEGIN TRANSACTION;
    UPDATE Students ... WHERE Id = 1;
    UPDATE Students ... WHERE Id = 2;
COMMIT;

2. Коротші транзакції

Чим швидше ви зробите COMMIT, тим менше шансів, що хтось інший вклиниться.

3. NOLOCK (читання без блокувань)

Якщо вам лише почитати і не важлива супер-точність:

SELECT * FROM Students WITH (NOLOCK);

Практичні сценарії

Сценарій 1: Безпечна реєстрація студента

Задача: Зареєструвати студента. Якщо такий Email вже є — не вставляти, а повернути помилку. Якщо студенту < 16 років — не реєструвати.

BEGIN TRY
    BEGIN TRANSACTION;

        DECLARE @Email NVARCHAR(100) = 'new.student@example.com';
        DECLARE @BirthDate DATE = '2010-01-01';

        -- 1. Перевірка Email
        IF EXISTS (SELECT 1 FROM Students WHERE Email = @Email)
        BEGIN
            THROW 51000, 'Такий Email вже існує!', 1;
        END

        -- 2. Перевірка віку
        IF DATEDIFF(YEAR, @BirthDate, GETDATE()) < 16
        BEGIN
             THROW 51000, 'Студент занадто молодий!', 1;
        END

        -- 3. Вставка
        INSERT INTO Students (LastName, FirstName, BirthDate, Email)
        VALUES ('Новий', 'Студент', @BirthDate, @Email);

    COMMIT;
    PRINT 'Студент успішно доданий!';
END TRY
BEGIN CATCH
    IF @@TRANCOUNT > 0
        ROLLBACK;
    PRINT 'Помилка: ' + ERROR_MESSAGE();
END CATCH

Сценарій 2: Пакетне оновлення з логуванням

BEGIN TRY
    BEGIN TRANSACTION;

        -- Таблиця для логу (тимчасова змінна)
        DECLARE @UpdateLog TABLE (
            StudentId INT,
            OldGrants DECIMAL(10, 2),
            NewGrants DECIMAL(10, 2)
        );

        -- Оновити стипендії з логуванням
        UPDATE Students
        SET Grants = Grants * 1.15
        OUTPUT
            INSERTED.Id,
            DELETED.Grants,
            INSERTED.Grants
        INTO @UpdateLog
        WHERE YEAR(BirthDate) = 1998;

        -- Перевірити кількість оновлених
        DECLARE @UpdatedCount INT = (SELECT COUNT(*) FROM @UpdateLog);

        IF @UpdatedCount = 0
        BEGIN
            -- Якщо нікого не оновили - можна і закоммітити (нічого не змінилось),
            -- або відкотити, якщо це вважається помилкою бізнес-логіки.
            PRINT 'Жоден студент не підпав під критерії.';
            COMMIT;
        END
        ELSE
        BEGIN
            COMMIT;
            PRINT CAST(@UpdatedCount AS NVARCHAR) + ' студентів оновлено';

            -- Показати лог
            SELECT * FROM @UpdateLog;
        END

END TRY
BEGIN CATCH
    IF @@TRANCOUNT > 0
        ROLLBACK;
    PRINT ERROR_MESSAGE();
END CATCH

Best Practices

1. Коротші транзакції

-- ❌ Погано - довга транзакція
BEGIN TRANSACTION;
    -- Довгий звіт, який блокує таблицю
    SELECT * FROM Students WHERE Grants > 0;
    -- Оновлення
    UPDATE Students SET Grants = 0 WHERE Id = 1;
COMMIT;

-- ✅ Добре - коротка транзакція
-- Звіт робимо БЕЗ транзакції (або з NOLOCK)
SELECT * FROM Students WITH(NOLOCK) WHERE Grants > 0;

-- А оновлення - швидко в транзакції
BEGIN TRANSACTION;
    UPDATE Students SET Grants = 0 WHERE Id = 1;
COMMIT;

2. Завжди TRY...CATCH

BEGIN TRY
    BEGIN TRANSACTION;
        -- Операції
    COMMIT;
END TRY
BEGIN CATCH
    IF @@TRANCOUNT > 0
        ROLLBACK;
    -- Обробка помилки
END CATCH

3. Явний COMMIT/ROLLBACK

-- ✅ Добре - явний COMMIT
BEGIN TRANSACTION;
    UPDATE Students ...;
COMMIT;

-- ❌ Погано - забули COMMIT
BEGIN TRANSACTION;
    UPDATE Students ...;
-- Транзакція висить і блокує таблицю!

4. Мінімум операцій

Тримайте в транзакції тільки те, що має бути атомарним:

-- ✅ Добре
BEGIN TRANSACTION;
    INSERT INTO Students ...;
    INSERT INTO StudentLog ...;
COMMIT;

-- ❌ Погано
BEGIN TRANSACTION;
    INSERT INTO Students ...;
    -- Відправка email (це повільно і не стосується БД!)
    -- Генерація PDF звіту
    INSERT INTO StudentLog ...;
COMMIT;

::


Практичні завдання


Резюме

Ключові моменти транзакцій:
  1. ACID властивості: Atomicity, Consistency, Isolation, Durability
  2. Синтаксис: BEGIN TRANSACTION → операції → COMMIT або ROLLBACK
  3. TRY...CATCH — обов'язково для production коду
  4. @@TRANCOUNT — перевірка активних транзакцій
  5. SAVEPOINT — часткове відкочування в транзакції
  6. Рівні ізоляції:
    • READ UNCOMMITTED (найшвидший, найменш безпечний)
    • READ COMMITTED (за замовчуванням)
    • REPEATABLE READ
    • SERIALIZABLE (найповільніший, найбезпечніший)
  7. Deadlocks — уникайте однаковим порядком доступу до таблиць
  8. Best Practices:
    • Коротші транзакції
    • Мінімум операцій в транзакції
    • Завжди явний COMMIT/ROLLBACK
    • Обробка помилок через TRY...CATCH
Вітаємо! Ви завершили вивчення основ SQL!

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

Copyright © 2026