DBA: kompeten mengatur sinkronisasi dan impor

Dengan pemrosesan kompleks dari kumpulan data besar ( proses ETL yang berbeda : impor, konversi, dan sinkronisasi dengan sumber eksternal), sering kali ada kebutuhan untuk "mengingat" sementara dan segera memproses sesuatu yang banyak.

Tugas khas semacam ini biasanya terdengar seperti ini: "Di sini departemen akuntansi mengunggah pembayaran yang terakhir diterima dari bank klien , kita perlu dengan cepat mengunggahnya ke situs web dan menautkannya ke akun"

Tapi ketika volume "sesuatu" ini mulai diukur dalam ratusan megabita, dan layanan Ini harus terus bekerja dengan basis dalam mode 24x7, ada banyak efek samping yang akan menghancurkan hidup Anda.

Untuk mengatasinya di PostgreSQL (dan tidak hanya di dalamnya), Anda dapat menggunakan beberapa opsi pengoptimalan yang memungkinkan Anda memproses lebih cepat dan dengan sumber daya yang lebih sedikit.

1. Kemana harus mengirim?


Pertama, mari kita putuskan di mana kita dapat mengunggah data yang ingin kita "proses".

1.1. Tabel Sementara (MEJA SEMENTARA)


Pada prinsipnya, untuk PostgreSQL, yang sementara adalah tabel yang sama dengan yang lainnya. Oleh karena itu, takhayul seperti "semuanya disimpan di sana hanya dalam memori, tetapi bisa berakhir" tidak benar . Tetapi ada beberapa perbedaan yang signifikan.

Namespace sendiri untuk setiap koneksi ke database


Jika dua koneksi mencoba membuat pada saat yang sama CREATE TABLE x, maka seseorang pasti akan mendapatkan kesalahan objek DB yang tidak unik .

Tetapi jika keduanya mencoba untuk mengeksekusi , maka keduanya biasanya akan melakukannya, dan masing-masing akan menerima salinan tabelnya sendiri. Dan tidak akan ada kesamaan di antara mereka.CREATE TEMPORARY TABLE x

"Penghancuran diri" dengan putuskan


Ketika Anda menutup koneksi, semua tabel sementara secara otomatis dihapus, sehingga DROP TABLE xtidak ada artinya dalam mengeksekusi "secara manual" , kecuali ...

Jika Anda bekerja melalui pgbouncer dalam mode transaksi , database terus menganggap bahwa koneksi ini masih aktif, dan ini sementara tabelnya masih ada.

Oleh karena itu, upaya untuk membuatnya kembali, dari koneksi lain ke pgbouncer, akan menghasilkan kesalahan. Tapi ini bisa dielakkan dengan mengambil keuntungan . Benar, lebih baik tidak melakukan hal yang sama, karena Anda dapat "tiba-tiba" mencari tahu data yang tersisa dari "pemilik sebelumnya" di sana. Alih-alih, jauh lebih baik untuk membaca manual, dan melihat bahwa ketika membuat tabel ada peluang untuk menambahkanCREATE TEMPORARY TABLE IF NOT EXISTS x

ON COMMIT DROP - yaitu, ketika transaksi selesai, tabel akan dihapus secara otomatis.

Non-replikasi


Karena hanya gabungan tertentu yang termasuk, tabel sementara tidak direplikasi. Tetapi ini menghilangkan kebutuhan untuk menulis dua kali data di heap + WAL, jadi INSERT / UPDATE / DELETE jauh lebih cepat di dalamnya.

Tetapi karena tabel sementara masih merupakan tabel "hampir biasa", itu juga tidak dapat dibuat pada replika. Setidaknya untuk saat ini, meskipun tambalan yang sesuai telah ada sejak lama.

1.2. Tabel tidak terdaftar (TABEL TIDAK DILOGGED)


Tapi apa yang harus dilakukan, misalnya, jika Anda memiliki semacam proses ETL rumit yang tidak dapat diimplementasikan dalam satu transaksi, dan Anda masih memiliki pgbouncer dalam mode transaksi ? ..

