Triggers: Comparisons, New Features, and a Trick
I’ll show a chart which indicates the level of support for trigger features in major open-source DBMSs.
I’ll comment on new features in MySQL 5.7.
I’ll show how triggers can be used to abort statements which are taking too long.
Trigger features in major open-source DBMSs
Feature | Firebird | Ingres | MySQL+MariaDB | PostgreSQL |
---|---|---|---|---|
Any compound statement | YES | – | YES | – |
Alter | YES | – | – | – |
Disable | YES | – | – | YES |
For each statement | – | YES | – | YES |
For each row | YES | YES | YES | YES |
One trigger, multiple trigger events | YES | YES | – | YES |
Multiple triggers for same situation | YES | – | YES | |
Views’ “instead of” clause | – | – | – | YES |
Deferrable | – | – | – | YES |
Normally standardish syntax | YES | – | YES | YES |
“New” and “Old” variables | YES | YES | YES | YES |
“When” clause or equivalent | – | YES | – | YES |
Explanation of each column:
“Any compound statement”:
“YES” if trigger body can be something like BEGIN statement1;statement2; … END.
“-” if trigger body an only be something like a single CALL statement.
“Alter”:
“YES” if ALTER TRIGGER is legal and can do something significant (more than renaming). Non-standard.
“-” if the normal way to change a trigger is to drop and create again.
“Disable”:
“YES” if there is a persistent way to make a trigger inactive or disabled. Non-standard.
“-” if the normal way to disable a trigger is to drop it or add an IF clause.
“For each statement”:
“YES” if checking happens before/after rest of statement processing
“-” if checking is only for each row
“For each row”:
“YES” if checking happens before/after rest of row processing
“-” if checking is only for each statement
“One trigger, multiple trigger events”:
“YES” if clauses like “BEFORE INSERT OR UPDATE OR DELETE” are possible. Non-standard.
“-” if clause can only contain one verb (no OR allowed)
“Multiple triggers for same situation”:
“YES” if there can be (for example) two BEFORE UPDATE triggers on table t.
“-” if only one trigger is possible for the same action at the same time.
Views’ “instead of” clause:
“YES” if INSTEAD OF triggers are possible (usually for views only)
“-” if the only trigger action times are BEFORE and AFTER
“Deferrable”:
“YES” if it’s possible to defer “after” checking until end of transaction. Non-standard.
“-” if all checking is during statement execution
“Normally standardish syntax”:
“YES” if normal CREATE TRIGGER syntax is reminiscent of standard syntax
“-” if normal/recommended syntax is CREATE RULE, or normal body is EXECUTE clause.
“New” and “Old” variables:
“YES” if a trigger can use NEW.column_name and/or OLD.column_name
“-” if column values are invisible during trigger execution
“When” clause or equivalent:
“YES” if syntax is CREATE TRIGGER … [WHEN condition] …;
“-” if condition has to be in an IF statement in the trigger body
Information for this chart comes from the documentation for Firebird 1.5, Ingres 10.0, MySQL 5.6, and PostgreSQL 9.3.
Two New Features In MySQL 5.7
The first enhancement is that it is possible to say
CREATE TABLE t (s1 INT NOT NULL)// CREATE TRIGGER t1 BEFORE INSERT ON t FOR EACH ROW BEGIN IF new.s1 IS NULL THEN SET new.s1=0; END IF; END// INSERT INTO t VALUES (NULL)//
That is, NOT NULL no longer stops the INSERT if the trigger was going to change the value to non-null anyway. This was a long-standing feature request, and I doubt that anyone will complain.
The second enhancement is WL#3253: Triggers: multiple triggers per table.
In 5.6, if there are two actions to trigger, one must put them both in one trigger:
CREATE TRIGGER t1 BEFORE INSERT ON t FOR EACH ROW BEGIN statement1 statement2 END
In 5.7 one can put them in two triggers:
CREATE TRIGGER t1 BEFORE INSERT ON t FOR EACH ROW BEGIN statement1 END CREATE TRIGGER t2 BEFORE INSERT ON t FOR EACH ROW BEGIN statement2 END
This is unnecessary for a DBMS like MySQL that supports compound statements for triggers, but it’s in the standard, and there was at least one feature request for it. The problem with multiple triggers is that there has to be some way to decide which one is activated first, and the standard isn’t helpful here — it says to follow the order of creation, but that’s rigid, and also
unreliable if one depends on CURRENT_TIMESTAMP. PostgreSQL “solved” this by saying the order is the alphabetical order of the trigger names, but then Oracle 11g came along with a syntax that MySQL 5.7 copies:
CREATE TRIGGER t2 BEFORE INSERT ON t FOR EACH ROW FOLLOWS t1 ... or CREATE TRIGGER t2 BEFORE INSERT ON t FOR EACH ROW PRECEDES t1 ...
“FOLLOWS t1” means “is activated immediately after t1”, and in INFORMATION_SCHEMA.TRIGGERS there is an integer column ACTION_ORDER which reflects this (this column is not new, but in earlier MySQL versions it is always zero).
The feature works, and the worklog task for it has good high-level architecture description. But, once you’ve made trigger t2 follow t1, you’re stuck — there’s still no ALTER TRIGGER. And I think that, even if you know what ACTION_ORDER is, you’ll still get confused. To test that, here is a quiz. After
CREATE TABLE t (s1 CHAR(3)); CREATE TRIGGER t1 BEFORE UPDATE ON t FOR EACH ROW SET new.s1 = CONCAT(old.s1,'c'); CREATE TRIGGER t2 BEFORE UPDATE ON t FOR EACH ROW PRECEDES t1 SET new.s1 = CONCAT(new.s1,'b'); INSERT INTO t VALUES ('a'); UPDATE t SET s1 = '';
Does s1 contain ‘ab’ or ‘abc’ or ‘acb’ or ‘ac’? Answer at end of post. If you get it wrong, you should continue with the old way and put all actions in a single trigger; however, the coder (apparently Mr Dmitry Shulga) deserves congratulation.
Stopping an update after 10 seconds
Of course the legitimate uses of triggers are (a) to make up for MySQL’s lack of CHECK clauses by producing an error when a NEW value is wrong; (b) to add to a summary in table b whenever there’s a change to table a.
A less tame use — because it is non-deterministic — is aborting a statement if some external condition occurs. The example here is saying, in effect, “If 10 seconds have elapsed since the update began, stop and return an error”. Since the ordinary timers like CURRENT_TIMESTAMP don’t change during a statement, I use SYSDATE.
[ UPDATE: In an earlier version of this article, I used a low-level
counter from PERFORMANCE_SCHEMA. Thanks to Justin Swanhart for suggesting a simpler way. It won’t work if the server was started with –sysdate-is-now, but my original method could also have failed in some circumstances. ]
/* Make a test table with 25 rows. */ CREATE TABLE t (s1 INT); INSERT INTO t VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9),(10), (11),(12),(13),(14),(15),(16),(17),(18),(19),(20), (21),(22),(23),(24),(25); /* Make a trigger so (current time - start time) is checked after each row update, and SIGNAL if the result is more than 10. */ delimiter // CREATE TRIGGER abort_after_elapsed_time BEFORE UPDATE ON t FOR EACH ROW IF TO_SECONDS(SYSDATE()) - @start_time > 10 THEN SIGNAL SQLSTATE '57005' SET message_text='Timeout'; END IF;// delimiter ; /* Set a variable which will contain the current time. */ /* This should be done just before doing the UPDATE. */ SET @start_time = TO_SECONDS(SYSDATE()); /* Do a very slow UPDATE which would take (25 rows * 1 second per row) if no timeout. */ select current_timestamp; UPDATE t SET s1 = SLEEP(1); select current_timestamp;
Example result showing “Timeout” error occurs after about 10 seconds:
MariaDB [test]> select current_timestamp; +---------------------+ | current_timestamp | +---------------------+ | 2013-10-07 12:54:39 | +---------------------+ 1 row in set (0.00 sec) MariaDB [test]> UPDATE t SET s1 = SLEEP(1); ERROR 1644 (57005): Timeout MariaDB [test]> select current_timestamp; +---------------------+ | current_timestamp | +---------------------+ | 2013-10-07 12:54:50 | +---------------------+ 1 row in set (0.00 sec)
Your mileage may vary but by luck it worked this way on my machine. Probably a progress bar would be more interesting, eh? I’ll get back to that theme if I ever finish one.
Answer to quiz question
‘ac’.
Wouldn’t SYSDATE(6) work? That should change on each invocation and let you calculate the time with microsecond precision.
Yes, even without microsecond precision SYSDATE looks better than what I was using for the example in the “Stopping an update after 10 seconds” section. I’ve revised the posting, with a thank you.
This is also kind of weird, but you could probably:
select time
into @run_time
from information_schema.processlist where id = connection_id();