【问题标题】:Bulk/batch update/upsert in PostgreSQLPostgreSQL 中的批量/批量更新/更新插入
【发布时间】:2011-08-11 01:23:11
【问题描述】:

我正在编写一个 Django-ORM 增强功能,它尝试缓存模型并将模型保存推迟到事务结束。这一切都差不多完成了,但是我在 SQL 语法上遇到了一个意想不到的困难。

我不是什么 DBA,但据我了解,对于许多小型查询,数据库并不能真正有效地工作。很少有更大的查询会好得多。例如,最好使用大批量插入(比如一次 100 行)而不是 100 个单行。

现在,据我所知,SQL 并没有真正提供任何语句来对表执行批量更新。这个术语似乎令人困惑,所以我将解释我的意思。我有一个任意数据数组,每个条目描述表中的一行。我想更新表中的某些行,每行都使用数组中相应条目中的数据。这个想法与批量插入非常相似。

例如:我的表可能有两列 "id""some_col"。现在,描述批量更新数据的数组包含三个条目(1, 'first updated')(2, 'second updated')(3, 'third updated')。更新前,该表包含以下行:(1, 'first')(2, 'second')(3, 'third')

我偶然发现了这个帖子:

Why are batch inserts/updates faster? How do batch updates work?

这似乎可以满足我的要求,但是我无法真正弄清楚最后的语法。

我还可以删除所有需要更新的行,然后使用批量插入重新插入它们,但是我很难相信这实际上会表现得更好。

我使用 PostgreSQL 8.4,所以这里也可以使用一些存储过程。然而,由于我计划最终开源该项目,因此欢迎任何更多可移植的想法或方法在不同的 RDBMS 上做同样的事情。

后续问题:如何进行批量“insert-or-update”/“upsert”语句?

测试结果

我在 4 个不同的表中执行了 100 次 10 次插入操作(因此总共 1000 次插入)。我在带有 PostgreSQL 8.4 后端的 Django 1.3 上进行了测试。

这些是结果:

  • 通过 Django ORM 完成的所有操作 - 每次通过 ~2.45 秒
  • 相同的操作,但不使用 Django ORM 完成 - 每次传递 ~1.48 秒
  • 仅插入操作,不向数据库查询序列值~0.72 秒
  • 仅插入操作,以 10 个块为单位执行(共 100 个块)~0.19 秒
  • 仅插入操作,一大块执行块~0.13 秒
  • 仅插入操作,每个块大约 250 条语句,~0.12 秒

结论:在单个connection.execute()中执行尽可能多的操作。 Django 本身引入了大量开销。

免责声明:我没有引入除默认主键索引之外的任何索引,因此插入操作可能会因此运行得更快。

【问题讨论】:

  • +1 因为我了解到“upsert”是一个真实的词(它甚至在wikipedia 上)
  • 你能告诉我们你正在运行的语句吗?我对“批量更新”一词有点困惑?你不能用一个 UPDATE 语句来做所有的更新吗?在 9.1 中,您还可以使用可写 CTE 在单个操作中执行 UPDATE 和 INSERT

标签: sql database postgresql insert sql-update


【解决方案1】:

批量插入

您可以通过@Ketema 修改三列的批量插入:

INSERT INTO "table" (col1, col2, col3)
  VALUES (11, 12, 13) , (21, 22, 23) , (31, 32, 33);

变成:

INSERT INTO "table" (col1, col2, col3)
  VALUES (unnest(array[11,21,31]), 
          unnest(array[12,22,32]), 
          unnest(array[13,23,33]))

用占位符替换值:

INSERT INTO "table" (col1, col2, col3)
  VALUES (unnest(?), unnest(?), unnest(?))

您必须将数组或列表作为参数传递给此查询。这意味着您可以在不进行字符串连接的情况下进行大量的批量插入(以及所有的麻烦和危险:sql 注入和引用地狱)。

批量更新

PostgreSQL 已将 FROM 扩展添加到 UPDATE。你可以这样使用它:

update "table" 
  set value = data_table.new_value
  from 
    (select unnest(?) as key, unnest(?) as new_value) as data_table
  where "table".key = data_table.key;

该手册缺少很好的解释,但postgresql-admin mailing list 上有一个示例。我试图详细说明:

