Oracle PL/SQL DDL Trigger T-SQL · Triggers · DDL Trigger

T-SQL DDL Triggers

Master T-SQL DDL triggers for schema-event auditing — CREATE/ALTER/DROP events, EVENTDATA() XML for accurate event details, ON DATABASE vs ON ALL SERVER scope, blocking dangerous operations with ROLLBACK, common event groups, and best practices.

A DDL trigger fires in response to a schema-changing event — anything that changes the structure of a database object rather than its data. The events include CREATE, ALTER, DROP (of tables, views, procedures, indexes, users, logins, etc.), plus GRANT, DENY, REVOKE, and a few others.

DDL triggers exist to audit, log, or block schema changes. They sit at one of two scopes:

ScopeCreated withCatches events from
DatabaseON DATABASEOnly the database in which the trigger lives
ServerON ALL SERVEREvery database on the SQL Server instance
CREATE [OR ALTER] TRIGGER trigger_name ON { DATABASE | ALL SERVER } [ WITH <ddl_trigger_option> [, ...n] ] FOR { event_type | event_group } [, ...n] AS BEGIN -- trigger body — typically uses EVENTDATA() END;
ElementMeaning
trigger_nameName for the new trigger
DATABASE / ALL SERVERScope — current database, or every database on the instance
event_typeSpecific event like CREATE_TABLE, DROP_PROCEDURE, ALTER_INDEX
event_groupGroup of related events like DDL_TABLE_EVENTS (covers CREATE/ALTER/DROP TABLE)
ddl_trigger_optionOptional settings — ENCRYPTION, EXECUTE AS

This is the most important function in DDL triggers. EVENTDATA() returns an XML document describing the event that fired the trigger — what happened, to which object, by which user, when, and the exact T-SQL command that ran.

⚠️ Don't use sys.objects to find the affected object. A common mistake (including in some online tutorials) is to write SELECT @name = name FROM sys.objects; in a DDL trigger to "get the table that was created." This returns an arbitrary row from the entire catalog and has nothing to do with the event that fired the trigger. The correct way is always EVENTDATA().

Useful XPath expressions inside the EVENTDATA XML:

XPathWhat it returns
/EVENT_INSTANCE/EventTypeThe event name (e.g. CREATE_TABLE)
/EVENT_INSTANCE/ObjectNameThe object that was changed
/EVENT_INSTANCE/SchemaNameIts schema
/EVENT_INSTANCE/LoginNameThe login that issued the command
/EVENT_INSTANCE/PostTimeWhen it happened
/EVENT_INSTANCE/TSQLCommand/CommandTextThe exact T-SQL text that fired the event
Example 1 — Audit Schema Changes

Build a simple log that records every CREATE / ALTER / DROP of a table in the current database.

SQL — Step 1: log table
CREATE TABLE TableChangeLog (
    ChangeID    INT IDENTITY(1,1) PRIMARY KEY,
    EventType   NVARCHAR(50),
    ObjectName  NVARCHAR(255),
    LoginName   NVARCHAR(128),
    ChangeTime  DATETIME2 DEFAULT SYSDATETIME(),
    TSQLCommand NVARCHAR(MAX)
);
T-SQL — Step 2: the DDL trigger
CREATE OR ALTER TRIGGER LogTableChanges
ON DATABASE
FOR CREATE_TABLE, ALTER_TABLE, DROP_TABLE
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @data XML = EVENTDATA();

    INSERT INTO TableChangeLog (EventType, ObjectName, LoginName, TSQLCommand)
    VALUES (
        @data.value('(/EVENT_INSTANCE/EventType)[1]',                   'NVARCHAR(50)'),
        @data.value('(/EVENT_INSTANCE/ObjectName)[1]',                  'NVARCHAR(255)'),
        @data.value('(/EVENT_INSTANCE/LoginName)[1]',                   'NVARCHAR(128)'),
        @data.value('(/EVENT_INSTANCE/TSQLCommand/CommandText)[1]',     'NVARCHAR(MAX)')
    );
END;
GO
📌 What changed from a typical online example. Many tutorials show a DDL trigger with SELECT @ObjectName = name FROM sys.objects — that's broken. EVENTDATA() is the only reliable way to know which object the event was about, and it's how Microsoft's documentation does it.

