Triggers Stored Procedures

DDL-тригери

Контроль змін структури бази даних та події рівня сервера

DDL-тригери

Проблема: Як контролювати зміни структури БД?

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

DROP TABLE Customers;  -- Упс... 😱

Або молодший розробник створює таблиці без дотримання стандартів:

CREATE TABLE tbl_temp_data (...);  -- Погана назва!

Питання: Як автоматично відстежувати та контролювати такі операції? Як записувати в журнал всі зміни структури БД?

Loading diagram...
flowchart LR
    Dev[Розробник] -->|CREATE/ALTER/DROP TABLE| DB[(База Даних)]
    DB -->|1. Спрацьовує DDL-тригер| DDLTrigger[DDL Trigger]
    DDLTrigger -->|2. Перевіряє умови| Check{Дозволено?}
    Check -->|Так| Allow[Виконати операцію]
    Check -->|Ні| Deny[ROLLBACK + Помилка]
    DDLTrigger -->|3. Записати в журнал| AuditLog[Audit Log]
    
    style DB fill:#3b82f6,color:#fff
    style DDLTrigger fill:#f59e0b,color:#fff
    style Deny fill:#ef4444,color:#fff
    style Allow fill:#10b981,color:#fff

DDL-тригер — це тригер, який спрацьовує при виконанні DDL-операцій (Data Definition Language): CREATE, ALTER, DROP для таблиць, представлень, індексів, процедур тощо.

DDL (Data Definition Language) — операції зі структурою БД:
  • CREATE TABLE, ALTER TABLE, DROP TABLE
  • CREATE INDEX, DROP INDEX
  • CREATE PROCEDURE, ALTER PROCEDURE
  • GRANT, DENY, REVOKE (права доступу)
На відміну від DML-тригерів (які реагують на зміни даних: INSERT/UPDATE/DELETE), DDL-тригери реагують на зміни структури.

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

Захист від випадкових змін

Заборонити видалення або зміну критичних об'єктів БД.

Приклад: Заборонити DROP TABLE або ALTER TABLE для звичайних користувачів.

Аудит змін структури

Логувати всі DDL-операції: хто, коли, що змінив.

Приклад: Записувати в таблицю аудиту всі CREATE/ALTER/DROP команди.

Перевірка стандартів

Контролювати дотримання naming conventions та стандартів розробки.

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

Контроль версій БД

Інтеграція з системами версійного контролю для відстеження змін схеми.

Приклад: Автоматично створювати скрипт зміни і зберігати в git.


Області дії DDL-тригерів

DDL-тригери можуть діяти на двох рівнях:

Тригер спрацьовує при DDL-операціях в поточній базі даних.

CREATE TRIGGER trg_AuditDatabaseChanges
ON DATABASE  -- ← Рівень поточної БД
FOR CREATE_TABLE, ALTER_TABLE, DROP_TABLE
AS
BEGIN
    PRINT 'Зміна структури БД виявлена!';
END;

Використання:

  • Контроль змін таблиць, процедур, функцій в конкретній ОЗ
  • Аудит розробницьких змін
  • Валідація naming conventions

Де зберігається: В папці Programmability → Database Triggers в SSMS.

Loading diagram...
@startuml
skinparam style plain
skinparam defaultFontName "Segoe UI"

package "SQL Server Instance" {
    
    database "Database 1" as DB1 {
        rectangle "DDL Trigger\n(DATABASE scope)" as DDL_DB1 #LightGreen
    }
    
    database "Database 2" as DB2 {
        rectangle "DDL Trigger\n(DATABASE scope)" as DDL_DB2 #LightGreen
    }
    
    rectangle "DDL Trigger\n(ALL SERVER scope)" as DDL_SERVER #LightCoral
    rectangle "LOGON Trigger\n(ALL SERVER)" as DDL_LOGON #LightBlue
}

note right of DDL_SERVER
  Спрацьовує на будь-яку DDL-операцію
  в будь-якій БД на сервері
end note

note right of DDL_LOGON
  Спрацьовує при підключенні
  користувача до сервера
end note

@enduml

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

Для рівня DATABASE

