Triggers Stored Procedures

DML-тригери

Автоматичне реагування на зміни даних - INSERT, UPDATE, DELETE

DML-тригери

Проблема: Як автоматизувати реакцію на зміни даних?

Уявіть інтернет-магазин книг. При кожному продажу потрібно:

  • Зменшити кількість книг на складі
  • Записати в журнал аудиту
  • Перевірити чи не продається останній екземпляр бестселера
  • Обновити статистику продажів

Можна робити це вручну в кожному запиті INSERT/UPDATE/DELETE... Але що, якщо база даних сама могла б відреагувати на зміну даних?

Loading diagram...
flowchart LR
    User[Користувач] -->|INSERT INTO Sales| DB[(База Даних)]
    DB -->|1. Спрацьовує тригер| Trigger[Trigger Logic]
    Trigger -->|2. Зменшує кількість| Books[Table: Books]
    Trigger -->|3. Записує в журнал| AuditLog[Table: AuditLog]
    Trigger -->|4. Повертає результат| User
    
    style DB fill:#3b82f6,color:#fff
    style Trigger fill:#f59e0b,color:#fff
    style Books fill:#10b981,color:#fff
    style AuditLog fill:#10b981,color:#fff

DML-тригер (Trigger) — це спеціалізована процедура, яка автоматично викликається при виникненні подій зміни даних (INSERT, UPDATE, DELETE) в таблиці або представленні.

DML — Data Manipulation Language (Мова Маніпулювання Даними). DML-тригери реагують саме на команди маніпулювання даними, а не на зміни структури БД (для цього є DDL-тригери).

Навіщо потрібні тригери?

Use Cases (Сценарії використання)

Забезпечення цілісності даних

Автоматична перевірка бізнес-правил, які неможливо реалізувати через FOREIGN KEY або CHECK constraints.

Приклад: Заборонити видалення книги-бестселера.

Аудит змін

Автоматичне логування всіх змін даних (хто, коли, що змінив).

Приклад: Записувати в окрему таблицю всі видалені книги.

Синхронізація даних

Автоматичне оновлення залежних даних в інших таблицях.

Приклад: При продажу зменшити кількість книг на складі.

Обчислення значень

Автоматичний перерахунок агрегованих значень.

Приклад: Оновлювати середню ціну книг при зміні ціни.

Важливо: Тригери НЕ повинні замінювати constraints (обмеження цілісності)!
  • FOREIGN KEY, CHECK, UNIQUE — використовуйте для простої цілісності
  • Тригери — для складної бізнес-логіки, яку неможливо виразити через constraints
Чому? Constraints швидші і явно показують структуру БД.

Фундаментальні концепції

Автоматичне виконання

Тригери НЕ викликаються явно. Вони спрацьовують автоматично при виникненні події.

-- При кожному INSERT потрібно вручну:
BEGIN TRANSACTION;

INSERT INTO Sales (BookId, Quantity, Price)
VALUES (1, 2, 250.00);

-- Зменшити кількість на складі
UPDATE Books
SET QuantityInStock = QuantityInStock - 2
WHERE Id = 1;

-- Записати в журнал
INSERT INTO AuditLog (ActionType, TableName, ActionDate)
VALUES ('INSERT', 'Sales', GETDATE());

COMMIT;

Таблиці INSERTED та DELETED

MS SQL Server надає дві логічні таблиці для доступу до даних, які змінюються:

ТаблицяКоли заповнюєтьсяЩо містить
INSERTEDINSERT, UPDATEНові значення рядків
DELETEDDELETE, UPDATEСтарі значення рядків (до зміни)
Loading diagram...
@startuml
skinparam style plain
skinparam defaultFontName "Segoe UI"

rectangle "Базова таблиця\nBooks" as Books #LightBlue

rectangle "INSERTED\n(в пам'яті)" as INS #LightGreen
rectangle "DELETED\n(в пам'яті)" as DEL #LightCoral

note right of INS
  Містить нові дані
  при INSERT та UPDATE
end note

note right of DEL
  Містить старі дані
  при DELETE та UPDATE
end note

Books --> INS : INSERT:\nДані спочатку\nйдуть в INSERTED
Books --> DEL : DELETE:\nДані спочатку\nйдуть в DELETED

INS -[hidden]down- DEL

@enduml

Як це працює:

  1. Перед вставкою в базову таблицю дані потрапляють в INSERTED
  2. Спрацьовує тригер → може прочитати дані з INSERTED
  3. Дані вставляються в базову таблицю
-- При виконанні:
INSERT INTO Books (Title, Price) VALUES ('SQL для початківців', 299.99);

-- Таблиця INSERTED містить:
-- Title                  | Price
-- 'SQL для початківців'  | 299.99

-- Таблиця DELETED — порожня
Ключова перевага: Доступ до даних без додаткових запитів до БД. Тригер може відразу прочитати що саме вставляється/видаляється/змінюється.

Типи DML-тригерів: AFTER vs INSTEAD OF

MS SQL Server підтримує два режими DML-тригерів:

Виконується ПІСЛЯ зміни даних в базовій таблиці.

CREATE TRIGGER trg_AfterInsert
ON Books
AFTER INSERT  -- або FOR INSERT (синоніми)
AS
BEGIN
    PRINT 'Книга вже додана в таблицю!';
END;

Характеристики:

  • ✅ Дані вже в таблиці
  • ✅ Можна відмінити зміни через ROLLBACK TRANSACTION
  • Тільки для таблиць (не для представлень)
  • Використання: аудит, синхронізація, логування
Loading diagram...
@startuml
skinparam style plain
skinparam defaultFontName "Segoe UI"

start

:Команда INSERT/UPDATE/DELETE;

if (Тип тригера?) then (INSTEAD OF)
  :Тригер INSTEAD OF виконується;
  note right
    Команда НЕ виконалась!
    Тригер вирішує що робити
  end note
  
  if (Тригер виконав команду?) then (Так)
    :Дані змінені в таблиці;
  else (Ні)
    :Дані НЕ змінені;
  endif
  
else (AFTER / FOR)
  :Команда виконується;
  :Дані змінені в таблиці;
  
  :Тригер AFTER виконується;
  note right
    Може відмінити зміни
    через ROLLBACK
  end note
  
  if (ROLLBACK в тригері?) then (Так)
    :Зміни відмінені;
  else (Ні)
    :Зміни збережені;
  endif
endif

stop

@enduml

Базовий синтаксис CREATE TRIGGER

CREATE TRIGGER [schema.]trigger_name
    ON {table_name | view_name}
    [WITH ENCRYPTION]  -- шифрування коду тригера
    {FOR | AFTER | INSTEAD OF}
    {[INSERT] [,] [UPDATE] [,] [DELETE]}
    [NOT FOR REPLICATION]  -- не виконувати при реплікації
AS
BEGIN
    -- Тіло тригера (T-SQL код)
END;

Анатомія:

  • trigger_name — ім'я тригера (рекомендація: trg_<Table>_<Action>_<Operation>, наприклад trg_Books_After_Insert)
  • ON table_name — для якої таблиці/представлення створюється тригер
  • FOR | AFTER | INSTEAD OF — режим виконання
    • FOR та AFTER — синоніми (за замовчуванням AFTER)
  • INSERT, UPDATE, DELETE — на які події реагувати (можна комбінувати)
  • NOT FOR REPLICATION — тригер не виконається при реплікації даних

Практичні приклади

Приклад 1: Логування кількості змінених рядків

Задача: При додаванні або зміні авторів виводити повідомлення про кількість оброблених рядків.

CREATE TRIGGER trg_Authors_AfterInsertUpdate
ON book.Authors
FOR INSERT, UPDATE  -- Спрацює і на INSERT, і на UPDATE
AS
BEGIN
    DECLARE @rowCount INT = @@ROWCOUNT;
    
    RAISERROR('%d рядків було додано або модифіковано', 0, 1, @rowCount);
END;
GO

-- Тестуємо:
INSERT INTO book.Authors (FirstName, LastName, CountryId)
VALUES ('Артур', 'Конан Дойл', 2);

-- Результат: "1 рядків було додано або модифіковано"

Пояснення:

  • @@ROWCOUNT — глобальна змінна, яка містить кількість рядків, що оброблені останньою командою
  • RAISERROR — виводить форматоване повідомлення (0 — рівень важливості для інформаційних повідомлень)

Приклад 2: Перевірка дати публікації

Задача: Заборонити додавання книг, дата публікації яких старше 30 днів.

