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

Posted

技术标签:

【中文标题】仅当行已更改时,MySQL 才在更新后触发【英文标题】:MySQL Trigger after update only if row has changed 【发布时间】:2011-09-11 21:13:24 【问题描述】:

是否有可能仅在数据已真正更改的情况下使用“更新后”触发器。 我知道“新旧”。但是当使用它们时,我只能比较列。 例如“NEW.count OLD.count”。

但我想要类似的东西:运行触发器 if "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 |
+------+------+

关键是,有更新,但没有任何变化。 但是触发器还是跑了。恕我直言,应该有一种方法不会。

我知道我可以使用

如果现在.b 旧.b

对于这个例子。

但是想象一个包含不断变化的列的大表。 您必须比较每一列,如果数据库发生更改,您必须调整触发器。 并且比较硬编码的行的每一列并不“感觉”好:)

加法

正如你在网上看到的那样

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

mysql 知道该行没有改变。但它不与触发器共享此知识。 像“AFTER REAL UPDATE”之类的触发器或类似的东西会很酷。

【问题讨论】:

如果列发生变化,无论如何您都需要调整触发器,因为它会插入INSERT INTO bar VALUES(NEW.a, NEW.b);。有什么办法可以避免吗?像INSERT INTO bar VALUES(SELECT * FROM foo WHERE…); 这样的东西会起作用吗? @juwens,这个问题你应该得到 10+。令人难以置信的是,他们怎么能如此反直觉地建立这种行为! @zcat,不,您不一定需要在每次表更改时调整触发器。你可以做INSERT INTO bar, SELECT * FROM foo WHERE foo.id = OLD.id,没问题。 附带说明一下,Postgres 的行为也是如此,但您可以在创建触发器语句中添加 WHEN NEW.* IS DISTINCT FROM OLD.* 条件,因此 MySQL 应该实现类似的东西... 【参考方案1】:

作为一种解决方法,您可以使用时间戳(旧的和新的)来检查,当行没有更改时,时间戳是否更新。 (这可能是混淆的根源?因为那个也称为“更新时”,但在没有发生变化时不执行) 一秒钟内的更改将不会执行触发器的那部分,但在某些情况下可能没问题(例如当您有一个应用程序拒绝快速更改时)。

例如,而不是

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

你可以使用

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

那么您不必每次更新方案时都更改触发器(您在问题中提到的问题。)

编辑:添加完整示例

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)

由于 mysql 在处理时间戳方面的行为,它正在工作。 仅当更新发生更改时才会更新时间戳。

文档在这里: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 |
+-------+-----------+------+-----+-------------------+-----------------------------+

【讨论】:

我不明白这是怎么回事。您能否更详细地解释一下您的意思。 @derkommissar:我添加了一个例子 有参考链接吗 如果更新频率低于一秒,这将不起作用。时间戳将更改(但更改为相同的值)。您必须使用足够准确的时间戳(6)来跟踪所有更新 正如我在下面的回答中,不要忘记使用 (null 感知运算符)来跟踪从 null 到返回到 null 的更改【参考方案2】:

但是想象一个包含不断变化的列的大表。您必须比较每一列,如果数据库发生更改,您必须调整触发器。并且比较硬编码的每一行并不“感觉”好:)

是的,但这是继续的方式。

附带说明一下,在更新之前预先检查也是一种很好的做法:

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

在您的示例中,这将使其更新(因此覆盖两行行而不是三行。

【讨论】:

@Denis,这不需要,MySQL检查该值是否真的改变了,如果需要,只启动UPDATE(更新+触发器) .你的检查只会让事情变慢。 @Johan:这是必需的,而 MySQL 不这样做。如果这样做,它就不会尊重 SQL 标准——而且 OP 一开始就不会问他的问题。 @Denis,是的,我在 5.0 和 5.5 中使用我自己的测试触发器进行了检查,它确实做到了。真烦人。 这并不烦人,这很正常,而且实际上是可取的:有时,在更新时触发并不管是否发生更改都会触发它是有用的。间接后果是,如果没有实际发生更改,则由开发人员(或他的 ORM,即使后者从来没有做得最好)来决定是否更新。 @Yohan:你可能会觉得this discussion很有趣。【参考方案3】:

我无法评论,所以请注意,如果您的列支持 NULL 值,OLD.xNEW.x 是不够的,因为

SELECT IF(1&lt;&gt;NULL,1,0)

返回 0 与相同

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

所以它不会跟踪 FROM 和 TO NULL 的变化

这个场景下正确的做法是

((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))

【讨论】:

感谢您的宝贵回答/提示!这使得时间戳解决方案更具吸引力,而比较解决方案实际上无法使用。 或者你可以使用COALESCE(),它返回不是NULL的第一个参数。所以你可以写成IF COALESCE(OLD.X,'') &lt;&gt; COALESCE(NEW.X,'') 或者干脆使用mysql空感知比较运算符&lt;=&gt;【参考方案4】:

您可以通过使用NULL-safe equals operator &lt;=&gt; 和negating the result using NOT 比较每个字段来做到这一点。

完整的触发器将变为:

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 ;

(基于different answer of mine。)

【讨论】:

【参考方案5】:

如果有任何行影响新插入,那么它将在数据库中的不同表上更新。

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 ;

【讨论】:

【参考方案6】:

使用以下查询查看哪些行发生了变化:

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

这个查询的结果应该包含所有不同于旧记录的新记录。

【讨论】:

遗憾的是,这并不能回答问题。更新不会删除行,并且如果更新未更改数据,则 OP 会尝试停止触发器触发(将值为 1 的列更新为值 1 不会更改任何数据,但更新操作仍在运行,因此触发器也是。) 据我了解,UPDATE 查询会将旧值放入已删除的记录集中,将新值放入插入的记录集中。虽然这不会阻止触发器运行,但它可用于阻止触发器执行任何操作,这通常就足够了。 用扩展代码来说明你的答案可能是值得的。但是,请记住,这个问题也有 5 年历史了。 这里引用Microsoft的一段话 更新事务类似于删除操作后跟插入操作;旧行先复制到已删除表中,然后将新行复制到触发器表和插入表中。” 示例代码:在 foo INSERT INTO bar VALUES 更新后创建触发器 ins_sum(从插入中选择 *,从删除中选择 * 除外);【参考方案7】:
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

【讨论】:

【参考方案8】:

这里有两个有趣的死胡同(从 MySQL 5.7 开始)-

    new.*old.* 构造无效,MySQL 抱怨 Unknown table 'new'syntax to use near '*,这排除了类似的技巧 select ... from (select (select new.* union select old.*)a having count(*)=2) has_change

    documentation for "ROW_COUNT()" 有一个有用的线索-

对于 UPDATE 语句,affected-rows 值默认是实际更改的行数

确实,更新语句之后,ROW_COUNT() 正确显示了更新后发生更改的行数。但是,更新期间,在触发器内部,ROW_COUNT() = 0 总是。该函数在行级触发器中没有用处,并且截至本答案,MySQL 中没有语句级触发器。

希望这个“空结果”能避免未来的挫败感。

【讨论】:

以上是关于仅当行已更改时,MySQL 才在更新后触发的主要内容,如果未能解决你的问题,请参考以下文章

触发器 - 如何在更新另一列时更改另一列 - MySQL

仅当所有文本字段都已填写时才在 Swift 中启用按钮

Mysql 2 在同一个触发器中更新

更新事件后使用mysql触发器备份行

仅当条件为 True 时才在 python 中使用 Eel 调用 JavaScript 函数

MySQL数据类型 - 日期和时间类型