【问题标题】:PostgreSQL - insert rows based on select from another table, and update an FK in that table with the newly inserted rowsPostgreSQL - 根据从另一个表中的选择插入行,并使用新插入的行更新该表中的 FK
【发布时间】:2015-04-04 18:21:19
【问题描述】:

我正在两个表之间进行数据迁移(拆分出一个相关表)。现有表是reminders,它有一个start 列和一个新添加的dateset_id 列指向一个新的dateset 表,该表也有一个start 列。对于reminders 中的每一行,我想INSERTdateset 中复制一个新行,并将start 值复制过来,并UPDATEreminders 中使用新插入的dateset ID 对应行.

这是我尝试过的 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

我收到错误missing FROM-clause entry for table "reminder",因为我在RETURNING 子句中包含reminder.id 列,但实际上并未选择它进行插入。这是有道理的,但我不知道如何修改查询来做我需要的事情。我缺少一种完全不同的方法吗?

【问题讨论】:

    标签: sql postgresql


    【解决方案1】:

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

    1.临时添加一列

    正如其他人提到的,直接的方法是暂时将一列 reminder_id 添加到 dateset。使用来自reminder 表的原始IDs 填充它。使用它将reminderdateset 表连接起来。删除临时列。

    2。何时开始是唯一的

    如果start 列的值是唯一的,则可以通过将reminder 表与start 列上的dateset 表连接起来而无需额外的列。

    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
    ;
    

    3.何时开始不是唯一的

    即使在这种情况下,也可以在没有临时列的情况下执行此操作。主要思想如下。让我们看一下这个例子:

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

    reminder
    id    start         dateset_id
    3     2015-01-01    NULL
    7     2015-01-01    NULL
    

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

    dateset
    id    start
    1     2015-01-01
    2     2015-01-01
    

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

    reminder
    id    start         dateset_id
    3     2015-01-01    1
    7     2015-01-01    2
    

    reminder
    id    start         dateset_id
    3     2015-01-01    2
    7     2015-01-01    1
    

    这两种变体都是正确的。这给我们带来了以下解决方案。

    只需先插入所有行。

    INSERT INTO dateset (start)
    SELECT start FROM reminder;
    

    匹配/连接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
    ;
    

    我希望从代码中可以清楚地看出它的作用,尤其是当您将它与没有ROW_NUMBER 的更简单的版本进行比较时。显然,即使start 是唯一的,复杂的解决方案也可以工作,但它的效率不如简单的解决方案。

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

    【讨论】:

    • 2. 是如何工作的?似乎只有当 CTE 可以被视为视图并被更新时,这种变体才能起作用。我认为目前这在 postgres 中是不可能的。
    • @matthiaskrull,看起来你是对的。我使用的是 SQL Server 语法,但手头没有 Postgres 可以检查。在 Postgres 中,您需要在 UPDATE 语句中使用 FROM 子句来连接表。
    【解决方案2】:

    根据 Postgres 的变化进行更新:

    根据文档,Postgres 9.1 及更高版本支持在子查询中使用 INSERT RETURNING。原始答案中假设的 DML 子查询应该适用于 Postgres >= 9.1:

    UPDATE reminder SET dateset_id = (
        INSERT INTO dateset (start)
        VALUES (reminder.start)
        RETURNING dateset.id));
    

    原答案:

    这是另一种方式,不同于 Vladimir 迄今为止建议的 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;
    

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

    有趣的是,这表明返回值的 DML 子查询可能非常有用。如果可能的话,我们会写:

    UPDATE reminder SET dateset_id = (
        INSERT INTO dateset (start)
        VALUES (reminder.start)
        RETURNING dateset.id));
    

    【讨论】:

    • 哦,非常好!好的,在这一点上,我不知道该给谁赏金 - 这是第四个好的答案。希望我能把它送给所有回答的人。
    • 我想我会把它交给弗拉基米尔,只是为了建议最广泛的选项,包括一些不需要添加/删除临时列或函数的选项。但这是一个非常好的答案 - 谢谢!
    【解决方案3】:

    您只能使用 RETURNING 从 INSERT 部分返回列,而不能从所选表中返回。 因此,如果您愿意在日期集表中添加一列 reminder_id

        ALTER TABLE dateset ADD COLUMN reminder_id integer;
    

    以下语句将起作用:

    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
    

    只有当remindersstart的值都是唯一的时,以下2条语句才会也可以工作:

    INSERT INTO dateset(start) SELECT start FROM reminder;
    UPDATE reminder SET dateset_id = (SELECT id FROM dateset WHERE start=reminder.start);
    

    【讨论】:

    • 非常好的答案,谢谢!希望我可以将赏金奖励给多个答案,但由于我必须选择一个,我将选择 Vladimir's,因为它拥有最广泛的选项,包括在 start 不是唯一的情况下工作的选项,而不需要临时列.
    【解决方案4】:

    问题是您只能返回您插入的表中存在的列。您可以通过为表数据集提供一个附加列来解决它,您可以在其中插入remind.id,以便您可以返回它。

    然后在迁移之后您可以删除该列。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-10-08
      • 2013-09-05
      • 1970-01-01
      • 1970-01-01
      • 2021-06-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多