PostgreSQL中具有主键的重复行

Jos*_*rns 8 postgresql dynamic-sql default-value postgresql-9.4

假设我有一个名为 的表people,其中id是主键:

+-----------+---------+---------+
|  id       |  fname  |  lname  |
| (integer) | (text)  | (text)  |
+===========+=========+=========+
|  1        | Daniel  | Edwards |
|  2        | Fred    | Holt    |
|  3        | Henry   | Smith   |
+-----------+---------+---------+
Run Code Online (Sandbox Code Playgroud)

我正在尝试编写一个行重复查询,该查询足够健壮,可以解释表的架构更改。每当我向表中添加一列时,我都不想回去修改重复查询。

我知道我可以做到这一点,这将复制记录 ID 2 并为复制的记录提供一个新 ID:

INSERT INTO people (fname, lname) SELECT fname, lname FROM people WHERE id = 2;
Run Code Online (Sandbox Code Playgroud)

但是,如果我添加一age列,则需要修改查询以同时考虑年龄列。

显然我不能执行以下操作,因为它还会复制主键,从而导致duplicate key value violates unique constraint-- 而且,无论如何我都不希望它们共享相同的 ID:

INSERT INTO people SELECT * FROM people WHERE id = 2
Run Code Online (Sandbox Code Playgroud)

话虽如此,解决这一挑战的合理方法是什么?我宁愿远离存储过程,但我不是 100% 反对它们,我想......

Erw*_*ter 16

简单的 hstore

如果您安装了附加模块hstore下面链接中的说明),则有一种非常简单的方法可以在不了解其他列的情况下替换单个字段的值:

基本示例:复制行,id = 2但替换23

INSERT INTO people
SELECT (p #= hstore('id', '3')).* FROM people p WHERE id = 2;
Run Code Online (Sandbox Code Playgroud)

细节:

假设(因为它未在问题中定义)people.id是一个serial带有附加序列的列,您将需要序列中的下一个值。我们可以用 确定序列名称pg_get_serial_sequence()。细节:

或者,如果不会更改,您可以只对序列名称进行硬编码。
我们有这个疑问:

INSERT INTO people
SELECT (p #= hstore('id', nextval(pg_get_serial_sequence('people', 'id'))::text)).*
FROM people p WHERE id = 2;
Run Code Online (Sandbox Code Playgroud)

哪个有效,但在 Postgres 查询规划器中存在一个弱点:该表达式针对行中的每一列单独计算,浪费序列号和性能。为避免这种情况,请将表达式移动到 subqery 中并仅分解该行一次

INSERT INTO people
SELECT (p1).*
FROM  (
   SELECT p #= hstore('id', nextval(pg_get_serial_sequence('people', 'id'))::text) AS p1
   FROM   people p WHERE id = 2
   ) sub;
Run Code Online (Sandbox Code Playgroud)

一次(或几行)可能最快。

json / jsonb

如果您尚未hstore安装并且无法安装其他模块,您可以使用json_populate_record()或执行类似的技巧jsonb_populate_record(),但该功能没有记录并且可能不可靠。

临时临时表

另一个简单的解决方案是使用这样的临时临时文件:

BEGIN;
CREATE TEMP TABLE people_tmp ON COMMIT DROP AS
SELECT * FROM people WHERE id = 2;
UPDATE people_tmp SET id = nextval(pg_get_serial_sequence('people', 'id'));
INSERT INTO people TABLE people_tmp;
COMMIT;
Run Code Online (Sandbox Code Playgroud)

我添加ON COMMIT DROP了在事务结束时自动删除表。因此,我也将操作包装到它自己的事务中。两者都不是绝对必要的。

这提供了广泛的附加选项 - 您可以在插入之前对行进行任何操作,但由于创建和删除临时表的开销,它会变慢一点。

此解决方案适用于单行或同时适用于任意数量的行。每行自动从序列中获得一个新的默认值。

使用短(SQL 标准)表示法TABLE people

动态 SQL

对于一次行,动态 SQL 将是最快的。连接系统表pg_attribute或信息模式中的列,并在DO语句中动态执行或编写函数以供重复使用:

CREATE OR REPLACE FUNCTION f_row_copy(_tbl regclass, _id int, OUT row_ct int) AS
$func$
BEGIN
   EXECUTE (
      SELECT format('INSERT INTO %1$s(%2$s) SELECT %2$s FROM %1$s WHERE id = $1',
                    _tbl, string_agg(quote_ident(attname), ', '))
      FROM   pg_attribute
      WHERE  attrelid = _tbl
      AND    NOT attisdropped  -- no dropped (dead) columns
      AND    attnum > 0        -- no system columns
      AND    attname <> 'id'   -- exclude id column
      )
   USING _id;

   GET DIAGNOSTICS row_ct = ROW_COUNT;  -- directly assign OUT parameter
END
$func$  LANGUAGE plpgsql;
Run Code Online (Sandbox Code Playgroud)

称呼:

SELECT f_row_copy('people', 9);
Run Code Online (Sandbox Code Playgroud)

适用于具有名为 的整数列的任何表id。您也可以轻松地使列名动态化......

也许不是您想要的第一选择stay away from stored procedures,但话说回来,无论如何它都不是“存储过程” ......

有关的:

先进的解决方案

serial列是一个特例。如果你想用它们各自的默认值填充更多或所有列,它会变得更加复杂。考虑这个相关的答案: