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


8

有人可以向我解释这种行为吗?我在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);

在开始从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

但是,从表中选择没有结果。

test=# select * from test limit 1;
 id
----
(0 rows)

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

对为什么我看到这种行为有任何想法吗?

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

Answers:


7

简洁版本

乍一看,您的算法看起来为O(n * m),但由于所有行都具有相同的ID,因此有效地增大了O(n * m ^ 2)。而不是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更新(加上更改),而不是预期的5M更新,即10000 *(1 + 2 + 3 + ... + 499+ 500),超过1.25G的更新。当然,一行不仅是int的大小,还需要一些其他结构,因此表和索引的大小超过10 GB。

相关问答:


5

PostgreSQL仅VACUUM FULLDELETE或之后ROLLBACK(作为取消的结果)之后不返回磁盘空间。

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

附带说明一下,您的整个功能似乎有问题。我不确定您要测试什么,但是如果您要创建数据,则可以使用generate_series

INSERT INTO test
SELECT x FROM generate_series(1, batch_size*total_batches) AS t(x);

很酷,这解释了为什么表仍被标记为消耗大量数据,但是为什么它首先需要所有这些空间?根据我对MVCC的理解,它需要为事务维护更新/插入的元组的不同版本,但是它不需要为循环的每次迭代维护单独的版本。
Nikhil N

1
循环的每次迭代都会生成新的元组。
埃文·卡罗尔

2
是的,但我的印象是,MVCC不应为在交易过程中修改的所有元组创建元组。也就是说,当第一个INSERT运行时,Postgres将创建一个元组,并为每个UPDATE添加一个新的元组。由于每行更新都要运行500次,并且有10000个INSERT,因此在提交事务时,这等于500 * 10000行= 5M元组。现在这只是一个估计,但是无论5M *说要跟踪每个元组〜= 250MB都需要50个字节,这远远少于30GB。都是哪里来的?
Nikhil N

还是re:可疑函数,当索引字段以单调递增的方式多次更新时,我试图测试索引的行为,从而产生非常稀疏的索引,但是索引总是附加在磁盘上。
Nikhil N

我对你的想法感到困惑。您是否认为在循环中更新18e次的行是一个元组还是1e8元组?
埃文·卡罗尔

3

分析函数后的实际数字要大得多,因为表的所有行都具有相同的值,该值在每次迭代中都会多次更新。

当我们使用参数n和运行它时m

SELECT test_index(n, m);

m行插入和n * (m^2 + m) / 2更新。因此,对于n = 500m = 10000,Postgres将只需要插入1万行,但执行约25G(250亿个)元组更新。

考虑到Postgres中的一行有大约24个字节的开销,因此只有一int列的表每行将需要28个字节加上页面开销。因此,要完成操作,我们需要大约700GB的空间以及用于索引的空间(也将是数百GB)。


测试中

为了验证该理论,我们创建了另一个test_test具有一行的表。

CREATE TABLE test_test (i int not null) ;
INSERT INTO test_test (i) VALUES (0);

然后,我们添加一个触发器,test以便每次更新都会使计数器增加1。(省略代码)。然后,我们我们运行的功能,数值越小,n = 50m = 100

我们的理论预测

  • 100行插入
  • 25万个元组更新(252500 = 50 * 100 * 101/2)
  • 磁盘上的表至少需要7MB
  • (+索引空间)

测试1(原始test表,带索引)

    SELECT test_index(50, 100) ;

完成后,我们检查表内容:

x=# SELECT COUNT(*) FROM test ;
 count 
-------
   100
(1 row)

x=# SELECT i FROM test_test ;
   i    
--------
 252500
(1 row)

和磁盘使用情况(在“ 索引维护”中的“ 索引大小/使用情况统计信息”下查询):

tablename | indexname | num_rows | table_size | index_size | unique | number_of_scans | tuples_read 
----------+-----------+----------+------------+------------+--------+-----------------+-------------
test      | test_idx  |      100 | 8944 kB    | 5440 kB    | N      |           10001 |      505003
test_test |           |        1 | 8944 kB    |            | N      |                 |            

test表已将近9MB用于表,将5MB用于索引。请注意,该test_test表又使用了9MB!这是可以预期的,因为它还经历了25万次更新(我们的第二个触发器针对中的一行的test_test每次更新更新了的一行)test

还请注意在表上的扫描次数test(10K),元组的读取数(500K)。

测试2(test表无索引)

与上表完全相同,除了表没有索引。

tablename | indexname | num_rows | table_size | index_size | unique | number_of_scans | tuples_read 
----------+-----------+----------+------------+------------+--------+-----------------+-------------
 test        |        |      100 | 8944 kB    |            | N      |                 |            
 test_test   |        |        1 | 8944 kB    |            | N      |                 |            

该表的磁盘使用量大小相同,而索引的磁盘使用量当然没有大小。表上的扫描次数test为零,但元组也读取。

测试3(具有较低的填充系数)

尝试使用fillfactor 50和最低的10。根本没有改善。磁盘使用率几乎与以前的测试相同(使用默认填充因子100%)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.