Now test the trigger by making schema changes:

T-SQL — Step 3: trigger the trigger
CREATE TABLE TestTable (id INT PRIMARY KEY, name NVARCHAR(50));

ALTER TABLE TestTable ADD active BIT DEFAULT 1;

DROP TABLE TestTable;
OUTPUT
Messages
Commands completed successfully.
Commands completed successfully.

Inspect the log:

T-SQL — View the log
SELECT ChangeID, EventType, ObjectName, LoginName, ChangeTime
FROM   TableChangeLog
ORDER BY ChangeID;
OUTPUT
Results
(3 rows affected)
ChangeID EventType ObjectName LoginName ChangeTime
──────── ──────────── ────────── ────────── ───────────────────────
1 CREATE_TABLE TestTable sa 2026-05-03 14:23:05.123
2 ALTER_TABLE TestTable sa 2026-05-03 14:23:05.456
3 DROP_TABLE TestTable sa 2026-05-03 14:23:05.789
Example 2 — Block a Dangerous Operation

DDL triggers can also roll back the event that fired them — a great way to enforce policies. This trigger refuses to drop any table:

T-SQL — Refuse DROP TABLE
CREATE OR ALTER TRIGGER PreventTableDrops
ON DATABASE
FOR DROP_TABLE
AS
BEGIN
    PRINT 'DROP TABLE is disabled. Disable this trigger first.';
    ROLLBACK;
END;
GO

-- Try to drop a table
DROP TABLE TestTable;
OUTPUT
Messages
Msg 3609, Level 16, State 2, Line 1
DROP TABLE is disabled. Disable this trigger first.
Msg 3609, Level 16, State 2, Line 1
The transaction ended in the trigger. The batch has been aborted.

The DROP didn't happen — ROLLBACK inside a DDL trigger undoes the event that fired it.

Example 3 — Server-Level DDL Trigger

To catch events across every database on the instance, use ON ALL SERVER. Useful for tracking new database creation, login changes, etc.

T-SQL — Audit database creation across all DBs
USE master;
GO

CREATE OR ALTER TRIGGER AuditDatabaseCreation
ON ALL SERVER
FOR CREATE_DATABASE
AS
BEGIN
    DECLARE @data XML = EVENTDATA();
    PRINT 'New database created: ' +
          @data.value('(/EVENT_INSTANCE/DatabaseName)[1]', 'NVARCHAR(128)') +
          ' by ' + @data.value('(/EVENT_INSTANCE/LoginName)[1]', 'NVARCHAR(128)');
END;
GO
⚠️ Permissions matter. Server-level DDL triggers need CONTROL SERVER permission to create. Database-level DDL triggers need ALTER ANY DATABASE DDL TRIGGER in the database.
Event groupCovers
DDL_TABLE_EVENTSCREATE_TABLE, ALTER_TABLE, DROP_TABLE
DDL_INDEX_EVENTSCREATE/ALTER/DROP INDEX
DDL_PROCEDURE_EVENTSCREATE/ALTER/DROP PROCEDURE
DDL_LOGIN_EVENTSCREATE/ALTER/DROP LOGIN (server scope)
DDL_DATABASE_LEVEL_EVENTSEvery event in the current database
DDL_SERVER_LEVEL_EVENTSEvery event on the instance

Using a group instead of listing individual events keeps the trigger working as Microsoft adds new event types.

T-SQL — Find them
-- Database-level DDL triggers
SELECT name, parent_class_desc, is_disabled
FROM   sys.triggers
WHERE  parent_class = 0;             -- 0 = database

-- Server-level DDL triggers
SELECT name, parent_class_desc, is_disabled
FROM   sys.server_triggers;
  • DDL triggers fire on schema-changing events — CREATE, ALTER, DROP, GRANT etc.
  • Two scopes: ON DATABASE for events in one database, ON ALL SERVER for events across the entire instance.
  • Always use EVENTDATA() to find out what happened — never sys.objects without context. EVENTDATA returns XML you query with .value(xpath, type).
  • ROLLBACK inside a DDL trigger undoes the event — useful for blocking unwanted schema changes.
  • Use event groups like DDL_TABLE_EVENTS to cover related events; otherwise list specific events like CREATE_TABLE.
  • Server-level DDL triggers live in master and need CONTROL SERVER permission to create.