誰かが何百万もの更新を実行している奇妙な行動を説明できますか?


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からディスクの問題の警告を受け取る前に、ローカルマシンでこれを約1時間実行しました。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)

500の10000バッチを実行すると5,000,000行になるため、テーブル/インデックスのサイズはかなり小さくなります(MBのスケールで)。Postgresが関数で起こっているINSERT / UPDATEごとにテーブル/インデックスの新しいバージョンを作成していると思いますが、これは奇妙に見えます。関数全体がトランザクションで実行され、開始時にテーブルは空でした。

なぜ私はこの動作を見ているのですか?

具体的には、私が持っている2つの質問は次のとおりです。なぜこのスペースがデータベースによってまだ再利用されていないのですか。MVCCを考慮に入れても、30 GBは多くのようです

回答:


7

短縮版

アルゴリズムは一見O(n * m)に見えますが、すべての行が同じIDを持っているため、O(n * m ^ 2)を効果的に大きくします。500万行の代わりに、> 1.25G行を取得しています。

ロングバージョン

関数が暗黙のトランザクション内にあります。クエリをキャンセルした後にデータが表示されないのはそのためです。また、両方のループで更新/挿入されたタプルの異なるバージョンを維持する必要があるのはなぜですか。

さらに、ロジックにバグがあるか、行われた更新の数を過小評価していると思います。

外側のループの最初の反復-current_idは1から始まり、1行挿入します。次に、内側のループが同じ行に対して10000回更新を実行し、IDが10001のcurrent_idを示す唯一の行と10001のcurrent_idで終了します。10001トランザクションが終了していないため、行のバージョンは保持されます。

外部ループの2回目の反復-current_idが10001であるため、ID 10001で新しい行が挿入されます。これで、同じ「ID」を持つ2つの行と、両方の行の合計で10003バージョン(最初の行の10002、 2つ目)。次に、内部ループは両方の行を10000回更新し、20000の新しいバージョンを作成し、これまでに30003タプルを取得します...

外部ループの3番目の反復:現在のIDは20001で、新しい行がID 20001で挿入されます。3つの行があり、すべて同じ「ID」20001、30006行/タプルバージョンがこれまでにあります。次に、3行の10000回の更新を実行し、30000個の新しいバージョンを作成します。現在は60006 ...

...

(スペースに余裕があった場合)-外側のループの500回目の反復で、この反復だけで500行の5M更新を作成します

ご覧のとおり、予想される5Mの更新の代わりに、1000 + 2000 + 3000 + ... + 4990000 + 5000000の更新(および変更)があり、10000 *(1 + 2 + 3 + ... + 499+)になります。 500)、1.25G以上のアップデート。そしてもちろん、行はintのサイズだけではなく、追加の構造が必要なので、テーブルとインデックスは10ギガバイトを超えるサイズになります。

関連するQ&A:


5

PostgreSQLはVACUUM FULLDELETEまたはの後ではなく、の後にのみディスク領域を返しますROLLBACK(キャンセルの結果として)

VACUUMの標準形式は、テーブルとインデックスの不要な行のバージョンを削除し、将来の再利用に利用できるスペースをマークします。ただし、テーブルの最後にある1つ以上のページが完全に解放されて、排他的なテーブルロックを簡単に取得できる特別な場合を除いて、オペレーティングシステムにスペースは返されません。対照的に、VACUUM FULLは、デッドスペースのない完全な新しいバージョンのテーブルファイルを書き込むことにより、テーブルをアクティブに圧縮します。これによりテーブルのサイズが最小化されますが、時間がかかる場合があります。また、操作が完了するまで、テーブルの新しいコピー用に追加のディスク領域が必要です。

余談ですが、機能全体が疑わしいようです。あなたが何をテストしようとしているのかわかりませんが、データを作成したい場合は、generate_series

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

クール、それはなぜテーブルがまだ大量のデータを消費しているとマークされているのかを説明していますが、なぜそもそもそのすべてのスペースが必要だったのですか?MVCCについての私の理解から、トランザクションの更新/挿入されたタプルの異なるバージョンを維持する必要がありますが、ループの反復ごとに個別のバージョンを維持する必要はありません。
Nikhil N 2016

1
ループの反復ごとに新しいタプルが生成されます。
エヴァンキャロル

2
そうですが、私の印象では、MVCCはトランザクション中に変更されるすべてのタプルに対してタプルを作成するべきではありません。つまり、最初のINSERTが実行されると、Postgresは単一のタプルを作成し、UPDATEごとに単一の新しいタプルを追加します。各行に対してUPDATESが500回実行され、10000のINSERTがあるため、これは、トランザクションのコミット時に500 * 10000行= 5Mタプルになります。これは単なる見積もりですが、5Mに関係なく、各タプルを追跡するために50バイトと言って〜= 250MB、つまり30GB未満です。どこから来たの?
Nikhil N 2016

また、疑わしい関数について、インデックス付きフィールドが何度も更新されているときに、インデックスの動作をテストしようとしています。
Nikhil N 2016

私はあなたの考えについて混乱しています。ループで18e回更新された行は1つのタプルまたは1e8のタプルだと思いますか?
エヴァンキャロル

3

テーブルのすべての行が同じ値を取得し、各反復で複数回更新されるため、関数を分析した後の実際の数ははるかに大きくなります。

パラメーターnとそれを実行するとm

SELECT test_index(n, m);

あるm行の挿入、およびn * (m^2 + m) / 2更新が。したがって、n = 500およびのm = 10000場合、Postgresは1万行のみを挿入する必要がありますが、約25G(250億)のタプル更新を実行します。

Postgresの行には24バイトのオーバーヘッドがあることを考えると、単一のint列だけのテーブルでは、行ごとに28バイトとページのオーバーヘッドが必要になります。したがって、操作を完了するには、約700 GBとインデックス用のスペース(数百GBも必要)が必要になります。


テスト中

理論をテストするためにtest_test、1つの行を持つ別のテーブルを作成しました。

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

次に、トリガーを追加して、test更新ごとにカウンターを1増やします(コードは省略)。その後、我々は、我々はより小さな値で、機能を実行し、n = 50そしてm = 100

私たちの理論は予測します:

  • 100行の挿入、
  • 250Kタプルの更新(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テーブルには、インデックスのテーブルと5メガバイトのために、ほぼ9メガバイトを使用しています。test_testテーブルがさらに9MBを使用していることに注意してください!250Kの更新も行われたため、これは予想されています(2番目のトリガーtest_testは、の行の更新ごとに1つの行を更新しました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で)

fillfactor 50と最低の10で試した。まったく改善なし。ディスク使用量は、以前のテストとほぼ同じでした(デフォルトのfillfactorである100%を使用しました)。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.