为多个级联 1:M 子表插入重复行

zam*_*6ak 5 postgresql insert cte

想象一下具有 1:M 关系的多个父子表。我想“级联” - 根据根父表行选择插入重复行。每个表都有IDENTITY主键,每个子表都有其父 ID 的 FK(上一级)。

目标

给定根父表 ID,为其及其所有子表插入重复行。

我尝试了“级联”插入 CTE,但遇到了RETURNING仅限返回插入数据的问题,而我需要额外的信息来连接下一个INSERT.

我通过添加额外的列 ( ) 来完成此任务copied_from_id

我的问题

有没有办法在没有额外列的情况下完成相同的任务?

我确实看到了@Erwin Brandstetter 的这个答案,但他的例子只有 1 个父母和孩子,我不知道如何将其扩展到多个级别

例子

这是示例 DDL 和 DML 来说明问题

  • lvl_one - 最顶层、根、父表
  • lvl_two - lvl_one 的子表 (1:M)
  • lvl_三 - lvl_two 的子表 (1:M)

设置

--DROP TABLE IF EXISTS lvl_one,lvl_two,lvl_three CASCADE;
CREATE TABLE IF NOT EXISTS public.lvl_one (
    id      bigint NOT NULL GENERATED ALWAYS AS IDENTITY,
    name    text,
    CONSTRAINT lvl_one_pk PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS public.lvl_two (
    id          bigint NOT NULL GENERATED ALWAYS AS IDENTITY,
    lvl_one_id  bigint NOT NULL,    
    name        text,
    CONSTRAINT lvl_two_pk PRIMARY KEY (id),
    CONSTRAINT lvl_two_lvl_one_id_fk FOREIGN KEY (lvl_one_id)
        REFERENCES public.lvl_one (id) 
);
CREATE TABLE IF NOT EXISTS public.lvl_three (
    id          bigint NOT NULL GENERATED ALWAYS AS IDENTITY,
    lvl_two_id  bigint NOT NULL,    
    name        text,
    CONSTRAINT lvl_three_pk PRIMARY KEY (id),
    CONSTRAINT lvl_three_lvl_two_id_fk FOREIGN KEY (lvl_two_id)
        REFERENCES public.lvl_two (id) 
);
Run Code Online (Sandbox Code Playgroud)

初始数据

-- initial data
INSERT INTO lvl_one(name)               VALUES ('Honda'),   ('Ford'),       ('Toyota');
INSERT INTO lvl_two(lvl_one_id, name)   VALUES (1,'Civic'), (1,'Passport'), (3,'Prius');
INSERT INTO lvl_three(lvl_two_id, name) VALUES (1,'door'),  (1,'window'),   (3,'trunk');


SELECT * FROM lvl_one ORDER BY id;
-- id, name
--  1, "Honda"
--  2. "Ford"
--  3, "Toyota"

SELECT * FROM lvl_two ORDER BY id;
-- id, lvl_one_id, name
--  1, 1,   "Civic"
--  2, 1,   "Passport"
--  3, 3,   "Prius"

SELECT * FROM lvl_three ORDER BY id;
-- id, lvl_two_id,  name
--  1, 1,   "door"
--  2, 1,   "window"
--  3, 3,   "trunk"

SELECT 
  one.id AS one_id, one.name AS one_name
, two.id AS two_id, two.name AS two_name
, three.id AS three_id, three.name AS three_name
FROM lvl_one AS one 
LEFT OUTER JOIN lvl_two AS two ON one.id = two.lvl_one_id
LEFT OUTER JOIN lvl_three AS three ON two.id = three.lvl_two_id
ORDER BY one.id, two.id, three.id;
--1 "Honda"     1       "Civic"     1       "door"
--1 "Honda"     1       "Civic"     2       "window"
--1 "Honda"     2       "Passport"  NULL    NULL        
--2 "Ford"      NULL    NULL        NULL    NULL            
--3 "Toyota"    3       "Prius"     3       "trunk"
Run Code Online (Sandbox Code Playgroud)

解决方案(添加额外的列)

ALTER TABLE lvl_one   ADD COLUMN copied_from_id bigint;
ALTER TABLE lvl_two   ADD COLUMN copied_from_id bigint;
ALTER TABLE lvl_three ADD COLUMN copied_from_id bigint;


-- copy row id=1 from lvl_one and all its child tables
WITH source_one AS (
    SELECT id,name 
    FROM lvl_one 
    WHERE id=1
)
, copy_one AS (
    INSERT INTO lvl_one(name,copied_from_id)
    SELECT name,id AS copied_from_id
    FROM source_one
    RETURNING id AS new_one_id, copied_from_id
)
, copy_two AS (
    INSERT INTO lvl_two(lvl_one_id,name,copied_from_id)
    SELECT new_one_id, lvl_two.name,lvl_two.id AS copied_from_id
    FROM copy_one 
    INNER JOIN lvl_one ON lvl_one.id = copy_one.copied_from_id
    INNER JOIN lvl_two ON lvl_two.lvl_one_id = lvl_one.id
    RETURNING id AS new_two_id, copied_from_id
)
, copy_three AS (
    INSERT INTO lvl_three(lvl_two_id,name,copied_from_id)
    SELECT new_two_id, lvl_three.name, lvl_three.id AS copied_from_id
    FROM copy_two
    INNER JOIN lvl_two ON lvl_two.id = copy_two.copied_from_id
    INNER JOIN  lvl_three ON lvl_three.lvl_two_id = lvl_two.id
    RETURNING id AS new_three_id, copied_from_id
)
SELECT * FROM copy_one, copy_two, copy_three;
Run Code Online (Sandbox Code Playgroud)

复制的预期结果lvl_one.id=1

由于“复制”lvl_one.id=1行,将在所有 3 个表中创建以下行。

-- lvl_one
-- 4,Honda,1

-- lvl_two
--4,4,Civic,1
--5,4,Passport,2

-- lvl_three
--4,4,door,1
--5,4,window,2
Run Code Online (Sandbox Code Playgroud)

Erw*_*ter 2

不幸的是,RETURNINGan 的子句INSERT只能处理插入行中的列。由子句添加的列FROM在那里不可见。

看:

为了解决这个限制,我建议在SELECT每个之前INSERT使用 提前生成预期的新序列 ID nextval()。然后,您将旧 ID 和新 ID 放在同一行中以进行必要的连接。

此方法的一个额外的小问题是您的IDENTITY列带有GENERATE ALWAYS. 所以无论如何我们都需要OVERRIDING SYSTEM VALUE写入INSERT这些列。(或者您使用 创建IDENTITYGENERATED BY DEFAULT):

WITH ins1 AS (
   INSERT INTO lvl_one(name)
   SELECT name
   FROM   lvl_one
   WHERE  id = 1  --  $1 here
   RETURNING id AS new_parent_id, name   -- just the one
   )
, sel2 AS (
   SELECT ins1.new_parent_id, t2.id, t2.name, nextval(pg_get_serial_sequence('lvl_two', 'id')) AS new_id
   FROM   ins1
   JOIN   lvl_two t2 ON t2.lvl_one_id = 1   --  and $1 here
   )
, ins2 AS (
   INSERT INTO lvl_two(id, lvl_one_id, name) OVERRIDING SYSTEM VALUE 
   SELECT new_id, new_parent_id, name
   FROM   sel2
   )
, sel3 AS (
   SELECT sel2.new_id AS new_parent_id, t3.id, t3.name, nextval(pg_get_serial_sequence('lvl_three', 'id')) AS new_id
   FROM   sel2
   JOIN   lvl_three t3 ON t3.lvl_two_id = sel2.id  -- old parent ID
   )
, ins3 AS (
   INSERT INTO lvl_three(id, lvl_two_id, name) OVERRIDING SYSTEM VALUE 
   SELECT new_id, new_parent_id, name
   FROM   sel3
   )
SELECT ins1.new_parent_id AS lvl1_id, ins1.name AS lvl1_name
     , sel2.new_id AS lvl2_id, sel2.name AS lvl2_name
     , sel3.new_id AS lvl3_id, sel3.name AS lvl3_name
FROM   ins1 
LEFT   JOIN sel2 USING (new_parent_id)
LEFT   JOIN sel3 ON sel3.new_parent_id = sel2.new_id
ORDER  BY lvl1_id, lvl2_id, lvl3_id;
Run Code Online (Sandbox Code Playgroud)

db<>在这里摆弄

性能应该非常相似。主要好处是我们不需要按要求添加额外的表列。

第一个INSERT很简单,因为根据定义它只能影响单行,所以我没有SELECT在那里添加另一个。以下步骤遵循相同的模式,并且可以根据需要下降任意多个级别。

SELECT另请注意,原始解决方案中的外部会产生不正确的结果(不影响实际插入的行):

...
SELECT * FROM copy_one, copy_two, copy_three;
Run Code Online (Sandbox Code Playgroud)

CROSS JOIN表之间的行将合并不应合并的行并排除不应排除的行。