CREATE TRIGGER trigger_name
ON DATABASE  -- Область дії: поточна БД
[WITH ENCRYPTION]
{FOR | AFTER} {event_name | event_group} [, ...n]
AS
BEGIN
    -- Тіло тригера
END;

Для рівня ALL SERVER

CREATE TRIGGER trigger_name
ON ALL SERVER  -- Область дії: весь сервер
[WITH ENCRYPTION]
{FOR | AFTER} {event_name | event_group} [, ...n]
AS
BEGIN
    -- Тіло тригера
END;

Для LOGON (вхід на сервер)

CREATE TRIGGER trigger_name
ON ALL SERVER
[WITH ENCRYPTION]
FOR LOGON  -- Подія входу користувача
AS
BEGIN
    -- Тіло тригера
END;

Анатомія:

  • ON DATABASE | ALL SERVER — область дії
  • FOR | AFTER — режим виконання (тільки AFTER для DDL, немає INSTEAD OF!)
  • event_name — конкретна подія, наприклад CREATE_TABLE, DROP_TABLE
  • event_group — група подій, наприклад DDL_TABLE_EVENTS (всі події таблиць)

Події та групи подій

Основні події

ПодіяОписПриклад команди
CREATE_TABLEСтворення таблиціCREATE TABLE ...
ALTER_TABLEЗміна таблиціALTER TABLE ...
DROP_TABLEВидалення таблиціDROP TABLE ...
CREATE_INDEXСтворення індексуCREATE INDEX ...
DROP_INDEXВидалення індексуDROP INDEX ...
CREATE_PROCEDUREСтворення процедуриCREATE PROCEDURE ...
ALTER_PROCEDUREЗміна процедуриALTER PROCEDURE ...
DROP_PROCEDUREВидалення процедуриDROP PROCEDURE ...
CREATE_DATABASEСтворення БД (SERVER!)CREATE DATABASE ...
DROP_DATABASEВидалення БД (SERVER!)DROP DATABASE ...
GRANT_DATABASEНадання прав (БД)GRANT SELECT ...
DENY_DATABASEЗаборона прав (БД)DENY DELETE ...

Групи подій

Групи дозволяють реагувати на кілька схожих подій одночасно:

Група подійВключає події
DDL_TABLE_EVENTSCREATE/ALTER/DROP TABLE
DDL_VIEW_EVENTSCREATE/ALTER/DROP VIEW
DDL_INDEX_EVENTSCREATE/ALTER/DROP INDEX
DDL_PROCEDURE_EVENTSCREATE/ALTER/DROP PROCEDURE
DDL_FUNCTION_EVENTSCREATE/ALTER/DROP FUNCTION
DDL_DATABASE_LEVEL_EVENTSВсі DDL-події рівня БД
DDL_SERVER_LEVEL_EVENTSВсі DDL-події рівня сервера
Рекомендація: Використовуйте групи для простоти коду.Замість:
FOR CREATE_TABLE, ALTER_TABLE, DROP_TABLE
Пишіть:
FOR DDL_TABLE_EVENTS

Функція EVENTDATA(): Отримання інформації про подію

DDL-тригери не мають таблиць INSERTED/DELETED. Замість цього використовується функція EVENTDATA(), яка повертає XML з інформацією про подію.

CREATE TRIGGER trg_LogDDLChanges
ON DATABASE
FOR DDL_DATABASE_LEVEL_EVENTS
AS
BEGIN
    DECLARE @eventData XML = EVENTDATA();
    
    SELECT 
        @eventData.value('(/EVENT_INSTANCE/EventType)[1]', 'NVARCHAR(100)') AS EventType,
        @eventData.value('(/EVENT_INSTANCE/ObjectName)[1]', 'NVARCHAR(256)') AS ObjectName,
        @eventData.value('(/EVENT_INSTANCE/LoginName)[1]', 'NVARCHAR(256)') AS LoginName,
        @eventData.value('(/EVENT_INSTANCE/TSQLCommand/CommandText)[1]', 'NVARCHAR(MAX)') AS CommandText;
END;

Структура XML від EVENTDATA():

