PostgreSQL:更新意味着跨分区移动

发布于 2024-08-12 14:47:55 字数 2538 浏览 1 评论 0原文

(注意:更新了下面采用的答案。)

对于 PostgreSQL 8.1(或更高版本)分区表,如何定义 UPDATE 触发器和过程来“移动”记录从一个分区到另一个分区,如果UPDATE意味着对定义分区隔离的约束字段的更改?

例如,我有一个表记录分为活动记录和非活动记录,如下所示:

create table RECORDS (RECORD varchar(64) not null, ACTIVE boolean default true);
create table ACTIVE_RECORDS   ( check (ACTIVE) ) inherits RECORDS;
create table INACTIVE_RECORDS ( check (not ACTIVE) ) inherits RECORDS;

INSERT 触发器和函数运行良好:新的活动记录放入一个表中,新的非活动记录放入另一个表中。我希望对 ACTIVE 字段进行 UPDATE 来将记录从一个后代表“移动”到另一个后代表,但遇到一个错误,表明这可能是不可能的。

触发器规范和错误消息:

pg=> CREATE OR REPLACE FUNCTION record_update()
     RETURNS TRIGGER AS $$
     BEGIN
       IF (NEW.active = OLD.active) THEN
         RETURN NEW;
       ELSIF (NEW.active) THEN
         INSERT INTO active_records VALUES (NEW.*);
         DELETE FROM inactive_records WHERE record = NEW.record;
       ELSE
         INSERT INTO inactive_records VALUES (NEW.*);
         DELETE FROM active_records WHERE record = NEW.record;
       END IF;
       RETURN NULL;
     END;
     $$
     LANGUAGE plpgsql;
     
pg=> CREATE TRIGGER record_update_trigger
       BEFORE UPDATE ON records
       FOR EACH ROW EXECUTE PROCEDURE record_update();

pg=> select * from RECORDS;
record | active 
--------+--------
foo    | t         -- 'foo' record actually in table ACTIVE_RECORDS
bar    | f         -- 'bar' record actually in table INACTIVE_RECORDS
(2 rows)

pg=> update RECORDS set ACTIVE = false where RECORD = 'foo';
ERROR:  new row for relation "active_records" violates check constraint "active_records_active_check"

使用触发器过程(返回 NULL 等)建议我在调用触发器之前检查约束并引发错误,这意味着我当前的方法将不起作用。这可以开始工作吗?

附加答案

pg的[列表分区][2]似乎是实现此目的的最简单方法:

-- untested!
create table RECORDS (..., ACTIVE boolean...)
partition by list(ACTIVE) (
  partition   ACTIVE_RECORDS values (true),
  partition INACTIVE_RECORDS values (false)
) 

更新/答案

下面是我最终使用了更新触发过程,分配给每个分区的过程相同。完全归功于 Bell,他的回答为我提供了在分区上触发的关键见解:

CREATE OR REPLACE FUNCTION record_update()
RETURNS TRIGGER AS $$
BEGIN
  IF ( (TG_TABLE_NAME = 'active_records' AND NOT NEW.active)
        OR
       (TG_TABLE_NAME = 'inactive_records' AND NEW.active) ) THEN
    DELETE FROM records WHERE record = NEW.record;
    INSERT INTO records VALUES (NEW.*);
    RETURN NULL;
  END IF;

  RETURN NEW;
END;
$$
LANGUAGE plpgsql;

(Note: updated with adopted answer below.)

For a PostgreSQL 8.1 (or later) partitioned table, how does one define an UPDATE trigger and procedure to "move" a record from one partition to the other, if the UPDATE implies a change to the constrained field that defines the partition segregation?

For example, I've a table records partitioned into active and inactive records like so:

create table RECORDS (RECORD varchar(64) not null, ACTIVE boolean default true);
create table ACTIVE_RECORDS   ( check (ACTIVE) ) inherits RECORDS;
create table INACTIVE_RECORDS ( check (not ACTIVE) ) inherits RECORDS;

The INSERT trigger and function work well: new active records get put in one table, and new inactive records in another. I would like UPDATEs to the ACTIVE field to "move" a record from one one descendant table to the other, but am encountering an error which suggests that this may not be possible.

Trigger specification and error message:

