sql - 如何在 PostgreSQL 中编写 upsert 触发器?

标签 sql postgresql upsert

在 PostgreSQL 9.6 及更高版本中,定义将执行一个触发器函数的正确方法是什么? 每当插入由于唯一性约束而失败时更新?

我知道编写 insert ... on conflict ... do update set ... 语句很简单,但是我的 想法是我想要一些表将重复插入视为更新;否则那件 逻辑必须由应用程序而不是数据库来处理。

我找到的一个表面上有效的解决方案是:

create table versions (
  key           text primary key,
  version       text );

/* ### TAINT not sure whether there may be race conditions with this upsert trigger */
create function on_before_insert_versions() returns trigger language plpgsql volatile as $$ begin
  if exists ( select 1 from versions where key = new.key ) then
    update versions set version = new.version where key = new.key;
    return null;
    end if;
  return new;
  end; $$;

create trigger on_before_insert_versions
  before insert on versions for each row execute procedure on_before_insert_versions();

insert into versions values
  ( 'server', '3.0.3' ),
  ( 'api',    '2' );

insert into versions values
  ( 'api',    '3' );

select * from versions;

  key   | version 
--------+---------
 server | 3.0.3
 api    | 3

但是,触发器是否容易出现竞争条件?我试着用一个 insert ... on conflict ... do update set ... 触发器中的语句,但当然失败了 因为它自己触发触发函数,导致无限倒退。

我还尝试使用一对 alter table ... disable trigger .../enable 语句,但是 错误 cannot ALTER TABLE ... because it is being used by active queries in this session

在唯一性约束下始终执行更新而不是插入的规范形式是什么 违反 PostgreSQL?

更新——PostgreSQL 中的更新,或者它们的长期缺席,在某种程度上是一个热门话题,并且经常建议许多不那么完美的解决方案。

考虑到 Postgres 的维护者已经花费了如此多的时间和精力来使 insert ... on conflict .. do update 在没有竞争条件的情况下工作,拥抱一个自制的解决方案可能是不明智的似乎工作'(直到它不工作)。

当我写下我的问题时,我坚持要有一个 insert 触发器,它在发生冲突时执行 update; PostgreSQL 对此没有很好的支持,主要问题是您在 before insert 触发器中对同一表执行的 insert 将导致调用同一触发器。 @Laurenz Albe 建议如何摆脱无限循环,虽然建议的技术(巧妙!)看起来是一件值得记住的好事,但我们不知道对性能或其他副作用的可能影响。

最后,@Ilya Dyoshin 提议只从应用程序中调用一个包含必要 SQL 逻辑的函数,一针见血。我觉得这是一个双赢的解决方案,因为

1) 它不会将 insert into x for table x 的语义更改为“really mean update, sometimes”;

2) “upsert 语义”在应用程序代码中是明确的,但没有详细说明;

3) 您可以仍然执行插入而不打算进行隐式“更新”——事后看来,这可能是最重要的考虑因素。

最佳答案

我同意 Ilya 的观点,即在应用程序中以直接的方式执行此操作会更好。

但我本着思想实验的精神接受它,我的解决方案使用 pg_trigger_depth() 的强大功能来逃避无休止的递归:

CREATE OR REPLACE FUNCTION on_before_insert_versions() RETURNS trigger
   LANGUAGE plpgsql AS
$$BEGIN
   IF pg_trigger_depth() = 1 THEN
      INSERT INTO versions (key, version) VALUES (NEW.key, NEW.version)
         ON CONFLICT (key)
         DO UPDATE SET version = NEW.version;
      RETURN NULL;
   ELSE
      RETURN NEW;
   END IF;
END;$$;

您的解决方案绝对容易受到竞争条件的影响:两个并发的 INSERT 可能导致同时运行触发器,这两个触发器都无法在 versions 中找到匹配的行,因此导致一个 INSERT,其中一个一定会失败。

关于sql - 如何在 PostgreSQL 中编写 upsert 触发器?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47562240/

相关文章:

mysql - mysql 5.7中JSON数据类型列中的嵌套更新查询

sql - 如何在databricks sql中将字符串转换为日期

postgresql - 如何在同一张表的 GROUP 内执行 LIMIT?

postgresql - Now() 没有时区

sql - 在 sql (Firebird/MySql) 中更新/插入记录的正确/最快的方法是什么

java.sql.SQLException : Lock wait timeout exceeded; try restarting transaction exception occur in MYSQL 异常

sql - 查询 SQL Server 时出错

sql - 联合表 SQL 的更快方法

sql - Oracle PLS-00103 错误。您如何检查现有记录并根据该条件进行更新或插入?

sqlalchemy - 批量插入 sqlalchemy 核心与冲突更新