更新/插入操作的最佳临时表策略

时间:2015-05-12 14:23:00

标签: sql performance postgresql database-design rails-postgresql

我给出了一个来自远程服务器的事务记录列表,其中一些已存在于我们的数据库中,其中一些是新的。我的任务是更新已经存在的那些并插入那些不存在的那些。假设事务具有不依赖于我的本地数据库的远程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. 创建仅在事务生命周期内存在的临时表。
  2. 将我的所有记录转储到临时表中。
  3. 在单个语句中执行所有更新,同时从临时表中删除更新的记录。
  4. 将临时表中剩余的内容插入永久表中,因为它不是更新。
  5. 但后来我开始怀疑使用每会话临时表是否更有效,而不是将所有操作包装在单个事务中。我的数据库会话只会由单个线程使用,所以这应该是可能的:

    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. 这样可以避免每次收到新批记录时都必须创建临时表。相反,如果已经使用此数据库会话(很可能)处理了批次,则表格已经存在。

    2. 这节省了回滚空间,因为我没有在单个事务中将多个操作串联在一起。并不要求整个更新/插入操作是原子的;我使用事务的唯一原因是临时表会在提交时自动删除。

    3. 后一种方法可能优于前者吗?两种方法都有任何特殊的"陷阱"我应该知道吗?

1 个答案:

答案 0 :(得分:1)

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

upserts的最大问题是并发性问题,如下所述:http://www.depesz.com/2012/06/10/why-is-upsert-so-complicated/和此处:http://johtopg.blogspot.com.br/2014/04/upsertisms-in-postgres.html

我认为你的方法很好,虽然我根本不会使用临时表,并将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+将支持开箱即用的并发upsert,使用INSERT ... ON CONFLICT DOHING / UPDATE语法。