Postgres:upsert一行并更新主键列

时间:2017-03-29 20:54:48

标签: sql postgresql merge upsert

假设我的Postgres数据库中有两个表:

create table transactions
(
    id bigint primary key,
    doc_id bigint not null,
    -- lots of other columns...
    amount numeric not null
);

-- same columns
create temporary table updated_transactions
(
    id bigint primary key,
    doc_id bigint not null,
    -- lots of other columns...
    amount numeric not null
);

两个表只有一个主键,没有唯一索引。

我需要使用以下规则将updated_transactions中的行追加到transactions中:

  • id transactionsupdated_transactions中的列值不匹配
  • doc_id等其他列(amount除外)应匹配
  • 找到匹配的行时,同时更新amountid
  • 找不到匹配的行时,插入
id中的

updated_transactions值来自序列。 业务对象只填充updated_transactions然后合并 使用upsert查询将新行或更新的行放入transactions。 所以我原来保持不变的交易保持id完整,并保持更新 被分配了新的id s。

在MSSQL和Oracle中,它将是merge语句,类似于:

merge into transactions t
using updated_transactions ut on t.doc_id = ut.doc_id, ...
when matched then
    update set t.id = ut.id, t.amount = ut.amount
when not matched then
    insert (t.id, t.doc_id, ..., t.amount)
    values (ut.id, ut.doc_id, ..., ut.amount);

在PostgreSQL中,我想它应该是这样的:

insert into transactions(id, doc_id, ..., amount)
select coalesce(t.id, ut.id), ut.doc_id, ... ut.amount
from updated_transactions ut
left join transactions t on t.doc_id = ut.doc_id, ....
    on conflict
    on constraint transactions_pkey
    do update
    set amount = excluded.amount, id = excluded.id

问题在于do update子句:excluded.id是一个旧值 来自transactions表格,而我需要updated_transactions的新值。

对于ut.id子句,

do update值是不可访问的,而且我唯一能做到的 use是excluded行。但是excluded行只有coalesce(t.id, ut.id) 表达式,返回现有行的旧id值。

是否可以使用upsert查询更新idamount列?

2 个答案:

答案 0 :(得分:2)

在您用作键的那些列上创建唯一索引,并在upsert表达式中传递其名称,以便它使用它而不是pkey。 然后,如果没有找到匹配项,它将使用updated_transactions中的ID插入行。如果找到匹配项,则可以使用excluded.id从updated_transactions获取ID。

我认为left join transactions是多余的。

所以看起来有点像这样:

insert into transactions(id, doc_id, ..., amount)
select ut.id, ut.doc_id, ... ut.amount
from updated_transactions ut
    on conflict
    on constraint transactions_multi_column_unique_index
    do update
    set amount = excluded.amount, id = excluded.id

答案 1 :(得分:1)

看起来可以使用writable CTEs而不是普通的upsert来完成任务。

首先,我会发布更简单的查询版本,以回答原始问题。此解决方案假定doc_id, unit_id列代表候选键,但不要求在这些列上使用唯一索引。

测试数据:

create temp table transactions
(
    id bigint primary key,
    doc_id bigint,
    unit_id bigint,
    amount numeric
);

create temp table updated_transactions
(
    id bigint primary key,
    doc_id bigint,
    unit_id bigint,
    amount numeric
); 

insert into transactions(id, doc_id, unit_id, amount)
values (1, 1, 1, 10), (2, 1, 2, 15), (3, 1, 3, 10);

insert into updated_transactions(id, doc_id, unit_id, amount)
values (6, 1, 1, 11), (7, 1, 2, 15), (8, 1, 4, 20); 

updated_transactions合并到transactions的查询:

with new_values as
(
    select ut.id new_id, t.id old_id, ut.doc_id, ut.unit_id, ut.amount 
    from updated_transactions ut
    left join transactions t 
        on t.doc_id = ut.doc_id and t.unit_id = ut.unit_id
),
updated as
(
    update transactions tr
    set id = nv.new_id, amount = nv.amount
    from new_values nv
    where id = nv.old_id
    returning tr.*
)
insert into transactions(id, doc_id, unit_id, amount)
select ut.new_id, ut.doc_id, ut.unit_id, ut.amount
from new_values ut
where ut.new_id not in (select id from updated);

结果:

select * from transactions

-- id | doc_id | unit_id | amount
------+--------+---------+-------
--  3 |   1    |    3    |  10    -- not changed
--  6 |   1    |    1    |  11    -- updated
--  7 |   1    |    2    |  15    -- updated 
--  8 |   1    |    4    |  20    -- inserted

在我的真实应用中doc_id, unit_id并不总是唯一的,因此它们不代表候选键。为了匹配行,我考虑了行号,为按id s排序的行计算。所以这是我的第二个解决方案。

测试数据:

-- the tables are the same as above
insert into transactions(id, doc_id, unit_id, amount)
values (1, 1, 1, 10), (2, 1, 1, 15), (3, 1, 3, 10);

insert into updated_transactions(id, doc_id, unit_id, amount)
values (6, 1, 1, 11), (7, 1, 1, 15), (8, 1, 4, 20); 

合并查询:

with trans as
(
    select id, doc_id, unit_id, amount,
        row_number() over(partition by doc_id, unit_id order by id) row_num
    from transactions
),
updated_trans as
(
    select id, doc_id, unit_id, amount,
        row_number() over(partition by doc_id, unit_id order by id) row_num
    from updated_transactions
),
new_values as
(
    select ut.id new_id, t.id old_id, ut.doc_id, ut.unit_id, ut.amount 
    from updated_trans ut
    left join trans t 
        on t.doc_id = ut.doc_id and t.unit_id = ut.unit_id and t.row_num = ut.row_num
),
updated as
(
    update transactions tr
    set id = nv.new_id, amount = nv.amount
    from new_values nv
    where id = nv.old_id
    returning tr.*
)
insert into transactions(id, doc_id, unit_id, amount)
select ut.new_id, ut.doc_id, ut.unit_id, ut.amount
from new_values ut
where ut.new_id not in (select id from updated);

结果:

select * from transactions;

-- id | doc_id | unit_id | amount
------+--------+---------+-------
--  3 |   1    |    3    | 10     -- not changed
--  6 |   1    |    1    | 11     -- updated
--  7 |   1    |    1    | 15     -- updated
--  8 |   1    |    4    | 20     -- inserted

参考文献: