Bisakah seseorang menjelaskan perilaku aneh yang mengeksekusi jutaan UPDATES?

8

Bisakah seseorang menjelaskan perilaku ini kepada saya? Saya menjalankan query berikut pada Postgres 9.3 yang berjalan secara native di OS X. Saya mencoba mensimulasikan beberapa perilaku di mana ukuran indeks bisa tumbuh jauh lebih besar daripada ukuran tabel, dan malah menemukan sesuatu yang bahkan lebih aneh.

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);

Saya membiarkan ini berjalan sekitar satu jam di mesin lokal saya, sebelum saya mulai mendapatkan peringatan masalah disk dari OS X. Saya perhatikan bahwa Postgres menyedot sekitar 10MB / s dari disk lokal saya, dan bahwa database Postgres mengkonsumsi total besar 30GB dari mesin saya. Saya akhirnya membatalkan permintaan. Bagaimanapun, Postgres tidak mengembalikan ruang disk kepada saya dan saya menanyakan database untuk statistik penggunaan dengan hasil sebagai berikut:

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

Namun, memilih dari tabel tidak membuahkan hasil.

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

Menjalankan 10.000 kumpulan 500 adalah 5.000.000 baris, yang seharusnya menghasilkan ukuran tabel / indeks yang cukup kecil (pada skala MB). Saya menduga Postgres membuat versi baru dari tabel / indeks untuk setiap INSERT / UPDATE yang terjadi dengan fungsi, tetapi ini tampaknya aneh. Seluruh fungsi dijalankan secara transaksi, dan tabel kosong untuk memulai.

Adakah pemikiran mengapa saya melihat perilaku ini?

Secara khusus, dua pertanyaan yang saya miliki adalah: mengapa ruang ini belum direklamasi oleh database dan yang kedua adalah mengapa database membutuhkan ruang sebanyak ini? 30GB tampak seperti banyak bahkan ketika menghitung MVCC

Nikhil N
sumber

Jawaban:

7

Versi pendek

Algoritma Anda terlihat O (n * m) pada pandangan pertama, tetapi secara efektif menumbuhkan O (n * m ^ 2), karena semua baris memiliki ID yang sama. Alih-alih 5M baris, Anda mendapatkan> 1,25G baris

Versi panjang

Fungsi Anda di dalam transaksi implisit. Itu sebabnya Anda tidak melihat data setelah membatalkan kueri Anda, dan juga mengapa perlu mempertahankan versi berbeda dari tupel yang diperbarui / dimasukkan untuk kedua loop.

Selain itu, saya menduga Anda memiliki bug dalam logika Anda atau meremehkan jumlah pembaruan yang dibuat.

Iterasi pertama dari loop luar - current_id dimulai pada 1, menyisipkan 1 baris, kemudian loop dalam melakukan pembaruan 10.000 kali untuk baris yang sama, diselesaikan dengan satu-satunya baris yang menunjukkan ID 10001, dan current_id dengan nilai 10001. 10001 versi baris masih disimpan, karena transaksi belum selesai.

Iterasi kedua dari loop luar - karena current_id adalah 10001, baris baru dimasukkan dengan ID 10001. Sekarang Anda memiliki 2 baris dengan "ID" yang sama, dan total versi 10003 dari kedua baris (10002 dari yang pertama, 1 dari yang kedua). Kemudian loop dalam memperbarui 10.000 kali KEDUA baris, menciptakan 20000 versi baru, hingga 30003 tuple sejauh ini ...

Iterasi ketiga dari loop luar: ID saat ini adalah 20001, baris baru dimasukkan dengan ID 20001. Anda memiliki 3 baris, semuanya dengan "ID" 20001, 30006 baris / tuple versi yang sama sejauh ini. Kemudian Anda melakukan 10.000 pembaruan dari 3 baris, menciptakan 30000 versi baru, sekarang 60006 ...

...

(Jika ruang Anda diizinkan) - iterasi ke-500 dari loop luar, buat pembaruan 5M dari 500 baris, hanya di iterasi ini

Seperti yang Anda lihat, alih-alih pembaruan 5M yang Anda harapkan, Anda mendapat 1000 + 2000 + 3000 + ... + 4990000 + 5000000 pembaruan (ditambah perubahan), yang akan menjadi 10000 * (1 + 2 + 3 + ... + 499+ 500), lebih dari 1,25G pembaruan. Dan tentu saja satu baris bukan hanya ukuran int Anda, itu membutuhkan beberapa struktur tambahan, sehingga tabel dan indeks Anda mendapatkan lebih dari sepuluh gigabytes ukuran.

Tanya Jawab terkait:

Bruno Guardia
sumber
5

PostgreSQL hanya mengembalikan ruang disk setelah VACUUM FULL, bukan setelah DELETEatau ROLLBACK(sebagai akibat dari pembatalan)

Bentuk standar VACUUM menghapus versi baris mati dalam tabel dan indeks dan menandai ruang yang tersedia untuk digunakan kembali di masa depan. Namun, itu tidak akan mengembalikan ruang ke sistem operasi, kecuali dalam kasus khusus di mana satu atau lebih halaman di ujung tabel menjadi sepenuhnya bebas dan kunci meja eksklusif dapat dengan mudah diperoleh. Sebaliknya, VACUUM FULL secara aktif memadatkan tabel dengan menulis versi lengkap file tabel tanpa ruang kosong. Ini meminimalkan ukuran meja, tetapi bisa memakan waktu lama. Ini juga membutuhkan ruang disk tambahan untuk salinan tabel yang baru, sampai operasi selesai.