create table tmp
(
  id serial not null primary key,
  name text,
  age integer
);

insert into tmp (name,age) 
values ('keith', 43),('leslie', 40),('bexley', 19),('casey', 6);

update tmp set age = data_table.age
from
(select unnest(array['keith', 'leslie', 'bexley', 'casey']) as name, 
        unnest(array[44, 50, 10, 12]) as age) as data_table
where tmp.name = data_table.name;
 

StackExchange 上还有 other posts 解释 UPDATE...FROM.. 使用 VALUES 子句而不是子查询。它们可能更易于阅读,但仅限于固定数量的行。

【讨论】:

  • 发现 update ... from 示例特别有用,谢谢。
  • 我可以保证这种批量插入方法。当用逗号分隔多个值语句与完全独立的插入语句时,我注意到了一个数量级的改进。我的特定插入在几分钟内完成,而不是像以前那样一个小时。
  • 对于如上所示的插入用法,是否可以添加“ON CONFLICT”子句?如果是这样,它的作用是什么?
【解决方案2】:

我使用了 3 种策略来进行批量事务处理:

  1. 动态生成 SQL 语句,用分号连接它们,然后一次性提交语句。我以这种方式完成了多达 100 次插入,而且效率很高(针对 Postgres 完成)。
  2. JDBC 具有内置的批处理功能(如果已配置)。如果您生成事务,则可以刷新 JDBC 语句,以便它们一次性进行事务。这种策略需要更少的数据库调用,因为语句都是在一个批次中执行的。
  3. Hibernate 也支持 JDBC 批处理,但在这种情况下,您对 Hibernate Session 执行 flush() 方法,而不是底层的 JDBC 连接。它完成与 JDBC 批处理相同的事情。

顺便说一下,Hibernate 还支持集合获取中的批处理策略。如果您使用@BatchSize 注释集合,则在获取关联时,Hibernate 将使用IN 而不是=,从而导致加载集合的SELECT 语句更少。

【讨论】:

  • 谢谢,执行许多用分号分隔的INSERT / UPDATE 语句听起来是个好主意。事实上,Django ORM 的简单性只是单独执行每个操作。
  • 事实证明这是一个非常好的优化。我已经在上面发布了结果。
【解决方案3】:

批量插入可以这样完成:

INSERT INTO "table" ( col1, col2, col3)
  VALUES ( 1, 2, 3 ) , ( 3, 4, 5 ) , ( 6, 7, 8 );

将插入 3 行。

多重更新由 SQL 标准定义,但未在 PostgreSQL 中实现。

引用:

"根据标准,column-list 语法应该允许一个列表 要从单个行值表达式分配的列数,例如 一个子选择:

更新帐户 SET (contact_last_name, contact_first_name) = (从销售人员中选择姓氏、名字 WHERE salesmen.id = accounts.sales_id);"

参考:http://www.postgresql.org/docs/9.0/static/sql-update.html

【讨论】:

【解决方案4】:

将 json 填充到记录集中非常快(postgresql 9.3+)

big_list_of_tuples = [
    (1, "123.45"),
    ...
    (100000, "678.90"),
]

connection.execute("""
    UPDATE mytable
    SET myvalue = Q.myvalue
    FROM (
        SELECT (value->>0)::integer AS id, (value->>1)::decimal AS myvalue 
        FROM json_array_elements(%s)
    ) Q
    WHERE mytable.id = Q.id
    """, 
    [json.dumps(big_list_of_tuples)]
)

【讨论】:

  • 没有临时表的好解决方案。应该是cursor.execute ...
【解决方案5】:

关闭自动提交,最后只提交一次。在普通 SQL 中,这意味着在开始时发出 BEGIN,在结束时发出 COMMIT。您需要创建一个 function 才能进行实际的 upsert。

【讨论】:

  • 语句在事务中运行。问题是它们被一个一个地发送到数据库,结果证明效率非常低。
猜你喜欢
  • 1970-01-01
  • 2012-03-19
  • 2012-02-16
  • 1970-01-01
  • 2016-05-02
  • 2010-11-03
  • 2014-06-22
  • 2014-01-16
  • 2011-09-11
相关资源
最近更新 更多