Büyük bir tabloda yeni bir sütun doldurmanın en iyi yolu?


33

Postgres'te 7,801,611 satır bulunan 2,2 GB'lik bir tablomuz var. Ona bir uuid / guid sütunu ekliyoruz ve bu sütunu doldurmanın en iyi yolunun ne olduğunu merak ediyorum (buna bir NOT NULLsınırlama eklemek istiyoruz ).

Postgres'i doğru bir şekilde anladıysam, güncelleme teknik olarak bir silme ve eklemedir, bu nedenle temel olarak tüm 2.2 gb tablosunu yeniden oluşturur. Ayrıca bir köleyimiz çalışıyor, bu yüzden geride kalmasını istemiyoruz.

Zamanla yavaş yavaş yerleştiren bir senaryo yazmaktan daha iyi bir yolu var mı?


2
Zaten bir tane işlettin mi, ALTER TABLE .. ADD COLUMN ...yoksa cevaplanacak kısım mı?
ypercubeᵀᴹ

Henüz planlama aşamasında, herhangi bir tablo değişikliği yapmadım. Bunu daha önce sütun ekleyerek, doldurarak, sonra kısıtlamayı veya dizini ekleyerek yaptım. Ancak, bu tablo önemli ölçüde daha büyük ve yük, kilitleme, çoğaltma, vb. Hakkında endişeliyim ...
Collin Peters

Yanıtlar:


45

Bu, gereksinimlerinizin detaylarına bağlıdır.

