Cara terbaik untuk mengisi kolom baru di tabel besar?

33

Kami memiliki tabel 2,2 GB di Postgres dengan 7.801.611 baris di dalamnya. Kami menambahkan kolom uuid / panduan ke dalamnya dan saya bertanya-tanya apa cara terbaik untuk mengisi kolom itu (karena kami ingin menambahkan NOT NULLbatasan untuknya).

Jika saya mengerti Postgres dengan benar, pembaruan secara teknis adalah delete dan insert jadi ini pada dasarnya membangun kembali seluruh tabel 2.2 gb. Kami juga memiliki budak yang berjalan sehingga kami tidak ingin itu ketinggalan.

Apakah ada cara yang lebih baik daripada menulis naskah yang perlahan-lahan mengisi waktu?

Collin Peters
sumber
2
Sudahkah Anda menjalankan bagian ALTER TABLE .. ADD COLUMN ...atau bagian itu yang harus dijawab juga?
ypercubeᵀᴹ
Belum menjalankan modifikasi tabel apa pun, hanya dalam tahap perencanaan. Saya telah melakukan ini sebelumnya dengan menambahkan kolom, mengisinya, lalu menambahkan kendala atau indeks. Namun, tabel ini secara signifikan lebih besar dan saya khawatir tentang beban, penguncian, replikasi, dll ...
Collin Peters

Jawaban:

45

Ini sangat tergantung pada detail kebutuhan Anda.

