PostgreSQL 自引用 table - 如何在脚本中存储 parent ID?

PostgreSQL self referential table - how to store parent ID in script?

我有以下 table:

DROP SEQUENCE IF EXISTS CATEGORY_SEQ CASCADE;
CREATE SEQUENCE CATEGORY_SEQ START 1;

DROP TABLE IF EXISTS CATEGORY CASCADE;

CREATE TABLE CATEGORY (
  ID        BIGINT                 NOT NULL DEFAULT nextval('CATEGORY_SEQ'),
  NAME      CHARACTER VARYING(255) NOT NULL,
  PARENT_ID BIGINT
);

ALTER TABLE CATEGORY
  ADD CONSTRAINT CATEGORY_PK PRIMARY KEY (ID);
ALTER TABLE CATEGORY
  ADD CONSTRAINT CATEGORY_SELF_FK FOREIGN KEY (PARENT_ID) REFERENCES CATEGORY (ID);

现在我需要插入数据。所以我从 parent:

开始
INSERT INTO CATEGORY (NAME) VALUES ('PARENT_1');

现在我需要刚刚插入的parent的ID来添加children:

INSERT INTO CATEGORY (NAME, PARENT_ID) VALUES ('CHILDREN_1_1', <what_goes_here>);
INSERT INTO CATEGORY (NAME, PARENT_ID) VALUES ('CHILDREN_1_2', <what_goes_here>);

如何获取和存储 parent 的 ID,以便稍后在后续插入中使用它?

( tl;dr : 转到选项 3: INSERT with RETURNING )

回想一下,在 postgresql 中没有 table 的 "id" 概念,只有 序列 (通常但不一定用作默认值代理主键,使用 SERIAL 伪类型)。

如果您有兴趣获取新插入行的 id,有以下几种方法:


选项 1:CURRVAL(<sequence name>);

例如:

  INSERT INTO persons (lastname,firstname) VALUES ('Smith', 'John');
  SELECT currval('persons_id_seq');

序列的名字一定要知道,真的很随意;在此示例中,我们假设 table persons 具有使用 SERIAL 伪类型创建的 id 列。为了避免依赖于此并感觉更干净,您可以改用 pg_get_serial_sequence:

  INSERT INTO persons (lastname,firstname) VALUES ('Smith', 'John');
  SELECT currval(pg_get_serial_sequence('persons','id'));

警告:currval() 仅在 INSERT(已执行 nextval() )之后有效,在同一会话中.


选项 2:LASTVAL();

这与之前的类似,只是您不需要指定序列号:它会查找最近修改的序列(总是在您的会话中,与上面的警告相同)。


CURRVALLASTVAL 都是完全并发安全的。 PG 中 sequence 的行为是为了让不同的会话不会干扰,所以不存在竞争条件的风险(如果另一个会话在我的 INSERT 和我的 SELECT 之间插入另一行,我仍然得到我的正确值)。

但是他们确实有一个微妙的潜在问题。如果数据库有一些 TRIGGER(或规则),在插入 persons table 时,会在其他 table 中进行一些额外的插入...那么 LASTVAL 可能会给我们错误的价值。如果在 persons table 中进行了额外的插入,CURRVAL 甚至可能会出现此问题(这种情况不太常见,但风险仍然存在)。


选项 3:INSERTRETURNING

INSERT INTO persons (lastname,firstname) VALUES ('Smith', 'John') RETURNING id;

这是最干净、高效和安全的获取 ID 的方法。它没有以前的任何风险。

缺点?几乎 none:您可能需要修改调用 INSERT 语句的方式(在最坏的情况下,您的 API 或 DB 层可能不期望 INSERT 到 return 一个值);这不是标准 SQL(谁在乎);自 Postgresql 8.2(2006 年 12 月...)起可用


结论:如果可以,选择选项 3。在其他地方,首选 1。

注意:如果您打算获取最后全局插入的 id(不一定在您的会话中),所有这些方法都是无用的。为此,您必须求助于 select max(id) from table(当然,这不会从其他事务中读取未提交的插入)。

您可以使用带有 returning 子句的数据修改 CTE:

with parent_cat (parent_id) as (
   INSERT INTO CATEGORY (NAME) VALUES ('PARENT_1')
   returning id
)
INSERT INTO CATEGORY (NAME, PARENT_ID) 
VALUES 
  ('CHILDREN_1_1', (select parent_id from parent_cat) ), 
  ('CHILDREN_1_2', (select parent_id from parent_cat) );

答案是使用 RETURNINGWITH

WITH inserted AS (
  INSERT INTO CATEGORY (NAME) VALUES ('PARENT_1')
  RETURNING id
) INSERT INTO CATEGORY (NAME, PARENT_ID) VALUES
  ('CHILD_1_1', (SELECT inserted.id FROM inserted)),
  ('CHILD_2_1', (SELECT inserted.id FROM inserted));