Atau aliran data sangat besar sehingga tidak ada bandwidth yang cukup per koneksi dari basis data (baca, satu proses pada CPU)? ..

Atau bagian dari operasi berjalan secara serempak dalam koneksi yang berbeda? ..

Hanya ada satu opsi - untuk sementara membuat tabel non-temporer . Pun, ya. Yaitu:

  • menciptakan "nya" tabel dengan nama acak maksimal agar tidak menyeberang dengan siapa pun
  • Ekstrak : menuangkan data dari sumber eksternal ke dalamnya
  • Transform : berubah, diisi bidang pengikatan kunci
  • Load : menuangkan data jadi ke tabel target
  • menghapus tabel "saya"

Dan sekarang - lalat di salep. Bahkan, semua tulisan di PostgreSQL terjadi dua kali - pertama di WAL , kemudian di tubuh tabel / indeks. Semua ini dilakukan untuk mendukung ACID dan visibilitas data yang benar antara transaksi bersarang COMMITdan ROLLBACKbertingkat.

Tapi kami tidak membutuhkan ini! Kami memiliki seluruh proses atau berhasil dilewati, atau tidak . Tidak masalah berapa banyak transaksi perantara yang dikandungnya - kami tidak tertarik untuk "melanjutkan proses dari tengah", terutama ketika tidak jelas di mana itu.

Untuk ini, pengembang PostgreSQL memperkenalkan versi 9.1 seperti tabel non-journaled (UNLOGGED) :
. , , (. 29), . , ; . , . , , .
Singkatnya, itu akan jauh lebih cepat , tetapi jika server database "crash" - itu akan menjadi tidak menyenangkan. Tetapi seberapa sering hal ini terjadi, dan apakah proses ETL Anda tahu cara memodifikasinya dengan benar "dari tengah" setelah "revitalisasi" database? ..

Jika tidak, dan kasus di atas serupa dengan milik Anda - gunakan UNLOGGED, tetapi jangan pernah menyertakan atribut ini pada tabel nyata data dari mana Anda sayang.

1.3. ON COMMIT {DELETE ROWS | PENURUNAN}


Desain ini memungkinkan saat membuat tabel untuk mengatur perilaku otomatis ketika transaksi berakhir.

Tentang saya sudah menulis di atas, itu menghasilkan , tetapi situasinya lebih menarik - ini dia dihasilkan . Karena seluruh infrastruktur untuk menyimpan deskripsi meta dari tabel sementara persis sama dengan yang biasa, pembuatan dan penghapusan tabel sementara yang konstan mengarah ke "pembengkakan" yang kuat dari tabel sistem pg_class, pg_attribute, pg_attrdef, pg_depend, ... Sekarang bayangkan Anda memiliki pekerja di telepon menghubungkan ke database, yang setiap detik membuka transaksi baru, membuat, mengisi, memproses, dan menghapus tabel sementara ... Sampah di tabel sistem akan menumpuk berlebih, dan ini adalah rem tambahan selama setiap operasi.ON COMMIT DROPDROP TABLEON COMMIT DELETE ROWSTRUNCATE TABLE





Secara umum, jangan! Dalam hal ini, jauh lebih efisien CREATE TEMPORARY TABLE x ... ON COMMIT DELETE ROWSuntuk mengeluarkannya dari siklus transaksi - maka pada awal setiap transaksi baru tabel tersebut sudah ada (simpan panggilan CREATE), tetapi akan kosong , terima kasih TRUNCATE(kami juga menyimpan panggilan) di akhir transaksi sebelumnya.

1.4. SEPERTI ... TERMASUK ...


Saya sebutkan di awal bahwa salah satu kasus penggunaan yang umum untuk tabel sementara adalah berbagai jenis impor - dan pengembang dengan susah payah menyalin-tempel daftar bidang tabel target ke dalam deklarasi sementaranya ...

Tapi kemalasan adalah mesin kemajuan! Oleh karena itu, membuat tabel baru "pada model" bisa menjadi lebih sederhana:

