update/insert 操作的最佳温度 table 策略

best temp table strategy for update/insert operation

我从远程服务器得到了一份交易记录列表,其中一些已经存在于我们的数据库中,一些是新的。我的任务是更新已经存在的并插入不存在的。假设事务具有不依赖于我的本地数据库的远程 ID。列表的大小可以是 1 到 ~500 之间的任何值。

数据库是postgresql。

我最初的想法是这样的:

BEGIN
  CREATE TEMP TABLE temp_transactions (LIKE transactions) ON COMMIT DROP;
  INSERT INTO temp_transactions(...) VALUES (...);
  WITH updated_transactions AS (...update statement...) 
    DELETE FROM temp_transactions USING updated_transactions 
    WHERE temp_transactions.external_id = updated_transactions.external_id;
  INSERT INTO transactions SELECT ... FROM temp_transactions;
COMMIT;

换句话说:

  1. 创建一个仅在事务生命周期内存在的临时文件 table。
  2. 将我所有的记录转储到临时 table。
  3. 在一条语句中完成所有更新,同时从临时文件中删除更新的记录 table。
  4. 将临时 table 中剩余的所有内容插入永久 table,因为它不是更新。

但后来我开始怀疑使用每会话临时 table 并且不将所有操作包装在单个事务中是否更有效。我的数据库会话只会被单个线程使用,所以这应该是可能的:

CREATE TEMP TABLE temp_transactions IF NOT EXISTS (LIKE transactions);
INSERT INTO temp_transactions(...) VALUES (...);
WITH updated_transactions AS (...update statement...) 
  DELETE FROM temp_transactions USING updated_transactions 
  WHERE temp_transactions.external_id = updated_transactions.external_id;
INSERT INTO transactions SELECT ... FROM temp_transactions;
TRUNCATE temp_transactions;

我的想法:

  1. 这避免了每次收到新的一批记录时都必须创建临时文件 table。相反,如果已经使用此数据库会话处理了一个批次(这很可能),则 table 将已经存在。

  2. 这节省了回滚 space 因为我没有在单个事务中将多个操作串在一起。不要求整个 update/insert 操作是原子的;我使用事务的唯一原因是临时 table 会在提交时自动删除。

后一种方法可能优于前一种方法吗?这两种方法有什么特殊的 "gotchas" 我应该注意的吗?

您所描述的通常称为 upsert。甚至官方文档也提到了它,在这里:http://www.postgresql.org/docs/current/static/plpgsql-control-structures.html#PLPGSQL-UPSERT-EXAMPLE

更新插入的最大问题是并发问题,如下所述:http://www.depesz.com/2012/06/10/why-is-upsert-so-complicated/ and here: http://johtopg.blogspot.com.br/2014/04/upsertisms-in-postgres.html

我认为你的方法很好,虽然我根本不会使用临时 table,而是将 VALUES 部分放入 UPDATE 部分,使整个事情成为一个语句。

像这样:

CREATE TABLE test (id int, data int);
CREATE TABLE

WITH new_data (id, data) AS (
    VALUES (1, 2), (2, 6), (3, 10)
),
updated AS (
    UPDATE test t
    SET data = v.data
    FROM new_data v
    WHERE v.id = t.id
    RETURNING t.id
)
INSERT INTO test
SELECT *
FROM new_data v
WHERE NOT EXISTS (
    SELECT 1
    FROM updated u
    WHERE u.id = v.id
);
INSERT 0 3


SELECT * FROM test;
 id | data 
----+------
  1 |    2
  2 |    6
  3 |   10
(3 rows)

WITH new_data (id, data) AS (
    VALUES (1, 20), (2, 60), (4, 111)
),
updated AS (
    UPDATE test t
    SET data = v.data
    FROM new_data v
    WHERE v.id = t.id
    RETURNING t.id
)
INSERT INTO test
SELECT *
FROM new_data v
WHERE NOT EXISTS (
    SELECT 1
    FROM updated u
    WHERE u.id = v.id
);
INSERT 0 1

SELECT * FROM test;
 id | data 
----+------
  3 |   10
  1 |   20
  2 |   60
  4 |  111
(4 rows)

PG 9.5+ 将支持开箱即用的并发更新插入,使用 INSERT ... ON CONFLICT DO NOTHING/UPDATE 语法。