Jika Anda memiliki ruang kosong yang cukup (setidaknya 110% dari pg_size_pretty((pg_total_relation_size(tbl))) pada disk dan dapat membeli kunci saham untuk beberapa waktu dan kunci eksklusif untuk waktu yang sangat singkat , maka buat tabel baru termasuk uuidkolom yang digunakan CREATE TABLE AS. Mengapa?

Kode di bawah ini menggunakan fungsi dari uuid-ossmodul tambahan .

  • Kunci tabel terhadap perubahan SHAREmode bersamaan (masih memungkinkan pembacaan bersamaan). Upaya untuk menulis ke tabel akan menunggu dan akhirnya gagal. Lihat di bawah.

  • Salin seluruh tabel sambil mengisi kolom baru dengan cepat - mungkin memesan baris yang menguntungkan saat berada di dalamnya.
    Jika Anda akan memesan ulang baris, pastikan untuk mengatur work_memsetinggi yang Anda mampu (hanya untuk sesi Anda, bukan secara global).

  • Kemudian tambahkan batasan, kunci asing, indeks, pemicu dll ke tabel baru. Saat memperbarui sebagian besar tabel, lebih cepat membuat indeks dari awal daripada menambahkan baris secara berulang.

  • Saat tabel baru siap, jatuhkan yang lama dan ganti nama yang baru untuk menjadikannya pengganti drop-in. Hanya langkah terakhir ini yang mendapatkan kunci eksklusif di meja lama untuk sisa transaksi - yang seharusnya sangat singkat sekarang.
    Ini juga mengharuskan Anda menghapus objek apa pun tergantung pada tipe tabel (tampilan, fungsi menggunakan tipe tabel dalam tanda tangan, ...) dan membuatnya kembali sesudahnya.

  • Lakukan semuanya dalam satu transaksi untuk menghindari kondisi yang tidak lengkap.

BEGIN;
LOCK TABLE tbl IN SHARE MODE;

SET LOCAL work_mem = '???? MB';  -- just for this transaction

CREATE TABLE tbl_new AS 
SELECT uuid_generate_v1() AS tbl_uuid, <list of all columns in order>
FROM   tbl
ORDER  BY ??;  -- optionally order rows favorably while being at it.

ALTER TABLE tbl_new
   ALTER COLUMN tbl_uuid SET NOT NULL
 , ALTER COLUMN tbl_uuid SET DEFAULT uuid_generate_v1()
 , ADD CONSTRAINT tbl_uuid_uni UNIQUE(tbl_uuid);

-- more constraints, indices, triggers?

DROP TABLE tbl;
ALTER TABLE tbl_new RENAME tbl;

-- recreate views etc. if any
COMMIT;

Ini harus tercepat. Metode pembaruan lainnya yang ada harus menulis ulang seluruh tabel juga, hanya dengan cara yang lebih mahal. Anda hanya akan pergi rute itu jika Anda tidak memiliki cukup ruang kosong pada disk atau tidak mampu mengunci seluruh tabel atau menghasilkan kesalahan untuk upaya penulisan bersamaan.

Apa yang terjadi pada penulisan bersamaan?

Transaksi lain (dalam sesi lain) mencoba INSERT/ UPDATE/ DELETEdalam tabel yang sama setelah transaksi Anda mengambil SHAREkunci, akan menunggu sampai kunci dilepaskan atau waktu habis menendang, mana yang lebih dulu. Mereka akan gagal , karena tabel yang mereka coba tulis telah dihapus dari bawah mereka.

Tabel baru memiliki OID tabel baru, tetapi transaksi bersamaan telah menyelesaikan nama tabel ke OID dari tabel sebelumnya . Ketika kunci akhirnya dilepaskan, mereka mencoba mengunci meja sendiri sebelum menulis dan menemukan bahwa itu hilang. Postgres akan menjawab:

ERROR: could not open relation with OID 123456

Di mana 123456OID dari tabel lama. Anda perlu menangkap pengecualian itu dan mencoba lagi kueri dalam kode aplikasi Anda untuk menghindarinya.

Jika Anda tidak mampu untuk itu terjadi, Anda harus menjaga meja asli Anda.

Dua alternatif menjaga tabel yang ada

  1. Perbarui di tempat (mungkin menjalankan pembaruan pada segmen kecil sekaligus) sebelum Anda menambahkan NOT NULLkendala. Menambahkan kolom baru dengan nilai NULL dan tanpa NOT NULLkendala itu murah.
    Sejak Postgres 9.2 Anda juga dapat membuat CHECKbatasan denganNOT VALID :

    Kendala masih akan diberlakukan terhadap sisipan atau pembaruan berikutnya

    Itu memungkinkan Anda untuk memperbarui baris peu à peu - dalam beberapa transaksi terpisah . Ini menghindari menjaga kunci baris terlalu lama dan juga memungkinkan baris mati digunakan kembali. (Anda harus menjalankan VACUUMsecara manual jika tidak ada cukup waktu di antara autovacuum untuk memulai.) Akhirnya, tambahkan NOT NULLkendala dan hapus NOT VALID CHECKkendala:

    ALTER TABLE tbl ADD CONSTRAINT tbl_no_null CHECK (tbl_uuid IS NOT NULL) NOT VALID;
    
    -- update rows in multiple batches in separate transactions
    -- possibly run VACUUM between transactions
    
    ALTER TABLE tbl ALTER COLUMN tbl_uuid SET NOT NULL;
    ALTER TABLE tbl ALTER DROP CONSTRAINT tbl_no_null;

    Jawaban terkait membahas NOT VALIDlebih detail:

  2. Siapkan negara baru di tabel sementara , TRUNCATEasli dan isi ulang dari tabel temp. Semua dalam satu transaksi . Anda masih perlu mengambil SHAREkunci sebelum menyiapkan tabel baru untuk mencegah kehilangan penulisan bersamaan.

    Detail dalam jawaban terkait ini di SO:

Erwin Brandstetter
sumber
Jawaban yang fantastis! Tepatnya info yang saya cari. Dua pertanyaan 1. Apakah Anda punya ide tentang cara mudah untuk menguji berapa lama tindakan seperti ini akan berlangsung? 2. Jika perlu 5 menit, apa yang terjadi pada tindakan yang mencoba memperbarui baris dalam tabel itu selama 5 menit itu?
Collin Peters
@CollinPeters: 1. Bagian terbesar waktu akan digunakan untuk menyalin tabel besar - dan mungkin menciptakan kembali indeks dan kendala (tergantung). Mengganti dan mengganti nama itu murah. Untuk menguji Anda dapat menjalankan skrip SQL yang disiapkan tanpa LOCKdan tidak termasuk DROP. Saya hanya bisa mengucapkan dugaan liar dan tidak berguna. Adapun 2., harap pertimbangkan addendum untuk jawaban saya.
Erwin Brandstetter
@ErwinBrandstetter Lanjutkan pada tampilan ulang, jadi jika saya memiliki selusin tampilan yang masih menggunakan tabel lama (oid) setelah penggantian nama tabel. Apakah ada cara untuk melakukan penggantian yang mendalam alih-alih menjalankan kembali seluruh tampilan refresh / pembuatan?
CodeFarmer
@CodeFarmer: Jika Anda baru saja mengganti nama tabel, view tetap bekerja dengan tabel yang diubah namanya. Untuk membuat tampilan menggunakan tabel baru , Anda harus membuat ulang berdasarkan tabel baru. (Juga agar tabel lama dihapus.) Tidak ada (praktis) jalan lain di sekitarnya.
Erwin Brandstetter
14

Saya tidak punya jawaban "terbaik", tetapi saya punya jawaban "paling tidak buruk" yang mungkin membuat Anda menyelesaikan sesuatu dengan cukup cepat.

Tabel saya memiliki baris 2MM dan kinerja pembaruan menanjak ketika saya mencoba menambahkan kolom cap waktu sekunder yang default ke yang pertama.

ALTER TABLE mytable ADD new_timestamp TIMESTAMP ;
UPDATE mytable SET new_timestamp = old_timestamp ;
ALTER TABLE mytable ALTER new_timestamp SET NOT NULL ;

Setelah menggantung selama 40 menit, saya mencoba ini dalam jumlah kecil untuk mendapatkan gambaran berapa lama ini - perkiraannya sekitar 8 jam.

Jawaban yang diterima jelas lebih baik - tetapi tabel ini banyak digunakan dalam database saya. Ada beberapa lusin tabel yang FKEY ke atasnya; Saya ingin menghindari beralih KUNCI ASING pada banyak tabel. Dan kemudian ada pandangan.

Sedikit mencari dokumen, studi kasus dan StackOverflow, dan aku punya "A-Ha!" saat. Saluran tidak pada UPDATE inti, tetapi pada semua operasi INDEX. Tabel saya memiliki 12 indeks di atasnya - beberapa untuk kendala unik, beberapa untuk mempercepat perencana kueri, dan beberapa untuk pencarian teks lengkap.

Setiap baris yang DIPERBARUI tidak hanya bekerja pada DELETE / INSERT, tetapi juga overhead untuk mengubah setiap indeks dan memeriksa kendala.

Solusi saya adalah dengan menghapus setiap indeks dan kendala, memperbarui tabel, lalu menambahkan semua indeks / kendala kembali.

Butuh sekitar 3 menit untuk menulis transaksi SQL yang melakukan hal berikut:

  • MULAI;
  • menjatuhkan indeks / kendala
  • perbarui tabel
  • tambahkan kembali indeks / batasan
  • MELAKUKAN;

Skrip membutuhkan waktu 7 menit untuk dijalankan.

Jawaban yang diterima jelas lebih baik dan lebih tepat ... dan secara virtual menghilangkan kebutuhan akan waktu henti. Namun, dalam kasus saya, akan diperlukan lebih banyak pekerjaan "Pengembang" untuk menggunakan solusi itu dan kami memiliki jendela 30 menit dari waktu henti yang dijadwalkan yang dapat diselesaikan. Solusi kami mengatasinya dalam 10.

Jonathan Vanasco
sumber