<EVENT_INSTANCE>
  <EventType>CREATE_TABLE</EventType>
  <PostTime>2026-02-13T14:30:00.123</PostTime>
  <SPID>52</SPID>
  <ServerName>SQL-SERVER-01</ServerName>
  <LoginName>sa</LoginName>
  <UserName>dbo</UserName>
  <DatabaseName>BookStore</DatabaseName>
  <SchemaName>dbo</SchemaName>
  <ObjectName>NewTable</ObjectName>
  <ObjectType>TABLE</ObjectType>
  <TSQLCommand>
    <SetOptions .../>
    <CommandText>CREATE TABLE NewTable (Id INT);</CommandText>
  </TSQLCommand>
</EVENT_INSTANCE>

Корисні елементи XML:

  • EventType — тип події (CREATE_TABLE, DROP_PROCEDURE тощо)
  • ObjectName — назва об'єкта
  • LoginName — логін користувача, який виконав команду
  • CommandText — повний текст SQL-команди
  • PostTime — час виконання

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

Приклад 1: Заборона зміни та видалення таблиць

Задача: Заборонити всім користувачам (крім адміністратора) змінювати або видаляти таблиці.

CREATE TRIGGER trg_PreventTableModification
ON DATABASE
FOR ALTER_TABLE, DROP_TABLE
AS
BEGIN
    DECLARE @eventData XML = EVENTDATA();
    DECLARE @loginName NVARCHAR(256);
    DECLARE @objectName NVARCHAR(256);
    DECLARE @eventType NVARCHAR(100);
    
    -- Витягуємо інформацію з XML
    SELECT 
        @loginName = @eventData.value('(/EVENT_INSTANCE/LoginName)[1]', 'NVARCHAR(256)'),
        @objectName = @eventData.value('(/EVENT_INSTANCE/ObjectName)[1]', 'NVARCHAR(256)'),
        @eventType = @eventData.value('(/EVENT_INSTANCE/EventType)[1]', 'NVARCHAR(100)');
    
    -- Дозволяємо тільки адміністратору
    IF @loginName != 'sa' AND @loginName != 'BUILTIN\Administrators'
    BEGIN
        DECLARE @errorMsg NVARCHAR(500) = 
            'Модифікація та видалення таблиць заборонені. Зверніться до адміністратора.' +
            CHAR(13) + 'Спроба: ' + @eventType + ' для таблиці ' + @objectName;
        
        RAISERROR(@errorMsg, 16, 1);
        ROLLBACK;
    END
    ELSE
    BEGIN
        PRINT 'Операція дозволена для адміністратора';
    END
END;
GO

-- Тестуємо (якщо ви не sa):
ALTER TABLE book.Books ADD TestColumn INT;
-- Результат: "Модифікація та видалення таблиць заборонені..."

Пояснення:

  • Перевіряємо логін через EVENTDATA()
  • Якщо не адміністратор — викидаємо помилку і відміняємо операцію
  • ROLLBACK відміняє DDL-команду

Приклад 2: Аудит всіх DDL-змін

Задача: Записувати всі зміни структури БД в спеціальну таблицю аудиту.

-- Створюємо таблицю аудиту
CREATE TABLE dbo.DDLAuditLog (
    Id INT IDENTITY PRIMARY KEY,
    EventType NVARCHAR(100),
    ObjectName NVARCHAR(256),
    LoginName NVARCHAR(256),
    CommandText NVARCHAR(MAX),
    EventDate DATETIME DEFAULT GETDATE()
);
GO

-- Створюємо тригер для аудиту
CREATE TRIGGER trg_AuditDDLChanges
ON DATABASE
FOR DDL_DATABASE_LEVEL_EVENTS  -- Всі DDL-події в БД
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @eventData XML = EVENTDATA();
    
    INSERT INTO dbo.DDLAuditLog (EventType, ObjectName, LoginName, CommandText)
    SELECT
        @eventData.value('(/EVENT_INSTANCE/EventType)[1]', 'NVARCHAR(100)'),
        @eventData.value('(/EVENT_INSTANCE/ObjectName)[1]', 'NVARCHAR(256)'),
        @eventData.value('(/EVENT_INSTANCE/LoginName)[1]', 'NVARCHAR(256)'),
        @eventData.value('(/EVENT_INSTANCE/TSQLCommand/CommandText)[1]', 'NVARCHAR(MAX)');
