插入触发器最终在分区表中插入重复的行

Posted

技术标签:

【中文标题】插入触发器最终在分区表中插入重复的行【英文标题】:Insert trigger ends up inserting duplicate rows in partitioned table 【发布时间】:2014-02-25 21:15:34 【问题描述】:

我有一个分区表(我认为)有适当的INSERT 触发器和一些约束。不知何故,INSERT 语句为每个INSERT 插入 2 行:1 行用于父级,1 行用于相应分区。

简单的设置如下:

CREATE TABLE foo (
id SERIAL NOT NULL,
d_id INTEGER NOT NULL,
label VARCHAR(4) NOT NULL);

CREATE TABLE foo_0 (CHECK (d_id % 2 = 0)) INHERITS (foo);
CREATE TABLE foo_1 (CHECK (d_id % 2 = 1)) INHERITS (foo);

ALTER TABLE ONLY foo ADD CONSTRAINT foo_pkey PRIMARY KEY (id);
ALTER TABLE ONLY foo_0 ADD CONSTRAINT foo_0_pkey PRIMARY KEY (id);
ALTER TABLE ONLY foo_1 ADD CONSTRAINT foo_1_pkey PRIMARY KEY (id);

ALTER TABLE ONLY foo ADD CONSTRAINT foo_d_id_key UNIQUE (d_id, label);
ALTER TABLE ONLY foo_0 ADD CONSTRAINT foo_0_d_id_key UNIQUE (d_id, label);
ALTER TABLE ONLY foo_1 ADD CONSTRAINT foo_1_d_id_key UNIQUE (d_id, label);

CREATE OR REPLACE FUNCTION foo_insert_trigger()
RETURNS TRIGGER AS $$
BEGIN
    IF NEW.id IS NULL THEN
       NEW.id := nextval('foo_id_seq');
    END IF;

    EXECUTE 'INSERT INTO foo_' || (NEW.d_id % 2) || ' SELECT $1.*' USING NEW;
    RETURN NEW;
END
$$
LANGUAGE plpgsql;

CREATE TRIGGER insert_foo_trigger
    BEFORE INSERT ON foo
    FOR EACH ROW EXECUTE PROCEDURE foo_insert_trigger();

在进一步调试后,我隔离了导致它的原因:INSERT 触发器返回 NEW 而不是 NULL。但是我确实希望我的插入语句返回自动增量id,如果我只返回NULL,情况就不会如此。

解决办法是什么?为什么返回NEW 会导致这种看似“奇怪”​​的行为?

更新 #1

嗯,我知道为什么行被插入两次,从触发器的文档中可以清楚地看出:

每个语句触发器调用的触发器函数应始终 返回 NULL。每行触发器调用的触发器函数可以返回 调用执行程序的表行(HeapTuple 类型的值),如果 他们选择。在操作之前触发的行级触发器 以下选择:

它可以返回NULL来跳过当前行的操作。这 指示执行程序不执行行级操作 调用触发器(插入、修改或删除 特定的表格行)。

仅对于行级 INSERT 和 UPDATE 触发器,返回的行 成为将被插入或将替换正在被插入的行的行 更新。这允许触发器函数修改正在执行的行 插入或更新。

但我的问题仍然是如何不返回 NEW 并且仍然能够获得自动递增的 idROW_COUNT 例如?

更新 #2

我找到了解决方案,但我当然希望有更好的解决方案。基本上,您可以添加一个AFTER TRIGGER 来删除插入到父表中的行。这似乎非常低效,所以如果有人有更好的解决方案,请发布!

供参考的解决方案是:

CREATE TRIGGER insert_foo_trigger
    BEFORE INSERT ON foo
    FOR EACH ROW EXECUTE PROCEDURE foo_insert_trigger();


CREATE OR REPLACE FUNCTION foo_delete_master() 
RETURNS TRIGGER AS $$
BEGIN
    DELETE FROM ONLY foo WHERE id = NEW.id;
    RETURN NEW;
END
$$
LANGUAGE plpgsql;

CREATE TRIGGER after_insert_foo_trigger
    AFTER INSERT ON foo
    FOR EACH ROW EXECUTE PROCEDURE foo_delete_master();

【问题讨论】:

