PostgreSQL - 根据另一个表中的select插入行,并使用新插入的行更新该表中的FK

Car*_*yer 10 sql postgresql

我正在两个表之间进行数据迁移(拆分相关表).现有的表是reminders,它有一个start列和一个新添加的dateset_id列,指向一个新dateset表,该表也有一start列.对于每一行reminders,我想要INSERT一个新的行,dateset其中start复制的值,以及带有新插入的ID UPDATE的相应行.remindersdateset

这是我试过的SQL:

WITH inserted_datesets AS (
  INSERT INTO dateset (start)
  SELECT start FROM reminder
  RETURNING reminder.id AS reminder_id, id AS dateset_id
)
UPDATE reminder
SET dateset_id = ids.dateset_id
FROM inserted_datesets AS ids
WHERE reminder.id = ids.reminder_id
Run Code Online (Sandbox Code Playgroud)

我收到一个错误missing FROM-clause entry for table "reminder",因为我reminder.idRETURNING子句中包含了列,但实际上没有为插入选择它.这是有道理的,但我无法弄清楚如何修改查询以执行我需要的操作.我缺少一种完全不同的方法吗?

Vla*_*nov 10

有几种方法可以解决这个问题.

1.暂时添加一列

正如其他人所提到的,直接的方式就是一列临时添加reminder_iddateset.原装填充它IDsreminder表.用它来加入reminderdateset表.删除临时列.

2.当开始是独特的

如果start列的值是唯一的,则可以通过reminderdateset表与start列上的表连接来在没有额外列的情况下执行此操作.

INSERT INTO dateset (start)
SELECT start FROM reminder;

WITH
CTE_Joined
AS
(
    SELECT
        reminder.id AS reminder_id
        ,reminder.dateset_id AS old_dateset_id
        ,dateset.id AS new_dateset_id
    FROM
        reminder
        INNER JOIN dateset ON dateset.start = reminder.start
)
UPDATE CTE_Joined
SET old_dateset_id = new_dateset_id
;
Run Code Online (Sandbox Code Playgroud)

3.开始时不是唯一的

即使在这种情况下,也可以在没有临时列的情况下进行.主要思想如下.我们来看看这个例子:

我们有两行reminder具有相同的start值和ID 3和7:

reminder
id    start         dateset_id
3     2015-01-01    NULL
7     2015-01-01    NULL
Run Code Online (Sandbox Code Playgroud)

将它们插入后dateset,将生成新的ID,例如1和2:

dateset
id    start
1     2015-01-01
2     2015-01-01
Run Code Online (Sandbox Code Playgroud)

我们如何链接这两行并不重要.最终结果可能是

reminder
id    start         dateset_id
3     2015-01-01    1
7     2015-01-01    2
Run Code Online (Sandbox Code Playgroud)

要么

reminder
id    start         dateset_id
3     2015-01-01    2
7     2015-01-01    1
Run Code Online (Sandbox Code Playgroud)

这两种变体都是正确的.这带来了以下解决方案.

只需先插入所有行.

INSERT INTO dateset (start)
SELECT start FROM reminder;
Run Code Online (Sandbox Code Playgroud)

start知道它不是唯一的,在列上匹配/连接两个表.通过添加ROW_NUMBER和连接两列来"使它"独一无二.可以缩短查询时间,但我明确地说明了每一步:

WITH
CTE_reminder_rn
AS
(
    SELECT
        id
        ,start
        ,dateset_id
        ,ROW_NUMBER() OVER (PARTITION BY start ORDER BY id) AS rn
    FROM reminder
)
,CTE_dateset_rn
AS
(
    SELECT
        id
        ,start
        ,ROW_NUMBER() OVER (PARTITION BY start ORDER BY id) AS rn
    FROM dateset
)
,CTE_Joined
AS
(
    SELECT
        CTE_reminder_rn.id AS reminder_id
        ,CTE_reminder_rn.dateset_id AS old_dateset_id
        ,CTE_dateset_rn.id AS new_dateset_id
    FROM
        CTE_reminder_rn
        INNER JOIN CTE_dateset_rn ON 
            CTE_dateset_rn.start = CTE_reminder_rn.start AND
            CTE_dateset_rn.rn = CTE_reminder_rn.rn
)
UPDATE CTE_Joined
SET old_dateset_id = new_dateset_id
;
Run Code Online (Sandbox Code Playgroud)

我希望从代码中可以清楚地知道它的作用,特别是当你将它与没有的简单版本进行比较时ROW_NUMBER.显然,复杂的解决方案即使start是独一无二的也能工作,但它并不像一个简单的解决方案那样高效.

此解决方案假定dateset在此过程之前为空.


ell*_*ben 6

这是另一种做法,与弗拉基米尔到目前为止提出的3种方式不同.

临时函数将允许您读取创建的新行的ID以及查询中的其他值:

--minimal demonstration schema
CREATE TABLE dateset (
  id SERIAL PRIMARY KEY,
  start TIMESTAMP
  -- other things here...
);

CREATE TABLE reminder (
  id SERIAL PRIMARY KEY,
  start TIMESTAMP,
  dateset_id INTEGER REFERENCES dateset(id)
  -- other things here...
);

--pre-migration data
INSERT INTO reminder (start) VALUES ('2014-02-14'), ('2014-09-06'), ('1984-01-01'), ('2014-02-14');

--all at once
BEGIN;

CREATE FUNCTION insertreturning(ts TIMESTAMP) RETURNS INTEGER AS $$
    INSERT INTO dateset (start)
    VALUES (ts)
    RETURNING dateset.id;
  $$ LANGUAGE SQL;

UPDATE reminder SET dateset_id = insertreturning(reminder.start);

DROP FUNCTION insertreturning(TIMESTAMP);

ALTER TABLE reminder DROP COLUMN start;

END;
Run Code Online (Sandbox Code Playgroud)

在我意识到写作INSERT ... RETURNING子查询可以解决问题之后,这种解决问题的方法就出现了.虽然INSERTs不允许作为子查询,但函数调用肯定是.

有趣的是,这表明返回值的DML子查询可能广泛有用.如果可能,我们只会写:

UPDATE reminder SET dateset_id = (
    INSERT INTO dateset (start)
    VALUES (reminder.start)
    RETURNING dateset.id));
Run Code Online (Sandbox Code Playgroud)


ral*_*.w. 5

您只能使用 RETURNING 从 INSERT 部分返回列,而不能从所选表中返回。所以,如果你愿意在你的日期集表中添加一个列提醒_id,

    ALTER TABLE dateset ADD COLUMN reminder_id integer;
Run Code Online (Sandbox Code Playgroud)

以下语句将起作用:

WITH inserted_datesets AS (
  INSERT INTO dateset (start, reminder_id)
  SELECT start, id FROM reminder
  RETURNING reminder_id, id AS dateset_id
)
UPDATE reminder
SET dateset_id = ids.dateset_id
FROM inserted_datesets AS ids
WHERE id = reminder_id
Run Code Online (Sandbox Code Playgroud)

只有当该列的值开始提醒 都是独一无二的,下面的2条语句也能发挥作用:

INSERT INTO dateset(start) SELECT start FROM reminder;
UPDATE reminder SET dateset_id = (SELECT id FROM dateset WHERE start=reminder.start);
Run Code Online (Sandbox Code Playgroud)