我想在PostgreSQL中对表进行大量更新,但我不需要在整个操作中维护事务完整性,因为我知道我正在更改的列不会被写入或在更新期间阅读。我想知道在psql控制台中是否有一种简单的方法可以使这些类型的操作更快。
例如,假设我有一个名为“orders”的表,有3500万行,我想这样做:
UPDATE orders SET status = null;
为了避免被转移到offtopic讨论,让我们假设3500万列的所有状态值当前都设置为相同(非空)值,从而使索引无用。
此语句的问题是需要很长时间才能生效(仅因为锁定),并且所有更改的行都会被锁定,直到整个更新完成。此更新可能需要5个小时,而类似
UPDATE orders SET status = null WHERE (order_id > 0 and order_id < 1000000);
可能需要1分钟。超过3500万行,执行上述操作并将其分成35块只需要35分钟,为我节省4小时25分钟。
我可以使用脚本进一步细分(在这里使用伪代码):
for (i = 0 to 3500) {
db_operation ("UPDATE orders SET status = null
WHERE (order_id >" + (i*1000)"
+ " AND order_id <" + ((i+1)*1000) " + ")");
}
此操作可能仅在几分钟内完成,而不是35分钟。
所以这归结为我真正的要求。我不想写一个怪异的脚本来分解操作,每次我想做这样一个大的一次性更新。有没有办法在SQL中完成我想要的东西?
答案 0 :(得分:36)
...我不需要保持交易完整性 整个操作,因为我知道我改变的列是 在更新期间不会被写入或读取。
PostgreSQL's MVCC model中的任何UPDATE
都会写入新版 整行 。如果并发事务更改同一行的任何列,则会出现耗时的并发问题。 Details in the manual.了解相同的列并不会被并发事务所触及,这可以避免某些可能出现的并发症,而不是其他的。
为了避免被转移到离场讨论,让我们假设 目前设置了3500万列的所有状态值 到相同的(非空)值,从而使索引无用。
更新 整个表格 (或其主要部分)时,Postgres 从不使用索引 。当必须读取所有或大多数行时,顺序扫描更快。相反:索引维护意味着UPDATE
的额外成本。
例如,假设我有一个名为&#34; order&#34;有3500万 行,我想这样做:
UPDATE orders SET status = null;
我知道您的目标是提供更通用的解决方案(见下文)。但要解决 实际问题 问:无论表格大小如何,都可以在 几毫秒 中处理:
ALTER TABLE orders DROP column status
, ADD column status text;
使用
ADD COLUMN
添加列时,表中的所有现有行 使用列的默认值初始化(NULL
,如果没有DEFAULT
指定的子句)。如果没有DEFAULT子句,这只是元数据更改......
和
DROP COLUMN
表单不会物理删除列,只是简单地删除 使它对SQL操作不可见。随后插入和更新 表中的操作将为列存储空值。从而, 删除列很快但不会立即减少 表的磁盘大小,作为丢弃的空间占用的空间 列未回收。随着时间的推移,这个空间将被回收 现有行已更新。 (这些陈述不适用于 丢弃系统列;这是通过立即重写完成的。)
确保根据列(外键约束,索引,视图等)不具有对象。您需要删除/重新创建它们。除此之外,系统目录表pg_attribute
上的微小操作可以完成这项工作。在表上需要独占锁,这可能会导致繁重的并发负载问题。因为它只需要几毫秒,你应该还可以。
如果您要保留列默认值,请在单独的命令中将其添加回。在同一个命令中执行此操作会立即将其应用于所有行,从而消除效果。然后,您可以更新batches中的现有列。 请按照文档链接阅读手册中的 Notes 。
dblink
。它允许访问&#34; remote&#34; Postgres数据库隐式单独连接。 &#34;遥控器&#34;数据库可以是当前数据库,从而实现&#34;自治事务&#34; :函数在&#34; remote&#34;中写入的内容。 db已提交且无法回滚。
这允许运行单个函数,以更小的部分更新大表,并且每个部分都单独提交。避免为非常大的行构建事务开销,更重要的是,在每个部分之后释放锁。这允许并发操作在没有太多延迟的情况下继续进行,并且使死锁的可能性降低。
如果您没有并发访问权限,这几乎没有用 - 除了在异常后避免ROLLBACK
。对于这种情况,还要考虑SAVEPOINT
。
首先,许多小交易实际上更贵。这个 只适用于大表 。甜蜜点取决于很多因素。
如果您不确定自己在做什么: 单个交易是安全的方法 。为了使其正常工作,桌面上的并发操作必须发挥作用。例如:并发写入可以将行移动到应该已经处理过的分区。或并发读取可以看到不一致的中间状态。 你被警告了。
需要首先安装附加模块dblink:
与dblink建立连接非常依赖于数据库集群的设置和安全策略。这可能很棘手。相关的后续回答更多如何连接dblink :
按照指示创建 FOREIGN SERVER
和 USER MAPPING
以简化和简化连接(除非您已经有连接)。 />
假设serial PRIMARY KEY
有或没有差距。
CREATE OR REPLACE FUNCTION f_update_in_steps()
RETURNS void AS
$func$
DECLARE
_step int; -- size of step
_cur int; -- current ID (starting with minimum)
_max int; -- maximum ID
BEGIN
SELECT INTO _cur, _max min(order_id), max(order_id) FROM orders;
-- 100 slices (steps) hard coded
_step := ((_max - _cur) / 100) + 1; -- rounded, possibly a bit too small
-- +1 to avoid endless loop for 0
PERFORM dblink_connect('myserver'); -- your foreign server as instructed above
FOR i IN 0..200 LOOP -- 200 >> 100 to make sure we exceed _max
PERFORM dblink_exec(
$$UPDATE public.orders
SET status = 'foo'
WHERE order_id >= $$ || _cur || $$
AND order_id < $$ || _cur + _step || $$
AND status IS DISTINCT FROM 'foo'$$); -- avoid empty update
_cur := _cur + _step;
EXIT WHEN _cur > _max; -- stop when done (never loop till 200)
END LOOP;
PERFORM dblink_disconnect();
END
$func$ LANGUAGE plpgsql;
呼叫:
SELECT f_update_in_steps();
您可以根据需要参数化任何部分:表名,列名,值,......只需确保清理标识符以避免SQL注入:
关于避免空UPDATE:
答案 1 :(得分:4)
您应该将此列委托给另一个表,如下所示:
create table order_status (
order_id int not null references orders(order_id) primary key,
status int not null
);
然后你设置status = NULL的操作将是即时的:
truncate order_status;
答案 2 :(得分:3)
Postgres使用MVCC(多版本并发控制),因此如果您是唯一的编写者,则避免任何锁定;任何数量的并发读者都可以在桌面上工作,并且不会有任何锁定。
因此,如果确实需要5小时,则必须出于不同的原因(例如,您做并发写入,与您声称的不相同)。
答案 3 :(得分:3)
首先 - 您确定需要更新所有行吗?
也许有些行已经有status
NULL?
如果是,那么:
UPDATE orders SET status = null WHERE status is not null;
至于对更改进行分区 - 这在纯sql中是不可能的。所有更新都在单笔交易中。
在“纯sql”中执行此操作的一种可能方法是安装dblink,使用dblink连接到同一个数据库,然后通过dblink发出大量更新,但对于这样一个简单的任务来说似乎有点过分了。 / p>
通常只需添加适当的where
即可解决问题。如果没有 - 只需手动分区。编写脚本太多了 - 你通常可以用简单的单行编写:
perl -e '
for (my $i = 0; $i <= 3500000; $i += 1000) {
printf "UPDATE orders SET status = null WHERE status is not null
and order_id between %u and %u;\n",
$i, $i+999
}
'
为了便于阅读,我在这里包裹了一行,通常它只是一行。上述命令的输出可以直接输入psql:
perl -e '...' | psql -U ... -d ...
或者首先发送文件然后发送到psql(如果以后需要该文件):
perl -e '...' > updates.partitioned.sql
psql -U ... -d ... -f updates.partitioned.sql
答案 4 :(得分:3)
我会使用CTAS:
begin;
create table T as select col1, col2, ..., <new value>, colN from orders;
drop table orders;
alter table T rename to orders;
commit;
答案 5 :(得分:2)
我绝不是DBA,但是经常需要更新3500万行的数据库设计可能会有......问题。
一个简单的WHERE status IS NOT NULL
可能会加速一些事情(如果你有一个状态索引) - 不知道实际的用例,我假设这是经常运行的,很大一部分35百万行可能已经处于空状态。
但是,您可以通过LOOP statement在查询中创建循环。我只是做一个小例子:
CREATE OR REPLACE FUNCTION nullstatus(count INTEGER) RETURNS integer AS $$
DECLARE
i INTEGER := 0;
BEGIN
FOR i IN 0..(count/1000 + 1) LOOP
UPDATE orders SET status = null WHERE (order_id > (i*1000) and order_id <((i+1)*1000));
RAISE NOTICE 'Count: % and i: %', count,i;
END LOOP;
RETURN 1;
END;
$$ LANGUAGE plpgsql;
然后可以通过类似于:
的操作来运行它SELECT nullstatus(35000000);
您可能希望选择行计数,但请注意确切的行计数可能需要很长时间。 PostgreSQL wiki有一篇关于slow counting and how to avoid it的文章。
另外,RAISE NOTICE部分就是跟踪脚本的距离。如果您没有监控通知,或者不在乎,最好将其删除。
答案 6 :(得分:2)
你确定这是因为锁定吗?我不这么认为,还有很多其他可能的原因。要找出答案,你总是可以尝试做锁定。试试这个: 开始; SELECT NOW(); SELECT * FROM order FOR UPDATE; SELECT NOW(); ROLLBACK;
要了解实际发生的情况,首先应运行EXPLAIN(EXPLAIN UPDATE命令SET状态...)和/或EXPLAIN ANALYZE。也许你会发现你没有足够的内存来有效地进行UPDATE。如果是这样,SET work_mem TO'xxxMB';可能是一个简单的解决方案。
另外,请关闭PostgreSQL日志,看看是否出现了一些与性能相关的问题。
答案 7 :(得分:1)
尚未提及的一些选项:
使用new table技巧。在你的情况下你可能需要做的就是编写一些触发器来处理它,以便原始表的更改也会传播到你的表副本,就像这样......(percona就是一个例子做触发方式的东西)。另一种选择可能是&#34;创建一个新列然后用它替换旧列&#34; trick,以避免锁定(不清楚是否有助于提高速度)。
可能会计算最大ID,然后生成&#34;您需要的所有查询&#34;并将它们作为单个查询传递给# home_controller.rb
class HomeController < ApplicationController
def index; end
end
然后它可能没有那么多锁定,仍然是所有SQL,尽管你事先有额外的逻辑来执行它:(
答案 8 :(得分:0)
PostgreSQL版本11通过Fast ALTER TABLE ADD COLUMN with a non-NULL default功能自动为您处理。请尽可能升级到版本11。
此blog post中提供了解释。