pg=> CREATE OR REPLACE FUNCTION record_update()
     RETURNS TRIGGER AS $
     BEGIN
       IF (NEW.active = OLD.active) THEN
         RETURN NEW;
       ELSIF (NEW.active) THEN
         INSERT INTO active_records VALUES (NEW.*);
         DELETE FROM inactive_records WHERE record = NEW.record;
       ELSE
         INSERT INTO inactive_records VALUES (NEW.*);
         DELETE FROM active_records WHERE record = NEW.record;
       END IF;
       RETURN NULL;
     END;
     $
     LANGUAGE plpgsql;
     
pg=> CREATE TRIGGER record_update_trigger
       BEFORE UPDATE ON records
       FOR EACH ROW EXECUTE PROCEDURE record_update();

pg=> select * from RECORDS;
record | active 
--------+--------
foo    | t         -- 'foo' record actually in table ACTIVE_RECORDS
bar    | f         -- 'bar' record actually in table INACTIVE_RECORDS
(2 rows)

pg=> update RECORDS set ACTIVE = false where RECORD = 'foo';
ERROR:  new row for relation "active_records" violates check constraint "active_records_active_check"

Playing with the trigger procedure (returning NULL and so forth) suggests to me that the constraint is checked, and the error raised, before my trigger is invoked, meaning that my current approach won't work. Can this be gotten to work?

ADDITIONAL ANSWER

pg's [list partitioning][2] appears to be the easiest way to accomplish this:

-- untested!
create table RECORDS (..., ACTIVE boolean...)
partition by list(ACTIVE) (
  partition   ACTIVE_RECORDS values (true),
  partition INACTIVE_RECORDS values (false)
) 

UPDATE/ANSWER

Below is the UPDATE trigger procedure I ended up using, the same procedure assigned to each of the partitions. Credit is entirely to Bell, whose answer gave me the key insight to trigger on the partitions:

CREATE OR REPLACE FUNCTION record_update()
RETURNS TRIGGER AS $
BEGIN
  IF ( (TG_TABLE_NAME = 'active_records' AND NOT NEW.active)
        OR
       (TG_TABLE_NAME = 'inactive_records' AND NEW.active) ) THEN
    DELETE FROM records WHERE record = NEW.record;
    INSERT INTO records VALUES (NEW.*);
    RETURN NULL;
  END IF;

  RETURN NEW;
END;
$
LANGUAGE plpgsql;

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

只是在用心讲痛 2024-08-19 14:47:55

它可以工作,只需要为每个分区定义执行移动的触发器,而不是整个表。因此,像表定义和 INSERT 触发器一样开始

CREATE TABLE records (
 record varchar(64) NOT NULL,
 active boolean default TRUE
);

CREATE TABLE active_records (CHECK (active)) INHERITS (records);
CREATE TABLE inactive_records (CHECK (NOT active)) INHERITS (records);

CREATE OR REPLACE FUNCTION record_insert()
RETURNS TRIGGER AS $
BEGIN
  IF (TRUE = NEW.active) THEN
    INSERT INTO active_records VALUES (NEW.*);
  ELSE
    INSERT INTO inactive_records VALUES (NEW.*);
  END IF;
  RETURN NULL;
END;
$
LANGUAGE plpgsql;

CREATE TRIGGER record_insert_trigger
 BEFORE INSERT ON records
 FOR EACH ROW EXECUTE PROCEDURE record_insert();

...让我们有一些测试数据...

INSERT INTO records VALUES ('FirstLittlePiggy', TRUE);
INSERT INTO records VALUES ('SecondLittlePiggy', FALSE);
INSERT INTO records VALUES ('ThirdLittlePiggy', TRUE);
INSERT INTO records VALUES ('FourthLittlePiggy', FALSE);
INSERT INTO records VALUES ('FifthLittlePiggy', TRUE);

现在是分区上的触发器。 if NEW.active = OLD.active 检查隐式检查 active 的值,因为我们首先知道表中允许包含哪些内容。

CREATE OR REPLACE FUNCTION active_partition_constraint()
  RETURNS TRIGGER AS $
    BEGIN
      IF NOT (NEW.active) THEN
        INSERT INTO inactive_records VALUES (NEW.*);
        DELETE FROM active_records WHERE record = NEW.record;
        RETURN NULL;
      ELSE
        RETURN NEW;
      END IF;
    END;
    $
    LANGUAGE plpgsql;

CREATE TRIGGER active_constraint_trigger
  BEFORE UPDATE ON active_records
  FOR EACH ROW EXECUTE PROCEDURE active_partition_constraint();

CREATE OR REPLACE FUNCTION inactive_partition_constraint()
  RETURNS TRIGGER AS $
    BEGIN
      IF (NEW.active) THEN
        INSERT INTO active_records VALUES (NEW.*);
        DELETE FROM inactive_records WHERE record = NEW.record;
        RETURN NULL;
      ELSE
        RETURN NEW;
      END IF;
    END;
    $
    LANGUAGE plpgsql;