Eğer sahip olduğunuz yeterli boş alan (en az% 110 pg_size_pretty((pg_total_relation_size(tbl))diskte) ve göze süredir payı kilidi ve bir çok kısa bir süre için özel kilit , daha sonra bir oluşturmak Yeni bir tablo dahil uuidkullanarak sütununda CREATE TABLE AS. Niye ya?

Aşağıdaki kod ek uuid-ossmodülden bir fonksiyon kullanır .

  • Tabloyu SHAREmoddaki eşzamanlı değişikliklere karşı kilitleyin (eşzamanlı okumalara izin verir). Tabloya yazma girişimleri bekleyecek ve sonunda başarısız olacaktır. Aşağıya bakınız.

  • Yeni sütunu anında doldururken tüm tabloyu kopyalayın - bu sırada büyük olasılıkla satırları sıralayın.
    Eğer sen sipariş satır olacak, sette mutlaka work_memsen (değil global olarak, sadece oturum için) göze olarak en yüksek olarak.

  • Ardından yeni tabloya kısıtlamalar, yabancı anahtarlar, indeksler, tetikleyiciler vb. Ekleyin. Bir tablonun büyük bölümlerini güncellerken , sıfırdan indeks oluşturmak, yinelemeli satır eklemek yerine çok daha hızlıdır.

  • Yeni tablo hazır olduğunda, eskisini bırakın ve yerine geçmesi için yeniyi yeniden adlandırın. Sadece bu son adım, işlemin geri kalanı için eski masada özel bir kilit elde eder - bu şimdi çok kısa olmalıdır.
    Ayrıca tablo türüne (görünümler, imzadaki tablo türünü kullanan işlevler, ...) bağlı olarak herhangi bir nesneyi silmenizi ve daha sonra bunları yeniden oluşturmanızı gerektirir.

  • Eksik durumlardan kaçınmak için hepsini bir işlemde yapın.

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;

Bu en hızlı olmalı. Yerinde güncelleme yapmak için başka bir yöntem, tüm tabloyu da daha pahalı bir şekilde yeniden yazmak zorundadır. Bu rotaya yalnızca diskinizde yeterli boş alan yoksa veya tüm tabloyu kilitlemeye ya da eşzamanlı yazma denemeleri için hatalar üretmeyi göze alamazsanız gidersiniz.

Eşzamanlı yazılara ne olur?

Çalışırken (diğer oturumlarda) Diğer işlem INSERT/ UPDATE/ DELETEişlem almıştır sonra aynı tabloda SHAREkilit serbest veya hangisi önce gelirse bir zaman aşımı başladı içinde, kadar kilit, bekleyecektir. Her iki şekilde de başarısız olurlar ; çünkü yazmaya çalıştığı tablo altlarından silinmiştir.

Yeni tablonun yeni bir tablo OID'si var, ancak eşzamanlı işlem zaten tablo adını önceki tablonun OID'sine çözdü . Kilit nihayet serbest bırakıldığında, üzerine yazmadan önce masayı kendileri kilitlemeye çalışırlar ve gittiğini görürler. Postgres cevap verecektir:

ERROR: could not open relation with OID 123456

123456Eski masanın OID'si nerede . Bunu önlemek için bu istisnayı yakalamanız ve uygulama kodunuzdaki sorguları yeniden denemeniz gerekir.

Bunun gerçekleşmesini göze alamazsanız, orijinal masanızı korumanız gerekir.

Mevcut masayı koruyan iki alternatif

  1. NOT NULLKısıtlama eklemeden önce güncelleyin (muhtemelen güncellemeyi bir seferde küçük segmentlerde çalıştırma) . NULL değerleriyle ve NOT NULLkısıtlama olmadan yeni bir sütun eklemek ucuzdur.
    Postgres 9.2’den bu yana , ayrıca bir CHECKkısıtlamaNOT VALID da oluşturabilirsiniz :

    Kısıt, daha sonraki eklere veya güncellemelere karşı da uygulanmaya devam edecek

    Bu güncelleme satırlara izin verir peu à peu de - birçok ayrı işlemler . Bu, sıra kilitlerini çok uzun süre tutmaktan kaçınır ve ölü sıraların tekrar kullanılmasını sağlar. ( VACUUMAutovacuum'un devreye girmesi için yeterli zaman yoksa elle çalıştırmanız gerekir.) Son olarak, NOT NULLkısıtlamayı ekleyin ve kısıtlamayı kaldırın NOT VALID CHECK:

    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;

    İlgili cevap NOT VALIDdaha ayrıntılı olarak tartışıyor :

  2. Bir yeni durum hazırlayın geçici tablo , TRUNCATEorijinal ve doldurma sıcaklığı tablosundan. Hepsi bir arada . Eşzamanlı yazmaların kaybolmasını önlemek için yeni tabloyu hazırlamadan önce hala SHAREkilitlenmeniz gerekir .

    SO ile ilgili bu cevabın detayları:


Harika cevap! Tam olarak aradığım bilgi. İki soru 1. Böyle bir eylemin ne kadar süreceğini test etmenin kolay bir yolu hakkında bir fikriniz var mı? 2. 5 dakika sürmesi halinde, bu 5 dakika boyunca bu tablodaki bir satırı güncellemeye çalışan eylemler ne olur?
Collin Peters

@CollinPeters: 1. Aslanın zaman içindeki payı büyük masayı kopyalamaya - ve muhtemelen endeksleri ve kısıtlamaları (buna bağlı olarak) yeniden yaratmaya başlayacaktı. Bırakma ve yeniden adlandırma ucuzdur. Test etmek için hazırlanan SQL betiğinizi LOCKen fazla ve hariç bırakmadan çalıştırabilirsiniz DROP. Sadece vahşi ve işe yaramaz tahminler yapabilirdim. 2. olarak, lütfen cevabımın ekini düşünün.
Erwin Brandstetter

@ErwinBrandstetter Görünümleri yeniden oluşturmaya devam edin, bu nedenle tabloyu yeniden adlandırdıktan sonra hala eski tablo (oid) kullanan bir düzine görünümüm varsa. Tüm görünümün yenilenmesini / yaratılmasını yeniden denemek yerine derin yerini almanın bir yolu var mı?
CodeFarmer

@CodeFarmer: Bir tabloyu yeniden adlandırırsanız, görünümler yeniden adlandırılan tabloyla çalışmaya devam eder. Görünümlerin bunun yerine yeni tabloyu kullanmasını sağlamak için bunları yeni tabloya dayalı olarak yeniden oluşturmanız gerekir. (Ayrıca eski tablonun silinmesine izin vermek için.) Etrafında (pratik) bir yol yoktur.
Erwin Brandstetter

14

"En iyi" bir cevabım yok, ama işleri hızlı bir şekilde halletmeni sağlayacak "en az kötü" bir cevabım var.

Masamda 2MM satır vardı ve ilkine varsayılan olan ikincil bir zaman damgası sütunu eklemeye çalıştığımda güncelleme performansı düşüyordu.

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

40 dakika bekledikten sonra, bunun ne kadar süreceği konusunda bir fikir edinmek için küçük bir parti üzerinde denedim - tahmin 8 saat kadar sürdü.

Kabul edilen cevap kesinlikle daha iyi - ancak bu tablo veritabanımda yoğun bir şekilde kullanılıyor. Üzerinde FKEY bulunan birkaç düzine masa var; YABANCI ANAHTARLARI pek çok masaya değiştirmek istemiyorum. Ve sonra görüşler var.

Biraz belge, vaka çalışması ve StackOverflow araştırıyor ve "A-Ha!" an. Drenaj çekirdek GÜNCELLEME'de değil, tüm INDEX işlemlerinde. Masamın üzerinde 12 dizin var - birkaçı kısıtlamalar, birkaçı sorgu planlayıcısını hızlandırmak ve birkaçı da tam metin araması için.

GÜNCELLEME olan her satır sadece bir DELETE / INSERT üzerinde çalışmakla kalmayıp, aynı zamanda her bir dizini değiştirmenin ve kısıtlamaları kontrol etmenin ek yüküdür.

Benim çözümüm her dizini ve kısıtlamayı bırakmak, tabloyu güncellemek ve ardından tüm dizinleri / kısıtlamaları tekrar eklemek oldu.

Aşağıdakileri yapan bir SQL işlemi yazmak yaklaşık 3 dakika sürdü:

  • BAŞLA;
  • bırakılan endeksler / sabitler
  • güncelleme tablosu
  • dizinleri / kısıtlamaları yeniden ekleyin
  • COMMIT;

Senaryonun çalışması 7 dakika sürdü.

Kabul edilen cevap kesinlikle daha iyi ve daha doğrudur ... ve kesinti süresi ihtiyacını ortadan kaldırır. Benim durumumda, bu çözümü kullanmak için önemli ölçüde daha fazla "Geliştirici" çalışması olurdu ve bunun gerçekleştirilebilmesi için 30 dakikalık bir planlı kapalı kalma süresi vardı. Çözümümüz 10'da ele alındı.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.