我发现使用可写 CTE 来模拟 PostgreSQL 中的 upsert 是一个非常优雅的解决方案,直到我们在 Postgres 中获得实际的 upsert/merge。 (见:https://stackoverflow.com/a/8702291/558819)
但是,有一个问题:如何插入默认值?使用 NULL
当然不会有帮助,因为 NULL
被明确插入为 NULL
,与例如 MySQL 不同。一个例子:
WITH new_values (id, playlist, item, group_name, duration, sort, legacy) AS (
VALUES (651, 21, 30012, 'a', 30, 1, FALSE)
, (NULL::int, 21, 1, 'b', 34, 2, NULL::boolean)
, (668, 21, 30012, 'c', 30, 3, FALSE)
, (7428, 21, 23068, 'd', 0, 4, FALSE)
), upsert AS (
UPDATE playlist_items m
SET (playlist, item, group_name, duration, sort, legacy)
= (nv.playlist, nv.item, nv.group_name, nv.duration, nv.sort, nv.legacy)
FROM new_values nv
WHERE nv.id = m.id
RETURNING m.id
)
INSERT INTO playlist_items (playlist, item, group_name, duration, sort, legacy)
SELECT playlist, item, group_name, duration, sort, legacy
FROM new_values nv
WHERE NOT EXISTS (SELECT 1
FROM upsert m
WHERE nv.id = m.id)
RETURNING id
所以我想例如
legacy
第二列采用其默认值 VALUES
排。我尝试了一些方法,例如明确使用
DEFAULT
在 VALUES 列表中,这不起作用,因为 CTE 不知道它插入了什么。我也试过 coalesce(col, DEFAULT)
在似乎也不起作用的插入语句中。那么,有可能做我想做的吗?
最佳答案
Postgres 9.5 已实现 UPSERT
.见下文。
Postgres 9.4 或更高版本
这是一个棘手的问题。您遇到了此限制( per documentation ):
In a
VALUES
list appearing at the top level of anINSERT
, an expression can be replaced byDEFAULT
to indicate that the destination column's default value should be inserted.DEFAULT
cannot be used whenVALUES
appears in other contexts.
大胆强调我的。如果没有要插入的表,则不会定义默认值。所以你的问题没有直接的解决方案,但有很多可能的替代路线,具体取决于具体要求 .
从系统目录中获取默认值?
您可以从系统目录
pg_attrdef
中获取这些信息。 like @Patrick commented或来自 information_schema.columns
.在这里完成说明:但是,您仍然只有一个行列表,其中包含表达式的文本表示来 cooking 默认值。您必须动态地构建和执行语句以获取要使用的值。乏味而凌乱。相反,我们可以让内置的 Postgres 功能为我们做这件事:
简单快捷
插入一个虚拟行并让它返回以使用生成的默认值:
INSERT INTO playlist_items DEFAULT VALUES RETURNING *;
问题/解决方案范围
STABLE
or IMMUTABLE
default expressions .最VOLATILE
函数也能正常工作,但不能保证。 current_timestamp
函数族是稳定的,因为它们的值在事务中不会改变。特别是,这对 有副作用。
serial
列(或从序列中绘制的任何其他默认值)。但这应该不是问题,因为您通常不会写信给 serial
直接列。那些不应该在 INSERT
中列出的声明。serial
的剩余缺陷列:序列仍然通过单个调用前进以获得默认行,从而在编号中产生间隙。同样,这应该不是问题,因为在 serial
中通常会出现差距。列。 还有两个问题可以解决:
NOT NULL
,您必须插入虚拟值并替换为 NULL
结果中。 ON DELETE
.还有更好的办法:避免虚拟行
克隆一个 临时表包括列默认值并插入其中:
BEGIN;
CREATE TEMP TABLE tmp_playlist_items (LIKE playlist_items INCLUDING DEFAULTS)
ON COMMIT DROP; -- drop at end of transaction
INSERT INTO tmp_playlist_items DEFAULT VALUES RETURNING *;
...
结果相同,副作用更少。由于默认表达式是逐字复制的,如果有的话,克隆会从相同的序列中提取。但是完全避免了不需要的行或触发器的其他副作用。
归功于 Igor 的想法:
删除
NOT NULL
约束您必须为
NOT NULL
提供虚拟值。列,因为( per documentation ):Not-null constraints are always copied to the new table.
要么容纳那些在
INSERT
声明或(更好)消除约束:ALTER TABLE tmp_playlist_items
ALTER COLUMN foo DROP NOT NULL
, ALTER COLUMN bar DROP NOT NULL;
有一个快速而肮脏的方式具有 super 用户权限:
UPDATE pg_attribute
SET attnotnull = FALSE
WHERE attrelid = 'tmp_playlist_items'::regclass
AND attnotnull
AND attnum > 0;
它只是一个没有数据也没有其他用途的临时表,它在事务结束时被删除。所以捷径很诱人。不过,基本规则是:永远不要直接篡改系统目录。
那么,让我们来看看 清洁方式 :
在
DO
中使用动态 SQL 实现自动化陈述。您只需要保证拥有的常规权限,因为同一个角色创建了临时表。DO $$BEGIN
EXECUTE (
SELECT 'ALTER TABLE tmp_playlist_items ALTER '
|| string_agg(quote_ident(attname), ' DROP NOT NULL, ALTER ')
|| ' DROP NOT NULL'
FROM pg_catalog.pg_attribute
WHERE attrelid = 'tmp_playlist_items'::regclass
AND attnotnull
AND attnum > 0
);
END$$
更干净,仍然非常快。小心执行动态命令并警惕 SQL 注入(inject)。这个说法是安全的。我已发帖 several related answers with more explanation.
通用解决方案(9.4 及更早版本)
BEGIN;
CREATE TEMP TABLE tmp_playlist_items
(LIKE playlist_items INCLUDING DEFAULTS) ON COMMIT DROP;
DO $$BEGIN
EXECUTE (
SELECT 'ALTER TABLE tmp_playlist_items ALTER '
|| string_agg(quote_ident(attname), ' DROP NOT NULL, ALTER ')
|| ' DROP NOT NULL'
FROM pg_catalog.pg_attribute
WHERE attrelid = 'tmp_playlist_items'::regclass
AND attnotnull
AND attnum > 0
);
END$$;
LOCK TABLE playlist_items IN EXCLUSIVE MODE; -- forbid concurrent writes
WITH default_row AS (
INSERT INTO tmp_playlist_items DEFAULT VALUES RETURNING *
)
, new_values (id, playlist, item, group_name, duration, sort, legacy) AS (
VALUES
(651, 21, 30012, 'a', 30, 1, FALSE)
, (NULL, 21, 1, 'b', 34, 2, NULL)
, (668, 21, 30012, 'c', 30, 3, FALSE)
, (7428, 21, 23068, 'd', 0, 4, FALSE)
)
, upsert AS ( -- *not* replacing existing values in UPDATE (?)
UPDATE playlist_items m
SET ( playlist, item, group_name, duration, sort, legacy)
= (n.playlist, n.item, n.group_name, n.duration, n.sort, n.legacy)
-- ..., COALESCE(n.legacy, m.legacy) -- see below
FROM new_values n
WHERE n.id = m.id
RETURNING m.id
)
INSERT INTO playlist_items
(playlist, item, group_name, duration, sort, legacy)
SELECT n.playlist, n.item, n.group_name, n.duration, n.sort
, COALESCE(n.legacy, d.legacy)
FROM new_values n, default_row d -- single row can be cross-joined
WHERE NOT EXISTS (SELECT 1 FROM upsert u WHERE u.id = n.id)
RETURNING id;
COMMIT;
您只需要
LOCK
如果您有并发事务尝试写入同一个表。根据要求,这仅替换列
legacy
中的 NULL 值在 INSERT
的输入行中案件。可以轻松扩展到其他列或在 UPDATE
中工作情况也是如此。例如,您可以 UPDATE
也有条件:仅当输入值为 NOT NULL
.我在 UPDATE
中添加了注释行以上。旁白:除了
VALUES
中的第一个之外,您不需要在任何行中转换值。表达式,因为类型是从第一行派生的。Postgres 9.5
工具 UPSERT 与
INSERT .. ON CONFLICT .. DO NOTHING | UPDATE
.这在很大程度上简化了操作:INSERT INTO playlist_items AS m (id, playlist, item, group_name, duration, sort, legacy)
VALUES (651, 21, 30012, 'a', 30, 1, FALSE)
, (DEFAULT, 21, 1, 'b', 34, 2, DEFAULT) -- !
, (668, 21, 30012, 'c', 30, 3, FALSE)
, (7428, 21, 23068, 'd', 0, 4, FALSE)
ON CONFLICT (id) DO UPDATE
SET (playlist, item, group_name, duration, sort, legacy)
= (EXCLUDED.playlist, EXCLUDED.item, EXCLUDED.group_name
, EXCLUDED.duration, EXCLUDED.sort, EXCLUDED.legacy)
-- (..., COALESCE(l.legacy, EXCLUDED.legacy)) -- see below
RETURNING m.id;
我们可以附上
VALUES
条款到 INSERT
直接,这允许 DEFAULT
关键词。在 (id)
上发生独特违规的情况下, Postgres 更新代替。我们可以在 UPDATE
中使用排除的行. The manual:The
SET
andWHERE
clauses inON CONFLICT DO UPDATE
have access to the existing row using the table's name (or an alias), and to rows proposed for insertion using the specialexcluded
table.
和:
Note that the effects of all per-row
BEFORE INSERT
triggers are reflected in excluded values, since those effects may have contributed to the row being excluded from insertion.
剩下的角落案例
您有多种选择
UPDATE
: 你可以 ...WHERE
条款给 UPDATE
只写入选定的行。 COALESCE(l.legacy, EXCLUDED.legacy)
NOT NULL
时才更新:COALESCE(EXCLUDED.legacy, l.legacy)
但是没有办法辨别
DEFAULT
INSERT
中实际提供的值和值.只有结果 EXCLUDED
行是可见的。如果您需要区别,请回到之前的解决方案,我们可以为您提供两者。
关于sql - 使用 PostgreSQL 9.3 在 CTE UPSERT 中生成 DEFAULT 值,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/23794405/