CREATE TRIGGER trg_Books_CheckPublishDate
ON book.Books
FOR INSERT
AS
BEGIN
    DECLARE @publishDate SMALLDATETIME;
    
    -- Отримуємо дату з таблиці INSERTED
    SELECT @publishDate = DateOfPublish
    FROM inserted;
    
    -- Перевіряємо чи дата не старша 30 днів
    IF @publishDate <= DATEADD(DAY, -30, GETDATE())
    BEGIN
        RAISERROR('Це стара книга, дані про неї додані не будуть', 16, 1);
        ROLLBACK TRANSACTION;  -- Відміняємо INSERT
    END
    ELSE
    BEGIN
        PRINT 'Дані додані успішно';
    END
END;
GO

-- Тестуємо:
INSERT INTO book.Books (NameBook, ThemeId, AuthorId, Price, DateOfPublish, Pages)
VALUES ('Адміністрування MS SQL Server 2005', 21, 1, 125.0, '2007-09-01', 726);

-- Результат: "Це стара книга, дані про неї додані не будуть"
-- INSERT відмінено через ROLLBACK

Пояснення:

  • Використовуємо таблицю inserted для доступу до даних, які вставляються
  • DATEADD(DAY, -30, GETDATE()) — дата 30 днів тому від сьогодні
  • ROLLBACK TRANSACTION — відміняє всі зміни поточної транзакції (в тому числі INSERT)
  • RAISERROR з рівнем 16 — помилка рівня користувача
Best Practice: Завжди використовуйте IF EXISTS або змінні для роботи з inserted/deleted, оскільки ці таблиці можуть містити кілька рядків (наприклад, при масовому INSERT).

Приклад 3: Захист від видалення бестселера

Задача: Заборонити видалення книги, яка є лідером продажів.

CREATE TRIGGER trg_Books_PreventBestsellerDelete
ON book.Books
FOR DELETE
AS
BEGIN
    DECLARE @deletedBookName NVARCHAR(100);
    DECLARE @bestsellerName NVARCHAR(100);
    
    -- Отримуємо назву книги, яку видаляють
    SELECT @deletedBookName = NameBook
    FROM deleted;
    
    -- Знаходимо бестселер (книга з найбільшою кількістю продажів)
    SELECT TOP 1 @bestsellerName = b.NameBook
    FROM book.Books b
    INNER JOIN sale.Sales s ON b.Id = s.BookId
    GROUP BY b.NameBook
    ORDER BY COUNT(s.Id) DESC;
    
    -- Перевіряємо чи збігаються назви
    IF @bestsellerName = @deletedBookName
    BEGIN
        RAISERROR('Ви не можете видалити дану книгу - це бестселер!', 16, 1);
        ROLLBACK TRANSACTION;
    END
    ELSE
    BEGIN
        PRINT 'Книга видалена успішно';
    END
END;
GO

Пояснення:

  • Використовуємо таблицю deleted для доступу до видаляємих даних
  • Підзапит знаходить книгу з найбільшою кількістю продажів
  • Якщо книга співпадає з бестселером — видалення заборонено

Приклад 4: INSTEAD OF для "м'якого" видалення

Задача: Замість фізичного видалення книги програмування — позначати як видалену (soft delete).

-- Спочатку додамо колонку IsDeleted до таблиці Books
ALTER TABLE book.Books
ADD IsDeleted BIT NOT NULL DEFAULT 0;
GO

CREATE TRIGGER trg_Books_InsteadOfDelete
ON book.Books
INSTEAD OF DELETE
AS
BEGIN
    DECLARE @programmingThemeId INT;
    
    -- Отримуємо ID тематики "Програмування"
    SELECT @programmingThemeId = Id
    FROM book.Themes
    WHERE NameTheme = 'Програмування';
    
    -- Перевіряємо чи є в deleted книги цієї тематики
    IF EXISTS (
        SELECT 1
        FROM deleted
        WHERE ThemeId = @programmingThemeId
    )
    BEGIN
        -- Замість видалення — позначаємо IsDeleted = 1
        UPDATE book.Books
        SET IsDeleted = 1
        WHERE Id IN (SELECT Id FROM deleted WHERE ThemeId = @programmingThemeId);
        
        RAISERROR('Книги програмування не видалено фізично, а перенесено в архів', 10, 1);
    END
    ELSE
    BEGIN
        -- Для інших тематик — виконуємо реальне видалення
        DELETE FROM book.Books
        WHERE Id IN (SELECT Id FROM deleted);
        
        PRINT 'Книги видалено';
    END