这是 PostgreSQL 基于继承的分区的一个真正缺陷。几个之一;另一个是在插入/更新/从分区表中删除时,您没有得到正确的返回行数。 【参考方案1】:

更简单的方法是创建存储过程而不是触发器,例如 add_foo( [parameters] ),它将决定哪个分区适合插入行并返回 id(或新记录值,包括 id) .例如:

CREATE OR REPLACE FUNCTION add_foo(
    _d_id   INTEGER
,   _label  VARCHAR(4)
) RETURNS BIGINT AS $$
DECLARE
    _rec    foo%ROWTYPE;
BEGIN
    _rec.id := nextval('foo_id_seq');
    _rec.d_id := _d_id;
    _rec.label := _label;
    EXECUTE 'INSERT INTO foo_' || ( _d_id % 2 ) || ' SELECT $1.*' USING _rec;
    RETURN _rec.id;
END $$ LANGUAGE plpgsql;

【讨论】:

谢谢托马斯。第二种解决方案(函数)是一种更有效的方法,但存在维护问题。也就是说,每次更改或添加到表中的列时,都需要更新函数。顺便说一句,我尝试了您的第一个解决方案,但它似乎不起作用,至少对我而言。 我添加了您的触发器,带有 RETURN NULL 语句,以明确所有这些触发器应该合作。经过测试,它可以工作:) Tomasz:我测试了您的解决方案(带有AFTER 触发器的解决方案),它没有返回自动递增的 seq id。那是我的问题之一。我在 UPDATE 2 中发布的解决方案确实返回了 id。 我删除了这个解决方案。我现在无法重现据称带有额外“后”触发器的步骤所带来的成功。【参考方案2】:

这个问题提供了另一个解决方案: Postgres trigger-based insert redirection without breaking RETURNING

总之,为您的表创建一个视图,然后您可以使用INSTEAD OF 来处理更新,同时仍然能够返回NEW

未经测试的代码,但你明白了:

CREATE TABLE foo_base (
  id SERIAL NOT NULL,
  d_id INTEGER NOT NULL,
  label VARCHAR(4) NOT NULL
);

CREATE OR REPLACE VIEW foo AS SELECT * FROM foo_base;

CREATE TABLE foo_0 (CHECK (d_id % 2 = 0)) INHERITS (foo_base);
CREATE TABLE foo_1 (CHECK (d_id % 2 = 1)) INHERITS (foo_base);

ALTER TABLE ONLY foo_base ADD CONSTRAINT foo_base_pkey PRIMARY KEY (id);
ALTER TABLE ONLY foo_0 ADD CONSTRAINT foo_0_pkey PRIMARY KEY (id);
ALTER TABLE ONLY foo_1 ADD CONSTRAINT foo_1_pkey PRIMARY KEY (id);

ALTER TABLE ONLY foo_base ADD CONSTRAINT foo_base_d_id_key UNIQUE (d_id, label);
ALTER TABLE ONLY foo_0 ADD CONSTRAINT foo_0_d_id_key UNIQUE (d_id, label);
ALTER TABLE ONLY foo_1 ADD CONSTRAINT foo_1_d_id_key UNIQUE (d_id, label);

CREATE OR REPLACE FUNCTION foo_insert_trigger()
RETURNS TRIGGER AS $$
BEGIN
    IF NEW.id IS NULL THEN
       NEW.id := nextval('foo_base_id_seq');
    END IF;

    EXECUTE 'INSERT INTO foo_' || (NEW.d_id % 2) || ' SELECT $1.*' USING NEW;
    RETURN NEW;
END
$$
LANGUAGE plpgsql;

CREATE TRIGGER insert_foo_trigger
    INSTEAD OF INSERT ON foo
    FOR EACH ROW EXECUTE PROCEDURE foo_insert_trigger();

【讨论】:

谢谢詹姆斯。我也试试看。

以上是关于插入触发器最终在分区表中插入重复的行的主要内容,如果未能解决你的问题,请参考以下文章

基于 Postgres 触发器的分区,插入 NEW.* 给出值 (10,,,)

hive 插入parquet二级分区表数据倾斜优化

Bigquery 分区表中的重复项

使用Hive SQL插入动态分区的Parquet表OOM异常分析

流入 BQ 分区表

使用PostgreSQL插件pg_pathman对超大表分表的实践