CREATE TEMPORARY TABLE import_table(
  LIKE target_table
);

Karena Anda kemudian dapat menambahkan banyak data ke tabel ini, pencarian di dalamnya tidak akan pernah cepat. Tetapi ada solusi tradisional terhadap ini - indeks! Dan, ya, tabel sementara juga dapat memiliki indeks .

Karena, seringkali, indeks yang diinginkan bertepatan dengan indeks tabel target, Anda cukup menulis . Jika Anda juga membutuhkan -nilai (misalnya, untuk mengisi nilai kunci utama), Anda dapat menggunakan . Baik, atau hanya - - itu akan menyalin default, indeks, kendala ... Tapi di sini Anda perlu memahami bahwa jika Anda membuat tabel impor segera dengan indeks, maka data akan diisi lebih lamaLIKE target_table INCLUDING INDEXES

DEFAULTLIKE target_table INCLUDING DEFAULTSLIKE target_table INCLUDING ALL

daripada jika Anda mengisi semuanya terlebih dahulu, dan kemudian gulung indeks - lihat sebagai contoh bagaimana pg_dump melakukannya .

Secara keseluruhan, RTFM !

2. Bagaimana cara menulis?


Saya akan mengatakan secara sederhana - gunakan COPY-stream alih-alih "paket" INSERT, akselerasi pada waktu tertentu . Anda bahkan dapat langsung dari file yang sudah dibentuk sebelumnya.

3. Bagaimana cara menanganinya?


Jadi, biarkan pengantar kami terlihat seperti ini:

  • Anda memiliki di piring Anda sebuah piring dengan data klien untuk catatan 1M
  • setiap hari klien mengirimi Anda "gambar" lengkap baru
  • dari pengalaman Anda tahu bahwa tidak lebih dari 10 ribu catatan yang berubah dari waktu ke waktu

Contoh klasik dari situasi seperti ini adalah database KLADR - ada banyak alamat, tetapi dalam setiap minggu mengunggah perubahan (penggantian nama pemukiman, asosiasi jalan, penampilan rumah baru) ada sangat sedikit, bahkan secara nasional.

3.1. Algoritma sinkronisasi penuh


Untuk kesederhanaan, katakanlah bahwa Anda bahkan tidak perlu merestrukturisasi data - cukup bawa tabel dalam bentuk yang benar, yaitu:

  • hapus semua yang tidak lagi
  • perbarui semua yang sudah ada, dan Anda perlu memperbarui
  • masukkan semua yang belum

Mengapa dalam urutan ini perlu melakukan operasi? Karena ini adalah bagaimana ukuran tabel tumbuh minimal ( ingat tentang MVCC! ).

HAPUS DARI dst


Tidak, tentu saja, Anda hanya dapat melakukan dua operasi:

  • hapus ( DELETE) sama sekali
  • rekatkan semuanya dari gambar baru

Tetapi pada saat yang sama, berkat MVCC, ukuran meja akan meningkat tepat dua kali lipat ! Dapatkan + 1M catatan gambar di tabel karena pembaruan 10 ribu - redundansi begitu-begitu ...

TRUNCATE dst


Pengembang yang lebih berpengalaman tahu bahwa seluruh pelat dapat dibersihkan dengan cukup murah:

  • jelas ( TRUNCATE) seluruh tabel
  • rekatkan semuanya dari gambar baru

Metode ini efektif, kadang-kadang cukup berlaku , tetapi ada masalah ... Kami akan menyuntikkan catatan 1M, jadi kami tidak dapat membiarkan meja kosong selama ini (seperti yang akan terjadi tanpa membungkus dalam satu transaksi).

Yang berarti:

  • kami memulai transaksi panjang
  • TRUNCATEMenyebabkan AccessExclusive -Lock
  • kami melakukan penyisipan untuk waktu yang lama, dan semua orang pada saat ini bahkan tidak bisaSELECT

Ada yang buruk ...

ALTER TABLE ... RENAME ... / DROP TABEL ...