END;
GO

-- Тестуємо:
CREATE TABLE dbo.TestTable (Id INT);
DROP TABLE dbo.TestTable;

-- Перевіряємо журнал:
SELECT * FROM dbo.DDLAuditLog ORDER BY EventDate DESC;

Результат в DDLAuditLog:

EventTypeObjectNameLoginNameCommandTextEventDate
DROP_TABLETestTablesaDROP TABLE dbo.TestTable;2026-02-13 14:35:12
CREATE_TABLETestTablesaCREATE TABLE dbo.TestTable (...);2026-02-13 14:35:10

Приклад 3: Контроль naming conventions

Задача: Заборонити створення таблиць з назвами, які починаються з tbl_ або temp_.

CREATE TRIGGER trg_EnforceNamingConventions
ON DATABASE
FOR CREATE_TABLE
AS
BEGIN
    DECLARE @eventData XML = EVENTDATA();
    DECLARE @tableName NVARCHAR(256);
    
    SELECT @tableName = @eventData.value('(/EVENT_INSTANCE/ObjectName)[1]', 'NVARCHAR(256)');
    
    -- Перевіряємо naming convention
    IF @tableName LIKE 'tbl[_]%' OR @tableName LIKE 'temp[_]%'
    BEGIN
        RAISERROR('Назви таблиць не повинні починатися з "tbl_" або "temp_". Використовуйте осмислені назви!', 16, 1);
        ROLLBACK;
    END
END;
GO

-- Тестуємо:
CREATE TABLE tbl_BadName (Id INT);
-- Результат: "Назви таблиць не повинні починатися з "tbl_"..."

CREATE TABLE Customers (Id INT);  -- ✅ Дозволено

Приклад 4: Захист від видалення БД (рівень сервера)

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

USE master;
GO

CREATE TRIGGER trg_PreventDatabaseDrop
ON ALL SERVER  -- Рівень сервера!
FOR DROP_DATABASE
AS
BEGIN
    DECLARE @eventData XML = EVENTDATA();
    DECLARE @dbName NVARCHAR(256);
    
    SELECT @dbName = @eventData.value('(/EVENT_INSTANCE/DatabaseName)[1]', 'NVARCHAR(256)');
    
    RAISERROR('Видалення бази даних "%s" заборонено на цьому сервері!', 16, 1, @dbName);
    ROLLBACK;
END;
GO

-- Тестуємо:
DROP DATABASE TestDB;
-- Результат: "Видалення бази даних "TestDB" заборонено..."
Важливо: DDL-тригери рівня сервера зберігаються в базі master і відображаються в Server Objects → Triggers в SSMS.

Приклад 5: LOGON тригер — обмеження підключень

Задача: Заборонити підключення користувачу guest_user в неробочий час (18:00-08:00).

USE master;
GO

CREATE TRIGGER trg_RestrictLoginTime
ON ALL SERVER
FOR LOGON
AS
BEGIN
    DECLARE @loginName NVARCHAR(256) = ORIGINAL_LOGIN();
    DECLARE @currentHour INT = DATEPART(HOUR, GETDATE());
    
    -- Перевіряємо час та користувача
    IF @loginName = 'guest_user' AND (@currentHour >= 18 OR @currentHour < 8)
    BEGIN
        RAISERROR('Підключення для користувача "guest_user" дозволено тільки з 08:00 до 18:00', 16, 1);
        ROLLBACK;
    END
END;
GO

Пояснення:

  • ORIGINAL_LOGIN() — повертає логін користувача, який підключається
  • DATEPART(HOUR, GETDATE()) — поточна година (0-23)
  • Якщо умова не виконується — підключення відхилено
Обережно з LOGON тригерами!Якщо тригер містить помилку і заборо ВСІ підключення (включно з адмінами), ви можете заблокувати себе на сервері!Порятунок: Підключитися через Dedicated Administrator Connection (DAC):
sqlcmd -S .\SQLSERVER -A -U sa -P password
DROP TRIGGER trg_RestrictLoginTime ON ALL SERVER;

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