Sebagai catatan, seluruh fungsi Anda tampaknya dipertanyakan. Saya tidak yakin apa yang Anda coba untuk menguji, tetapi jika Anda ingin membuat data, Anda dapat menggunakannyagenerate_series

INSERT INTO test
SELECT x FROM generate_series(1, batch_size*total_batches) AS t(x);
Evan Carroll
sumber
Keren, itu menjelaskan mengapa tabel itu masih ditandai sebagai memakan begitu banyak data, tapi mengapa itu membutuhkan semua ruang itu di tempat pertama? Dari pemahaman saya tentang MVCC, itu perlu mempertahankan versi berbeda dari tupel yang diperbarui / dimasukkan untuk transaksi, tetapi seharusnya tidak perlu mempertahankan versi terpisah untuk setiap iterasi dari loop.
Nikhil N
1
Setiap iterasi dari loop menghasilkan tupel baru.
Evan Carroll
2
Benar, tapi kesan saya adalah bahwa MVCC seharusnya tidak membuat tupel untuk semua tupel yang dimodifikasi selama transaksi. Dengan kata lain, ketika INSERT pertama berjalan Postgres menciptakan satu tuple, dan itu menambahkan satu tuple baru untuk setiap UPDATE. Karena UPDATES dijalankan untuk setiap baris 500 kali, dan ada 10.000 INSERT, ini berarti 500 * 10000 baris = 5M tupel pada saat transaksi dilakukan. Sekarang ini hanya perkiraan, tetapi terlepas dari 5M * katakan 50 byte untuk melacak setiap tuple ~ = 250MB, yang JAUH kurang dari 30GB. Dari mana semua ini berasal?
Nikhil N
Juga kembali: fungsi yang dipertanyakan, saya mencoba untuk menguji perilaku indeks ketika bidang yang diindeks memperbarui berkali-kali tetapi dengan cara yang meningkat secara monoton, sehingga menghasilkan indeks yang sangat jarang, tetapi yang selalu ditambahkan pada disk.
Nikhil N
Saya bingung dengan apa yang Anda pikirkan. Apakah menurut Anda satu baris yang diperbarui 18e kali dalam satu lingkaran adalah satu tupel atau 1e8 tupel?
Evan Carroll
3

Angka aktual setelah menganalisis fungsi jauh lebih besar karena semua baris tabel mendapatkan nilai yang sama yang diperbarui beberapa kali dalam setiap iterasi.

Ketika kami menjalankannya dengan parameter ndan m:

SELECT test_index(n, m);

ada msisipan dan n * (m^2 + m) / 2pembaruan baris . Jadi, untuk n = 500dan m = 10000, Postgres perlu memasukkan hanya baris 10K tetapi melakukan pembaruan tuple ~ 25G (25 miliar).

Mempertimbangkan bahwa sebuah baris di Postgres memiliki 24 byte overhead, sebuah tabel dengan hanya satu intkolom akan membutuhkan 28 byte per baris plus overhead halaman. Jadi, agar operasi selesai, kita membutuhkan sekitar 700GB plus ruang untuk indeks (yang akan menjadi beberapa ratus GB juga).


Pengujian

Untuk menguji teorinya, kami membuat tabel lain test_testdengan satu baris.

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

Kemudian kami menambahkan pemicu testsehingga setiap pembaruan akan menambah penghitung sebesar 1. (Kode dihilangkan). Kemudian kita jalankan fungsinya, dengan nilai lebih kecil, n = 50dan m = 100.

Teori kami memprediksi :

  • Sisipan 100 baris,
  • Pembaruan tuple 250K (252500 = 50 * 100 * 101/2)
  • setidaknya 7MB untuk tabel pada disk
  • (+ ruang untuk indeks)

Tes 1 ( testtabel asli , dengan indeks)

    SELECT test_index(50, 100) ;

Setelah selesai, kami memeriksa konten tabel:

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

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

Dan penggunaan disk (kueri di bawah Indeks ukuran / statistik penggunaan dalam Pemeliharaan Indeks ):

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      |                 |            

The testmeja telah digunakan hampir 9MB untuk meja dan 5MB untuk indeks. Perhatikan bahwa test_testtabel telah menggunakan 9MB lagi! Itu diharapkan karena ia juga melalui pembaruan 250 ribu (pemicu kedua kami memperbarui satu baris test_testuntuk setiap pembaruan satu baris di test.)

Perhatikan juga jumlah pemindaian di atas meja test(10K) dan tupel berbunyi (500K).

Tes 2 ( testtabel tanpa indeks)

Persis sama dengan di atas, kecuali bahwa tabel tidak memiliki indeks.

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      |                 |            

Kami mendapatkan ukuran yang sama untuk penggunaan disk tabel dan tentu saja tidak ada penggunaan disk untuk indeks. Jumlah pemindaian di atas meja testadalah nol dan tupel juga berbunyi.

Tes 3 (dengan fillfactor rendah)

Mencoba dengan fillfactor 50 dan serendah mungkin, 10. Tidak ada perbaikan sama sekali. Penggunaan disk hampir identik dengan tes sebelumnya (yang menggunakan fillfactor default, 100 persen)

ypercubeᵀᴹ
sumber