CREATE TRIGGER inactive_constraint_trigger
  BEFORE UPDATE ON inactive_records 
  FOR EACH ROW EXECUTE PROCEDURE inactive_partition_constraint();

...并测试结果...

scratch=> SELECT * FROM active_records;
      record      | active 
------------------+--------
 FirstLittlePiggy | t
 ThirdLittlePiggy | t
 FifthLittlePiggy | t
(3 rows)

scratch=> UPDATE records SET active = FALSE WHERE record = 'ThirdLittlePiggy';
UPDATE 0
scratch=> SELECT * FROM active_records;
      record      | active 
------------------+--------
 FirstLittlePiggy | t
 FifthLittlePiggy | t
(2 rows)

scratch=> SELECT * FROM inactive_records;
      record       | active 
-------------------+--------
 SecondLittlePiggy | f
 FourthLittlePiggy | f
 ThirdLittlePiggy  | f
(3 rows)

It can be made to work, the trigger that does the move just needs to be defined for each partition, not the whole table. So start as you did for table definitions and the INSERT trigger

CREATE TABLE records (
 record varchar(64) NOT NULL,
 active boolean default TRUE
);

CREATE TABLE active_records (CHECK (active)) INHERITS (records);
CREATE TABLE inactive_records (CHECK (NOT active)) INHERITS (records);

CREATE OR REPLACE FUNCTION record_insert()
RETURNS TRIGGER AS $
BEGIN
  IF (TRUE = NEW.active) THEN
    INSERT INTO active_records VALUES (NEW.*);
  ELSE
    INSERT INTO inactive_records VALUES (NEW.*);
  END IF;
  RETURN NULL;
END;
$
LANGUAGE plpgsql;

CREATE TRIGGER record_insert_trigger
 BEFORE INSERT ON records
 FOR EACH ROW EXECUTE PROCEDURE record_insert();

... let's have some test data ...

INSERT INTO records VALUES ('FirstLittlePiggy', TRUE);
INSERT INTO records VALUES ('SecondLittlePiggy', FALSE);
INSERT INTO records VALUES ('ThirdLittlePiggy', TRUE);
INSERT INTO records VALUES ('FourthLittlePiggy', FALSE);
INSERT INTO records VALUES ('FifthLittlePiggy', TRUE);

Now the triggers on the partitions. The if NEW.active = OLD.active check is implicit in checking the value of active since we know what's allowed to be in the table in the first place.

CREATE OR REPLACE FUNCTION active_partition_constraint()
  RETURNS TRIGGER AS $
    BEGIN
      IF NOT (NEW.active) THEN
        INSERT INTO inactive_records VALUES (NEW.*);
        DELETE FROM active_records WHERE record = NEW.record;
        RETURN NULL;
      ELSE
        RETURN NEW;
      END IF;
    END;
    $
    LANGUAGE plpgsql;

CREATE TRIGGER active_constraint_trigger
  BEFORE UPDATE ON active_records
  FOR EACH ROW EXECUTE PROCEDURE active_partition_constraint();

CREATE OR REPLACE FUNCTION inactive_partition_constraint()
  RETURNS TRIGGER AS $
    BEGIN
      IF (NEW.active) THEN
        INSERT INTO active_records VALUES (NEW.*);
        DELETE FROM inactive_records WHERE record = NEW.record;
        RETURN NULL;
      ELSE
        RETURN NEW;
      END IF;
    END;
    $
    LANGUAGE plpgsql;

CREATE TRIGGER inactive_constraint_trigger
  BEFORE UPDATE ON inactive_records 
  FOR EACH ROW EXECUTE PROCEDURE inactive_partition_constraint();

... and test the results ...

scratch=> SELECT * FROM active_records;
      record      | active 
------------------+--------
 FirstLittlePiggy | t
 ThirdLittlePiggy | t
 FifthLittlePiggy | t
(3 rows)

scratch=> UPDATE records SET active = FALSE WHERE record = 'ThirdLittlePiggy';
UPDATE 0
scratch=> SELECT * FROM active_records;
      record      | active 
------------------+--------
 FirstLittlePiggy | t
 FifthLittlePiggy | t
(2 rows)

scratch=> SELECT * FROM inactive_records;
      record       | active 
-------------------+--------
 SecondLittlePiggy | f
 FourthLittlePiggy | f
 ThirdLittlePiggy  | f