Зміна тригера

-- Для рівня DATABASE
ALTER TRIGGER trg_AuditDDLChanges
ON DATABASE
FOR DDL_DATABASE_LEVEL_EVENTS
AS
BEGIN
    -- Новий код
END;

-- Для рівня ALL SERVER
ALTER TRIGGER trg_PreventDatabaseDrop
ON ALL SERVER
FOR DROP_DATABASE
AS
BEGIN
    -- Новий код
END;

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

-- Для рівня DATABASE
DISABLE TRIGGER trg_AuditDDLChanges ON DATABASE;

-- Для рівня ALL SERVER
DISABLE TRIGGER trg_PreventDatabaseDrop ON ALL SERVER;

-- Відключити ВСІ DDL-тригери БД
DISABLE TRIGGER ALL ON DATABASE;

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

ENABLE TRIGGER trg_AuditDDLChanges ON DATABASE;
ENABLE TRIGGER trg_PreventDatabaseDrop ON ALL SERVER;

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

-- Для рівня DATABASE
DROP TRIGGER trg_AuditDDLChanges ON DATABASE;

-- Для рівня ALL SERVER
DROP TRIGGER trg_PreventDatabaseDrop ON ALL SERVER;

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

-- Тригери рівня DATABASE
SELECT 
    name AS TriggerName,
    create_date AS CreatedDate,
    modify_date AS ModifiedDate,
    is_disabled AS IsDisabled
FROM sys.triggers
WHERE parent_class_desc = 'DATABASE';

-- Тригери рівня SERVER
SELECT 
    name AS TriggerName,
    create_date AS CreatedDate,
    modify_date AS ModifiedDate,
    is_disabled AS IsDisabled
FROM sys.server_triggers;

-- Отримати код тригера рівня DATABASE
SELECT m.definition
FROM sys.sql_modules m
INNER JOIN sys.triggers t ON m.object_id = t.object_id
WHERE t.name = 'trg_AuditDDLChanges'
  AND t.parent_class_desc = 'DATABASE';

-- Отримати код тригера рівня SERVER
SELECT m.definition
FROM sys.server_sql_modules m
INNER JOIN sys.server_triggers t ON m.object_id = t.object_id
WHERE t.name = 'trg_PreventDatabaseDrop';

Різниця між DML та DDL тригерами

ХарактеристикаDML-тригериDDL-тригери
ПодіїINSERT, UPDATE, DELETECREATE, ALTER, DROP, GRANT тощо
Об'єктДані в таблицяхСтруктура БД (таблиці, індекси...)
Області діїТільки таблиця/представленняDATABASE або ALL SERVER
INSTEAD OF✅ Підтримується❌ Не підтримується
INSERTED/DELETED✅ Таблиці доступні❌ Немає (використовується EVENTDATA)
EVENTDATA()❌ Немає✅ Повертає XML з інформацією
Use CaseВалідація даних, аудит DMLЗахист структури, аудит DDL

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


Резюме

Ключові моменти DDL-тригерів:
  1. Області дії: DATABASE (поточна БД) або ALL SERVER (весь сервер)
  2. Події: CREATE, ALTER, DROP для таблиць, процедур, БД тощо
  3. Групи подій: DDL_TABLE_EVENTS, DDL_DATABASE_LEVEL_EVENTS тощо
  4. EVENTDATA(): XML з інформацією про подію (замість INSERTED/DELETED)
  5. Тільки AFTER: INSTEAD OF не підтримується для DDL
  6. LOGON тригери: Спеціальний тип для контролю входу на сервер
  7. Захист: Використовуйте для захисту від випадкових змін структури
  8. Аудит: Логуйте всі DDL-операції для безпеки та compliance
Best Practices:
  • Використовуйте для критичних БД production
  • Логуйте DDL-зміни через EVENTDATA()
  • Обережно з LOGON тригерами (можна заблокувати сервер!)
  • Тестуйте в dev середовищі перед production
  • Документуйте всі DDL-тригери
Наступний крок: Вивчіть Transact-SQL розширення для написання складнішої логіки в тригерах.
Copyright © 2026