有人可以解释执行数百万个更新的奇怪行为吗?

Nik*_*l N 8 postgresql

有人可以向我解释这种行为吗?我在 OS X 上本机运行的 Postgres 9.3 上运行了以下查询。我试图模拟一些行为,其中索引大小可能增长得比表大小大得多,但发现了一些更奇怪的东西。

CREATE TABLE test(id int);
CREATE INDEX test_idx ON test(id);

CREATE FUNCTION test_index(batch_size integer, total_batches integer) RETURNS void AS $$
DECLARE
  current_id integer := 1;
BEGIN
FOR i IN 1..total_batches LOOP
  INSERT INTO test VALUES (current_id);
  FOR j IN 1..batch_size LOOP
    UPDATE test SET id = current_id + 1 WHERE id = current_id;
    current_id := current_id + 1;
  END LOOP;
END LOOP;
END;
$$ LANGUAGE plpgsql;

SELECT test_index(500, 10000);
Run Code Online (Sandbox Code Playgroud)

我让它在我的本地机器上运行了大约一个小时,然后我开始从 OS X 收到磁盘问题警告。我注意到 Postgres 从我的本地磁盘中吸收了大约 10MB/s,并且 Postgres 数据库正在消耗总计来自我的机器的 30GB。我最终取消了查询。无论如何,Postgres 没有将磁盘空间返回给我,我查询了数据库的使用情况统计信息,结果如下:

test=# SELECT nspname || '.' || relname AS "relation",
    pg_size_pretty(pg_relation_size(C.oid)) AS "size"
  FROM pg_class C
  LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
  WHERE nspname NOT IN ('pg_catalog', 'information_schema')
  ORDER BY pg_relation_size(C.oid) DESC
  LIMIT 20;

           relation            |    size
-------------------------------+------------
 public.test                   | 17 GB
 public.test_idx               | 14 GB
Run Code Online (Sandbox Code Playgroud)

但是,从表中选择没有产生任何结果。

test=# select * from test limit 1;
 id
----
(0 rows)
Run Code Online (Sandbox Code Playgroud)

运行 10000 批 500 是 5,000,000 行,这应该会产生一个非常小的表/索引大小(以 MB 为单位)。我怀疑 Postgres 正在为函数发生的每个 INSERT/UPDATE 创建表/索引的新版本,但这似乎很奇怪。整个函数以事务方式运行,并且表开始时为空。

关于为什么我会看到这种行为的任何想法?

具体来说,我的两个问题是:为什么这个空间还没有被数据库回收,第二个是为什么数据库首先需要这么多空间?即使考虑到 MVCC,30GB 似乎也很多

小智 7

精简版

乍一看,您的算法看起来是 O(n*m),但实际上增长了 O(n * m^2),因为所有行都具有相同的 ID。您得到的不是 5M 行,而是 >1.25G 行

长版

您的函数位于隐式事务中。这就是为什么在取消查询后看不到数据的原因,也是为什么它需要为两个循环维护不同版本的更新/插入元组。

此外,我怀疑您的逻辑存在错误或低估了所做的更新数量。

外循环的第一次迭代 - current_id 从 1 开始,插入 1 行,然后内循环对同一行执行 10000 次更新,最后只有一行显示 ID 为 10001,current_id 的值为 10001。10001由于事务尚未完成,因此仍保留行的版本。

外循环的第二次迭代 - 由于 current_id 为 10001,因此插入了 ID 为 10001 的新行。现在您有 2 行具有相同的“ID”,并且两行共有 10003 个版本(第一行的 10002,其中的 1第二个)。然后内部循环更新两行 10000 次,创建 20000 个新版本,到目前为止达到 30003 个元组......

外循环的第三次迭代:当前 ID 为 20001,插入一个 ID 为 20001 的新行。您有 3 行,所有行都具有相同的“ID” 20001、30006 行/元组版本。然后你执行 3 行的 10000 次更新,创建 30000 个新版本,现在 60006 ...

...

(如果您的空间允许) - 外循环的第 500 次迭代,仅在此迭代中创建 500 行的 5M 更新

如您所见,您获得了 1000 + 2000 + 3000 + ... + 4990000 + 5000000 次更新(加上更改),而不是预期的 500 万次更新,即 10000 * (1+2+3+...+499+ 500),超过 1.25G 的更新。当然,一行不仅仅是 int 的大小,它还需要一些额外的结构,因此您的表和索引的大小超过 10 GB。

相关问答:


Eva*_*oll 5

PostgreSQL 只在 之后返回磁盘空间VACUUM FULL,而不是在DELETEor 之后ROLLBACK(作为取消的结果)

VACUUM 的标准形式删除表和索引中的死行版本,并标记可用空间以供将来重用。但是,它不会将空间返回给操作系统,除非在表末尾的一个或多个页面完全空闲并且可以轻松获得排他表锁的特殊情况下。相比之下,VACUUM FULL 通过编写一个没有死空间的完整新版本的表文件来主动压缩表。这最大限度地减少了表的大小,但可能需要很长时间。它还需要额外的磁盘空间用于表的新副本,直到操作完成。

作为旁注,您的整个功能似乎有问题。我不确定你要测试什么,但如果你想创建数据,你可以使用generate_series

INSERT INTO test
SELECT x FROM generate_series(1, batch_size*total_batches) AS t(x);
Run Code Online (Sandbox Code Playgroud)

  • 是的,但我的印象是 MVCC 不应该为它在事务过程中修改的所有元组创建元组。也就是说,当第一次 INSERT 运行 Postgres 时,它会创建一个元组,并为每个 UPDATE 添加一个新元组。由于 UPDATES 为每行运行 500 次,并且有 10000 个 INSERT,这在事务提交时相当于 500*10000 行 = 5M 元组。现在这只是一个估计,但不管 5M * 说 50 字节来跟踪每个元组 ~= 250MB,这远小于 30GB。这一切从何而来? (2认同)