Sebagai opsi, isi semuanya menjadi tabel baru yang terpisah, dan kemudian cukup ganti namanya ke yang lama. Beberapa hal kecil yang jahat:

  • AccessExclusive juga , meskipun jauh lebih sedikit dalam waktu
  • semua rencana kueri / statistik tabel ini disetel ulang, perlu untuk mengarahkan ANALYZE
  • semua kunci asing (FK) pecah di atas meja

Ada tambalan WIP dari Simon Riggs, yang menyarankan untuk melakukan ALTERoperasi untuk mengganti badan tabel pada tingkat file, tanpa menyentuh statistik dan FK, tetapi tidak mengumpulkan kuorum.

HAPUS, PEMBARUAN, INSERT


Jadi, kami berhenti pada versi non-pemblokiran dari tiga operasi. Hampir tiga ... Bagaimana melakukan ini paling efektif?

--     ,     "" 
BEGIN;

--      
CREATE TEMPORARY TABLE tmp(
  LIKE dst INCLUDING INDEXES --    ,   
) ON COMMIT DROP; --       

-- -     COPY
COPY tmp FROM STDIN;
-- ...
-- \.

--  
DELETE FROM
  dst D
USING
  dst X
LEFT JOIN
  tmp Y
    USING(pk1, pk2) --   
WHERE
  (D.pk1, D.pk2) = (X.pk1, X.pk2) AND
  Y IS NOT DISTINCT FROM NULL; -- ""

--  
UPDATE
  dst D
SET
  (f1, f2, f3) = (T.f1, T.f2, T.f3)
FROM
  tmp T
WHERE
  (D.pk1, D.pk2) = (T.pk1, T.pk2) AND
  (D.f1, D.f2, D.f3) IS DISTINCT FROM (T.f1, T.f2, T.f3); --   

--  
INSERT INTO
  dst
SELECT
  T.*
FROM
  tmp T
LEFT JOIN
  dst D
    USING(pk1, pk2)
WHERE
  D IS NOT DISTINCT FROM NULL;

COMMIT;

3.2. Impor pemrosesan pos


Dalam KLADER yang sama, semua catatan yang diubah harus juga dijalankan melalui proses pasca - normalisasi, sorot kata kunci, dan bawa ke struktur yang diperlukan. Tetapi bagaimana Anda tahu apa yang sebenarnya telah berubah , tanpa menyulitkan kode sinkronisasi, idealnya tanpa menyentuhnya sama sekali?

Jika hanya proses Anda yang memiliki akses tulis pada saat sinkronisasi, maka Anda dapat menggunakan pemicu yang akan mengumpulkan semua perubahan untuk kami:

--  
CREATE TABLE kladr(...);
CREATE TABLE kladr_house(...);

--    
CREATE TABLE kladr$log(
  ro kladr, --      /
  rn kladr
);

CREATE TABLE kladr_house$log(
  ro kladr_house,
  rn kladr_house
);

--    
CREATE OR REPLACE FUNCTION diff$log() RETURNS trigger AS $$
DECLARE
  dst varchar = TG_TABLE_NAME || '$log';
  stmt text = '';
BEGIN
  --      
  IF TG_OP = 'UPDATE' THEN
    IF NEW IS NOT DISTINCT FROM OLD THEN
      RETURN NEW;
    END IF;
  END IF;
  --   
  stmt = 'INSERT INTO ' || dst::text || '(ro,rn)VALUES(';
  CASE TG_OP
    WHEN 'INSERT' THEN
      EXECUTE stmt || 'NULL,$1)' USING NEW;
    WHEN 'UPDATE' THEN
      EXECUTE stmt || '$1,$2)' USING OLD, NEW;
    WHEN 'DELETE' THEN
      EXECUTE stmt || '$1,NULL)' USING OLD;
  END CASE;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

Sekarang kami dapat memaksakan pemicu (atau mengaktifkan melalui ALTER TABLE ... ENABLE TRIGGER ...) sebelum memulai sinkronisasi :

