更新后仅当行已更改时才触发 MySQL 触发器

是否有任何可能性使用“更新后”触发器,只有在情况下的数据已经真正改变。 我知道“新的和旧的”,但是当我使用它们的时候,我只能比较栏目。 例如“ NEW.count < > OLD.count”。

但是我想要类似于: 运行触发器,如果“ NEW < > OLD”

一个例子:

create table foo (a INT, b INT);
create table bar (a INT, b INT);


INSERT INTO foo VALUES(1,1);
INSERT INTO foo VALUES(2,2);
INSERT INTO foo VALUES(3,3);


CREATE TRIGGER ins_sum
AFTER UPDATE ON foo
FOR EACH ROW
INSERT INTO bar VALUES(NEW.a, NEW.b);


UPDATE foo SET b = 3 WHERE a=3;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 1  Changed: 0  Warnings: 0




select * from bar;
+------+------+
| a    | b    |
+------+------+
|    3 |    3 |
+------+------+

重点是,有一个更新,但 什么都没变。 但是触发器还是启动了,恕我直言,应该有办法不让它启动。

我知道我本可以

IF NOW.b < > OLD.b

这个例子。

但是,想象一个具有更改列的大型表。 必须比较每一列,如果数据库发生变化,则必须调整触发器。 而且,比较硬编码行中的每一列“感觉”不太好:)

加法

你可以在线上看到

匹配的行: 1更改: 0警告: 0

MySQL 知道这条线没有改变,但是它并没有和触发器分享这些信息。 像“ AFTER REAL UPDATE”之类的触发器会很酷。

196599 次浏览

BUT imagine a large table with changing columns. You have to compare every column and if the database changes you have to adjust the trigger. AND it doesn't "feel" good to compare every row hardcoded :)

Yeah, but that's the way to proceed.

As a side note, it's also good practice to pre-emptively check before updating:

UPDATE foo SET b = 3 WHERE a=3 and b <> 3;

In your example this would make it update (and thus overwrite) two rows instead of three.

As a workaround, you could use the timestamp (old and new) for checking though, that one is not updated when there are no changes to the row. (Possibly that is the source for confusion? Because that one is also called 'on update' but is not executed when no change occurs) Changes within one second will then not execute that part of the trigger, but in some cases that could be fine (like when you have an application that rejects fast changes anyway.)

For example, rather than

IF NEW.a <> OLD.a or NEW.b <> OLD.b /* etc, all the way to NEW.z <> OLD.z */
THEN
INSERT INTO bar (a, b) VALUES(NEW.a, NEW.b) ;
END IF

you could use

IF NEW.ts <> OLD.ts
THEN
INSERT INTO bar (a, b) VALUES(NEW.a, NEW.b) ;
END IF

Then you don't have to change your trigger every time you update the scheme (the issue you mentioned in the question.)

EDIT: Added full example

create table foo (a INT, b INT, ts TIMESTAMP);
create table bar (a INT, b INT);


INSERT INTO foo (a,b) VALUES(1,1);
INSERT INTO foo (a,b) VALUES(2,2);
INSERT INTO foo (a,b) VALUES(3,3);


DELIMITER ///


CREATE TRIGGER ins_sum AFTER UPDATE ON foo
FOR EACH ROW
BEGIN
IF NEW.ts <> OLD.ts THEN
INSERT INTO bar (a, b) VALUES(NEW.a, NEW.b);
END IF;
END;
///


DELIMITER ;


select * from foo;
+------+------+---------------------+
| a    | b    | ts                  |
+------+------+---------------------+
|    1 |    1 | 2011-06-14 09:29:46 |
|    2 |    2 | 2011-06-14 09:29:46 |
|    3 |    3 | 2011-06-14 09:29:46 |
+------+------+---------------------+
3 rows in set (0.00 sec)


-- UPDATE without change
UPDATE foo SET b = 3 WHERE a = 3;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 1  Changed: 0  Warnings: 0


-- the timestamo didnt change
select * from foo WHERE a = 3;
+------+------+---------------------+
| a    | b    | ts                  |
+------+------+---------------------+
|    3 |    3 | 2011-06-14 09:29:46 |
+------+------+---------------------+
1 rows in set (0.00 sec)


-- the trigger didn't run
select * from bar;
Empty set (0.00 sec)


-- UPDATE with change
UPDATE foo SET b = 4 WHERE a=3;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0


