使用PostgreSQL 9.3在CTE UPSERT中生成默认值
我发现使用可写CTE模拟PostgreSQL中的upsert是一个非常优雅的解决方案,直到我们在Postgres中得到实际的upsert/merge。(见:) 但是,有一个问题:如何插入默认值?使用使用PostgreSQL 9.3在CTE UPSERT中生成默认值,sql,postgresql,merge,sql-insert,upsert,Sql,Postgresql,Merge,Sql Insert,Upsert,我发现使用可写CTE模拟PostgreSQL中的upsert是一个非常优雅的解决方案,直到我们在Postgres中得到实际的upsert/merge。(见:) 但是,有一个问题:如何插入默认值?使用NULL当然没有帮助,因为NULL被显式地作为NULL插入,这与MySQL不同。例如: WITH new_values (id, playlist, item, group_name, duration, sort, legacy) AS ( VALUES (651, 21, 30012, '
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
,但这不起作用,因为CTE不知道它插入了什么。我还在insert语句中尝试了coalesce(col,DEFAULT)
,但似乎也不起作用。那么,有可能实现我想要的吗?Postgres 9.5实现了UPSERT
。见下文
博士后9.4或以上
这是一个棘手的问题。您遇到了以下限制():
在插入
的顶层出现的值
列表中
表达式可以替换为DEFAULT
,以指示目标
应插入列的默认值默认值
在以下情况下不能使用
值
出现在其他上下文中。
我的。如果没有要插入的表,则不会定义默认值。因此,您的问题没有直接的解决方案,但根据具体要求,有许多可能的替代路线
是否从系统目录中获取默认值?
您可以从系统目录或从中获取它们。请在此完成说明:
INSERT INTO playlist_items DEFAULT VALUES RETURNING *;
问题/解决方案的范围
- 这只保证对你有用。大多数VOLATILE函数也能正常工作,但没有保证。
函数系列符合稳定条件,因为它们的值在事务中不会更改。current\u timestamp
特别是,这对序列列(或序列中的任何其他默认图形)有副作用。但这不应该是个问题,因为您通常不会直接写入
列。这些内容根本不应该列在serial
语句中。INSERT
列的剩余缺陷:单次调用获取默认行仍会使序列提前,从而在编号中产生间隙。同样,这不应该是一个问题,因为在serial
列中通常会出现间隙serial
- 如果定义了列
,则必须在结果中插入伪值并替换为非NULL
NULL
- 我们实际上不想插入伪行。我们可以稍后删除(在同一事务中),但这可能会有更多的副作用,比如删除时的触发器
。有一个更好的方法:
非空
约束
您必须为notnull
列提供伪值,因为():
NOTNULL约束总是复制到新表中
要么适应INSERT
语句中的约束,要么(更好)消除约束:
ALTER TABLE tmp_playlist_items
ALTER COLUMN foo DROP NOT NULL
, ALTER COLUMN bar DROP NOT NULL;
有一种快速且肮脏的方式具有超级用户权限:
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注入。这份声明是安全的。我已经发了
一般解决方案(9.4及以上版本)
我们可以将VALUES
子句直接附加到INSERT
,这允许使用DEFAULT
关键字。在(id)
上出现唯一违规的情况下,Postgres将更新。我们可以在更新中使用排除的行
关于冲突的中的SET
和WHERE
子句可以访问
使用表名称(或别名)的现有行,以及
建议使用特殊的排除表插入
以及:
请注意,插入前所有每行触发器的效果如下
反映在排除的值中,因为这些影响可能是原因之一
添加到要从插入中排除的行
剩余角盒
您有多种更新选项:您可以
- 。。。根本不更新:将
WHERE
子句添加到update
以仅写入选定行
- 。。。仅更新选定列
- 。。。仅当列当前为空时更新:
COALESCE(
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$$
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;
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;