(3 rows)
童话 2024-08-19 14:47:55

请注意,您可以按列表分区,并让数据库完成在分区之间移动行的所有艰苦工作。
(未经 8.4 测试,但很可能有效,正如 pilcrow 的评论)。

在以下示例中,使用主键中的列之一创建了一个表,并按列表对其进行分区。

create table t (
  -- natural primary key
  doc_type varchar not null default 'PRODUCT',
  doc_id   int not null generated always as identity,
  
  -- content columns
  title    varchar not null,
  
  -- primary key
  primary key (doc_type, doc_id)
)
partition by list(doc_type);

-- partitions of t
create table t_product partition of t for values in ('PRODUCT');
create table t_default partition of t default;

然后,我们插入一些应以 t_productt_default 结尾的数据,具体取决于 doc_type 的值。

insert into t (doc_type, title) values
('PRODUCT', 'My first product'),  -- 1
('ARTICLE', 'My first article'),  -- 2
('TOPIC',   'My first topic'),    -- 3
('PRODUCT', 'My second product'), -- 4
('PRODUCT', 'My third product'),  -- 5
('ARTICLE', 'My second article'), -- 6
('TOPIC',   'My second topic'),   -- 7
('PRODUCT', 'My fourth product'); -- 8

我们检查行是否自动移至右侧表格

select * from t_product;

doc_type|doc_id|title            |
--------+------+-----------------+
PRODUCT |     1|My first product |
PRODUCT |     4|My second product|
PRODUCT |     5|My third product |
PRODUCT |     8|My fourth product|

现在,让我们将 PRODUCT 转换为 ARTICLE 看看会发生什么。

update t
set    doc_type = 'ARTICLE'
where  doc_type = 'PRODUCT'
and    doc_id = 1;

可以看到该行不再位于 t_product 分区中,

select * from t_product;
doc_type|doc_id|title            |
--------+------+-----------------+
PRODUCT |     4|My second product|
PRODUCT |     5|My third product |
PRODUCT |     8|My fourth product|

而是位于 t_default 分区中。

doc_type|doc_id|title            |
--------+------+-----------------+
ARTICLE |     2|My first article |
TOPIC   |     3|My first topic   |
ARTICLE |     6|My second article|
TOPIC   |     7|My second topic  |
ARTICLE |     1|My first product |

Beware that you can partition by list and let the database do all the hard work to move rows among partitions.
(untested for 8.4 but most probably working, as for pilcrow comment).

In the following example, a table is created and partitioned by list, using one of the columns in the primary key.

create table t (
  -- natural primary key
  doc_type varchar not null default 'PRODUCT',
  doc_id   int not null generated always as identity,
  
  -- content columns
  title    varchar not null,
  
  -- primary key
  primary key (doc_type, doc_id)
)
partition by list(doc_type);

-- partitions of t
create table t_product partition of t for values in ('PRODUCT');
create table t_default partition of t default;

Then we insert some data that should end in t_product or t_default, depending on the value of doc_type.

insert into t (doc_type, title) values
('PRODUCT', 'My first product'),  -- 1
('ARTICLE', 'My first article'),  -- 2
('TOPIC',   'My first topic'),    -- 3
('PRODUCT', 'My second product'), -- 4
('PRODUCT', 'My third product'),  -- 5
('ARTICLE', 'My second article'), -- 6
('TOPIC',   'My second topic'),   -- 7
('PRODUCT', 'My fourth product'); -- 8

We check rows are automatically moved to the right table

select * from t_product;

doc_type|doc_id|title            |
--------+------+-----------------+
PRODUCT |     1|My first product |
PRODUCT |     4|My second product|
PRODUCT |     5|My third product |
PRODUCT |     8|My fourth product|

Now, let us convert a PRODUCT into an ARTICLE to see what happens.

update t
set    doc_type = 'ARTICLE'
where  doc_type = 'PRODUCT'
and    doc_id = 1;

It can be seen the row is not in the t_product partition anymore

select * from t_product;
doc_type|doc_id|title            |
--------+------+-----------------+
PRODUCT |     4|My second product|
PRODUCT |     5|My third product |
PRODUCT |     8|My fourth product|

but in the t_default partition.

doc_type|doc_id|title            |
--------+------+-----------------+
ARTICLE |     2|My first article |
TOPIC   |     3|My first topic   |
ARTICLE |     6|My second article|
TOPIC   |     7|My second topic  |
ARTICLE |     1|My first product |
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文