-- the timestamp changed
select * from foo;
+------+------+---------------------+
| a    | b    | ts                  |
+------+------+---------------------+
|    1 |    1 | 2011-06-14 09:29:46 |
|    2 |    2 | 2011-06-14 09:29:46 |
|    3 |    4 | 2011-06-14 09:34:59 |
+------+------+---------------------+
3 rows in set (0.00 sec)


-- and the trigger ran
select * from bar;
+------+------+---------------------+
| a    | b    | ts                  |
+------+------+---------------------+
|    3 |    4 | 2011-06-14 09:34:59 |
+------+------+---------------------+
1 row in set (0.00 sec)

It is working because of mysql's behavior on handling timestamps. The time stamp is only updated if a change occured in the updates.

Documentation is here:
https://dev.mysql.com/doc/refman/5.7/en/timestamp-initialization.html

desc foo;
+-------+-----------+------+-----+-------------------+-----------------------------+
| Field | Type      | Null | Key | Default           | Extra                       |
+-------+-----------+------+-----+-------------------+-----------------------------+
| a     | int(11)   | YES  |     | NULL              |                             |
| b     | int(11)   | YES  |     | NULL              |                             |
| ts    | timestamp | NO   |     | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
+-------+-----------+------+-----+-------------------+-----------------------------+

I cant comment, so just beware, that if your column supports NULL values, OLD.x<>NEW.x isnt enough, because

SELECT IF(1<>NULL,1,0)

returns 0 as same as

NULL<>NULL 1<>NULL 0<>NULL 'AAA'<>NULL

So it will not track changes FROM and TO NULL

The correct way in this scenario is

((OLD.x IS NULL AND NEW.x IS NOT NULL) OR (OLD.x IS NOT NULL AND NEW.x IS NULL) OR (OLD.x<>NEW.x))

You can do this by comparing each field using the NULL-safe equals operator <=> and then negating the result using NOT.

The complete trigger would become:

DROP TRIGGER IF EXISTS `my_trigger_name`;


DELIMITER $$


CREATE TRIGGER `my_trigger_name` AFTER UPDATE ON `my_table_name` FOR EACH ROW
BEGIN
/*Add any fields you want to compare here*/
IF !(OLD.a <=> NEW.a AND OLD.b <=> NEW.b) THEN
INSERT INTO `my_other_table` (
`a`,
`b`
) VALUES (
NEW.`a`,
NEW.`b`
);
END IF;
END;$$


DELIMITER ;

(Based on a different answer of mine.)

In here if there any row affect with new insertion Then it will update on different table in the database.

DELIMITER $$


CREATE TRIGGER "give trigger name" AFTER INSERT ON "table name"
FOR EACH ROW
BEGIN
INSERT INTO "give table name you want to add the new insertion on previously given table" (id,name,age) VALUES (10,"sumith",24);
END;
$$
DELIMITER ;
MYSQL TRIGGER BEFORE UPDATE IF OLD.a<>NEW.b


USE `pdvsa_ent_aycg`;


DELIMITER $$


CREATE TRIGGER `cisterna_BUPD` BEFORE UPDATE ON `cisterna` FOR EACH ROW


BEGIN


IF OLD.id_cisterna_estado<>NEW.id_cisterna_estado OR OLD.observacion_cisterna_estado<>NEW.observacion_cisterna_estado OR OLD.fecha_cisterna_estado<>NEW.fecha_cisterna_estado


THEN


INSERT INTO cisterna_estado_modificaciones(nro_cisterna_estado, id_cisterna_estado, observacion_cisterna_estado, fecha_cisterna_estado) values (NULL, OLD.id_cisterna_estado, OLD.observacion_cisterna_estado, OLD.fecha_cisterna_estado);


END IF;


END

Use the following query to see which rows have changes:

(select * from inserted) except (select * from deleted)

The results of this query should consist of all the new records that are different from the old ones.

Here are two interesting dead ends (as of MySQL 5.7)-

  1. The new.* and old.* constructs are invalid, MySQL complains about Unknown table 'new' or syntax to use near '*, which precludes tricks like select ... from (select (select new.* union select old.*)a having count(*)=2) has_change

  2. The documentation for "ROW_COUNT()" has a useful clue-

For UPDATE statements, the affected-rows value by default is the number of rows actually changed

And indeed, after an update statement, ROW_COUNT() correctly shows the count of rows that had changes from the update. However, during the update, inside the trigger, ROW_COUNT() = 0 always. That function has no useful value in a row-level trigger, and there's no statement-level trigger in MySQL as of this answer.

Hope this "null result" prevents future frustration.