CREATE TRIGGER log
  AFTER INSERT OR UPDATE OR DELETE
  ON kladr
    FOR EACH ROW
      EXECUTE PROCEDURE diff$log();

CREATE TRIGGER log
  AFTER INSERT OR UPDATE OR DELETE
  ON kladr_house
    FOR EACH ROW
      EXECUTE PROCEDURE diff$log();

Dan kemudian diam-diam dari tabel log kita mengekstrak semua perubahan yang kita butuhkan dan menjalankannya melalui penangan tambahan.

3.3. Impor set terkait


Di atas, kami mempertimbangkan kasus ketika struktur data sumber dan penerima bertepatan. Tetapi bagaimana jika pembongkaran dari sistem eksternal memiliki format yang berbeda dari struktur penyimpanan di basis data kami?

Ambil penyimpanan pelanggan dan akun mereka sebagai contoh, opsi klasik banyak-ke-satu:

CREATE TABLE client(
  client_id
    serial
      PRIMARY KEY
, inn
    varchar
      UNIQUE
, name
    varchar
);

CREATE TABLE invoice(
  invoice_id
    serial
      PRIMARY KEY
, client_id
    integer
      REFERENCES client(client_id)
, number
    varchar
, dt
    date
, sum
    numeric(32,2)
);

Tetapi pembongkaran dari sumber eksternal datang kepada kami dalam bentuk "semua dalam satu":

CREATE TEMPORARY TABLE invoice_import(
  client_inn
    varchar
, client_name
    varchar
, invoice_number
    varchar
, invoice_dt
    date
, invoice_sum
    numeric(32,2)
);

Jelas, data pelanggan dapat digandakan dengan cara ini, dan catatan utama adalah "akun":

0123456789;;A-01;2020-03-16;1000.00
9876543210;;A-02;2020-03-16;666.00
0123456789;;B-03;2020-03-16;9999.00

Untuk model, cukup masukkan data pengujian kami, tetapi ingat - COPYlebih efisien!

INSERT INTO invoice_import
VALUES
  ('0123456789', '', 'A-01', '2020-03-16', 1000.00)
, ('9876543210', '', 'A-02', '2020-03-16', 666.00)
, ('0123456789', '', 'B-03', '2020-03-16', 9999.00);

Pertama, kami memilih "potongan" yang mengacu pada "fakta" kami. Dalam kasus kami, akun merujuk ke pelanggan:

CREATE TEMPORARY TABLE client_import AS
SELECT DISTINCT ON(client_inn)
--   SELECT DISTINCT,    
  client_inn inn
, client_name "name"
FROM
  invoice_import;

Untuk mengaitkan akun dengan ID pelanggan dengan benar, kita harus terlebih dahulu mencari tahu atau menghasilkan pengidentifikasi ini. Tambahkan bidang untuk mereka:

ALTER TABLE invoice_import ADD COLUMN client_id integer;
ALTER TABLE client_import ADD COLUMN client_id integer;

Kami akan menggunakan metode sinkronisasi tabel dengan koreksi kecil yang dijelaskan di atas - kami tidak akan memperbarui atau menghapus apa pun di tabel target, karena mengimpor klien adalah "append-only":

--     ID   
UPDATE
  client_import T
SET
  client_id = D.client_id
FROM
  client D
WHERE
  T.inn = D.inn; -- unique key

--       ID
WITH ins AS (
  INSERT INTO client(
    inn
  , name
  )
  SELECT
    inn
  , name
  FROM
    client_import
  WHERE
    client_id IS NULL --  ID  
  RETURNING *
)
UPDATE
  client_import T
SET
  client_id = D.client_id
FROM
  ins D
WHERE
  T.inn = D.inn; -- unique key

--  ID    
UPDATE
  invoice_import T
SET
  client_id = D.client_id
FROM
  client_import D
WHERE
  T.client_inn = D.inn; --  

Sebenarnya, semuanya - invoice_importsekarang kami telah mengisi bidang komunikasi client_idyang dengannya kami akan memasukkan akun.

All Articles