END;
GO

Пояснення:

  • INSTEAD OF DELETE — команда DELETE НЕ виконалась автоматично
  • Тригер сам вирішує: для програмування — soft delete, для інших — реальне видалення
  • Важливо: Якщо потрібне реальне видалення, тригер повинен сам виконати DELETE

Правила та обмеження

Заборонено в тілі DML-тригерів:
  • ❌ Створення БД: CREATE/ALTER/DROP DATABASE
  • TRUNCATE TABLE (спрацює тільки DELETE)
  • RECONFIGURE, BACKUP, RESTORE
  • GRANT, REVOKE (права доступу)
  • SELECT INTO (створення таблиці)
  • UPDATE STATISTICS
Дозволено:
  • ✅ INSERT, UPDATE, DELETE в інших таблицях
  • ✅ SELECT для читання даних
  • ✅ Виклик зберігаємих процедур
  • ✅ Робота з тимчасовими таблицями
  • ✅ ROLLBACK TRANSACTION
Важливі правила:
  1. Тимчасові таблиці: НЕ можна створювати тригери для тимчасових таблиць, але тригер може звертатися до них
  2. Представлення: INSTEAD OF тригери можуть бути створені для представлень
  3. AFTER тригери: Тільки для таблиць
  4. Повернення результатів: Тригери не повинні повертати result sets (уникайте SELECT без INTO/INSERT)
  5. Рекурсія: Підтримується до 32 рівнів (потрібно увімкнути RECURSIVE_TRIGGERS)

Управління тригерами

Зміна тригера (ALTER TRIGGER)

ALTER TRIGGER trg_Books_AfterInsert
ON book.Books
AFTER INSERT
AS
BEGIN
    -- Новий код тригера
    PRINT 'Оновлений тригер!';
END;

Відключення тригера

-- Відключити конкретний тригер
DISABLE TRIGGER trg_Books_AfterInsert ON book.Books;

-- Відключити ВСІ тригери на таблиці
DISABLE TRIGGER ALL ON book.Books;

Увімкнення тригера

-- Увімкнути конкретний тригер
ENABLE TRIGGER trg_Books_AfterInsert ON book.Books;

-- Увімкнути ВСІ тригери на таблиці
ENABLE TRIGGER ALL ON book.Books;

Видалення тригера

DROP TRIGGER trg_Books_AfterInsert;

-- Видалити кілька тригерів відразу
DROP TRIGGER trg_Books_AfterInsert, trg_Authors_AfterUpdate;

Метадані тригерів

Інформація про тригери зберігається в системних представленнях:

-- Список всіх тригерів бази даних
SELECT 
    name AS TriggerName,
    OBJECT_NAME(parent_id) AS TableName,
    is_disabled AS IsDisabled,
    is_instead_of_trigger AS IsInsteadOf
FROM sys.triggers
WHERE parent_class_desc = 'OBJECT_OR_COLUMN';

-- Отримати код тригера
SELECT m.definition
FROM sys.sql_modules m
INNER JOIN sys.triggers t ON m.object_id = t.object_id
WHERE t.name = 'trg_Books_AfterInsert';

-- Альтернатива: через sp_helptext
EXEC sp_helptext 'trg_Books_AfterInsert';

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


Резюме

Ключові моменти DML-тригерів:
  1. Автоматичне виконання — спрацьовують без явного виклику
  2. INSERTED та DELETED — логічні таблиці для доступу до даних
  3. AFTER — виконується після зміни (тільки таблиці)
  4. INSTEAD OF — виконується замість команди (таблиці + представлення)
  5. Комбінації подій — можна реагувати на INSERT, UPDATE, DELETE одночасно
  6. ROLLBACK — відміна змін при валідації
  7. Не для тимчасових таблиць — тригери НЕ можна створювати для #temp
  8. sys.triggers — метадані про тригери
Best Practices:
  • Тримайте тригери простими та швидкими
  • Уникайте складних запитів в тригерах (знижують performance)
  • Логуйте тільки необхідне
  • Пам'ятайте про масові операції (inserted/deleted можуть містити багато рядків)
  • Використовуйте constraints замість тригерів де можливо
Наступний крок: Вивчіть DDL-тригери для контролю змін структури БД.
Copyright © 2026