Cara mempercepat kinerja penyisipan di PostgreSQL

215

Saya sedang menguji kinerja penyisipan Postgres. Saya punya tabel dengan satu kolom dengan nomor sebagai tipe datanya. Ada juga indeksnya. Saya mengisi basis data menggunakan kueri ini:

insert into aNumber (id) values (564),(43536),(34560) ...

Saya memasukkan 4 juta baris dengan sangat cepat 10.000 sekaligus dengan permintaan di atas. Setelah database mencapai 6 juta baris, kinerja menurun drastis menjadi 1 Juta baris setiap 15 menit. Apakah ada trik untuk meningkatkan kinerja penyisipan? Saya membutuhkan kinerja penyisipan yang optimal pada proyek ini.

Menggunakan Windows 7 Pro pada mesin dengan RAM 5 GB.

Luke101
sumber
5
Anda juga perlu menyebutkan versi Pg dalam pertanyaan. Dalam hal ini tidak membuat banyak perbedaan, tetapi itu untuk banyak pertanyaan.
Craig Ringer
1
letakkan indeks di atas meja dan memicu jika ada dan jalankan skrip sisipkan. Setelah Anda menyelesaikan beban curah, Anda dapat membuat ulang indeks.
Sandeep

Jawaban:

481

Lihat mengisi database dalam manual PostgreSQL, artikel bagus seperti biasa pada depesz , dan pertanyaan SO ini .

(Perhatikan bahwa jawaban ini adalah tentang memuat data secara massal ke dalam DB yang ada atau membuat yang baru. Jika Anda tertarik DB memulihkan kinerja dengan pg_restoreatau psqlmengeksekusi pg_dumpoutput, banyak dari ini tidak berlaku sejak pg_dumpdan pg_restoresudah melakukan hal-hal seperti membuat pemicu dan indeks setelah selesai skema + pemulihan data) .

Banyak yang harus dilakukan. Solusi yang ideal adalah mengimpor ke UNLOGGEDtabel tanpa indeks, kemudian mengubahnya menjadi login dan menambahkan indeks. Sayangnya di PostgreSQL 9.4 tidak ada dukungan untuk mengubah tabel dari UNLOGGEDmenjadi login. 9.5 menambahkan ALTER TABLE ... SET LOGGEDuntuk mengizinkan Anda melakukan ini.

Jika Anda dapat membuat database Anda offline untuk impor massal, gunakan pg_bulkload.

Jika tidak:

  • Nonaktifkan semua pemicu di atas meja

  • Jatuhkan indeks sebelum memulai impor, buat kembali setelahnya. (Dibutuhkan jauh lebih sedikit waktu untuk membangun indeks dalam satu pass daripada untuk menambahkan data yang sama secara progresif, dan indeks yang dihasilkan jauh lebih kompak).

  • Jika melakukan impor dalam satu transaksi, aman untuk menghilangkan batasan kunci asing, melakukan impor, dan menciptakan kembali kendala sebelum melakukan. Jangan lakukan ini jika impor dibagi menjadi beberapa transaksi karena Anda mungkin memasukkan data yang tidak valid.

  • Jika memungkinkan, gunakan COPYsebagai ganti INSERTs

  • Jika Anda tidak dapat menggunakan, COPYpertimbangkan untuk menggunakan multi-nilai INSERTjika praktis. Anda sepertinya sudah melakukan ini. Jangan mencoba mendaftar terlalu banyak nilai dalam satu VALUES; nilai-nilai itu harus sesuai dengan memori beberapa kali lipat, jadi simpan beberapa ratus per pernyataan.

  • Batch insert Anda ke dalam transaksi eksplisit, lakukan ratusan ribu atau jutaan insert per transaksi. Tidak ada batasan praktis AFAIK, tetapi batching akan memungkinkan Anda pulih dari kesalahan dengan menandai awal setiap batch dalam data input Anda. Sekali lagi, Anda tampaknya sudah melakukan ini.

  • Gunakan synchronous_commit=offdan besar commit_delayuntuk mengurangi biaya fsync (). Ini tidak akan banyak membantu jika Anda telah memasukkan pekerjaan Anda ke dalam transaksi besar.

  • INSERTatau COPYsecara paralel dari beberapa koneksi. Berapa banyak tergantung pada subsistem disk perangkat keras Anda; Sebagai aturan praktis, Anda ingin satu koneksi per hard drive fisik jika menggunakan penyimpanan yang terpasang langsung.

  • Tetapkan checkpoint_segmentsnilai tinggi dan aktifkan log_checkpoints. Lihatlah log PostgreSQL dan pastikan tidak mengeluh tentang pos pemeriksaan yang terjadi terlalu sering.

  • Jika dan hanya jika Anda tidak keberatan kehilangan seluruh cluster PostgreSQL Anda (database Anda dan yang lainnya pada cluster yang sama) menjadi bencana besar jika sistem crash saat impor, Anda dapat menghentikan Pg, mengatur fsync=off, memulai Pg, melakukan impor, lalu (sangat) hentikan Pg dan atur fsync=onlagi. Lihat konfigurasi WAL . Jangan lakukan ini jika sudah ada data yang Anda pedulikan di basis data apa pun di instalasi PostgreSQL Anda. Jika Anda mengatur fsync=offAnda juga dapat mengatur full_page_writes=off; sekali lagi, ingatlah untuk mengaktifkannya kembali setelah impor Anda untuk mencegah korupsi basis data dan kehilangan data. Lihat pengaturan yang tidak tahan lama dalam manual Pg.

Anda juga harus melihat penyetelan sistem Anda:

  • Gunakan SSD berkualitas baik untuk penyimpanan sebanyak mungkin. SSD yang baik dengan cache write-back yang andal dan dilindungi daya membuat tingkat komit jauh lebih cepat. Mereka kurang bermanfaat ketika Anda mengikuti saran di atas - yang mengurangi flush disk / jumlah fsync()s - tetapi masih bisa sangat membantu. Jangan gunakan SSD murah tanpa perlindungan kegagalan daya yang benar kecuali Anda tidak peduli untuk menyimpan data Anda.

  • Jika Anda menggunakan RAID 5 atau RAID 6 untuk penyimpanan yang terhubung langsung, hentikan sekarang. Cadangkan data Anda, merestrukturisasi array RAID Anda ke RAID 10, dan coba lagi. RAID 5/6 tidak memiliki harapan untuk kinerja penulisan massal - meskipun pengontrol RAID yang baik dengan cache yang besar dapat membantu.

  • Jika Anda memiliki opsi untuk menggunakan pengontrol RAID perangkat keras dengan cache write-back yang didukung baterai besar ini benar-benar dapat meningkatkan kinerja penulisan untuk beban kerja dengan banyak komitmen. Tidak banyak membantu jika Anda menggunakan komit async dengan commit_delay atau jika Anda melakukan lebih sedikit transaksi besar selama pemuatan massal.

  • Jika memungkinkan, simpan WAL ( pg_xlog) pada disk / array disk yang terpisah. Ada sedikit gunanya menggunakan sistem file terpisah pada disk yang sama. Orang sering memilih untuk menggunakan pasangan RAID1 untuk WAL. Sekali lagi, ini memiliki lebih banyak efek pada sistem dengan tingkat komit yang tinggi, dan itu memiliki sedikit efek jika Anda menggunakan tabel yang tidak dicatat sebagai target pemuatan data.

Anda juga mungkin tertarik dalam Optimasi PostgreSQL untuk pengujian cepat .

Craig Ringer
sumber
1
Apakah Anda setuju bahwa penalti penulisan dari RAID 5/6 agak dikurangi jika SSD berkualitas baik digunakan? Jelas masih ada penalti, tapi saya pikir perbedaannya jauh lebih menyakitkan daripada dengan HDD.
1
Saya belum mengujinya. Saya akan mengatakan itu mungkin kurang buruk - efek amplifikasi tulis yang buruk dan (untuk menulis kecil) kebutuhan untuk siklus baca-modifikasi-tulis masih ada, tetapi hukuman berat untuk pencarian yang berlebihan harus menjadi masalah.
Craig Ringer
Bisakah kita menonaktifkan indeks alih-alih menjatuhkannya, misalnya, dengan mengatur indisvalid( postgresql.org/docs/8.3/static/catalog-pg-index.html ) ke false, lalu memuat data dan kemudian membawa indeks secara online REINDEX?
Vladislav Rastrusny
1
@CraigRinger Saya sudah menguji RAID-5 vs RAID-10 dengan SSD pada Perc H730. RAID-5 sebenarnya lebih cepat. Juga mungkin perlu dicatat bahwa memasukkan / transaksi dalam kombinasi dengan bytea besar tampaknya lebih cepat daripada menyalin. Secara keseluruhan, saran yang bagus.
atlaste
2
Adakah yang melihat adanya peningkatan kecepatan utama UNLOGGED? Tes cepat menunjukkan peningkatan 10-20%.
serg
15

Penggunaan COPY table TO ... WITH BINARYyang sesuai dengan dokumentasi adalah " agak lebih cepat daripada format teks dan CSV ." Hanya lakukan ini jika Anda memiliki jutaan baris untuk disisipkan, dan jika Anda merasa nyaman dengan data biner.

Berikut adalah contoh resep dalam Python, menggunakan psycopg2 dengan input biner .

Mike T
sumber
1
Mode biner dapat menghemat waktu pada beberapa input, seperti cap waktu, di mana penguraiannya nontrivial. Untuk banyak tipe data itu tidak menawarkan banyak manfaat atau bahkan bisa sedikit lebih lambat karena peningkatan bandwidth (misalnya bilangan bulat kecil). Poin yang bagus untuk meningkatkannya.
Craig Ringer
11

Selain posting Craig Ringer dan posting blog depesz yang luar biasa, jika Anda ingin mempercepat sisipan Anda melalui antarmuka ODBC ( psqlodbc ) dengan menggunakan sisipan pernyataan siap pakai dalam transaksi, ada beberapa hal tambahan yang perlu Anda lakukan untuk membuatnya bekerja cepat:

  1. Setel level-kesalahan-kembalikan-ke-kesalahan ke "Transaksi" dengan menentukan Protocol=-1dalam string koneksi. Secara default psqlodbc menggunakan level "Pernyataan", yang menciptakan SAVEPOINT untuk setiap pernyataan dan bukannya seluruh transaksi, membuat sisipan lebih lambat.
  2. Gunakan pernyataan yang disiapkan sisi server dengan menentukan UseServerSidePrepare=1dalam string koneksi. Tanpa opsi ini, klien mengirim seluruh pernyataan penyisipan beserta setiap baris yang dimasukkan.
  3. Nonaktifkan komit otomatis pada setiap pernyataan menggunakan SQLSetConnectAttr(conn, SQL_ATTR_AUTOCOMMIT, reinterpret_cast<SQLPOINTER>(SQL_AUTOCOMMIT_OFF), 0);
  4. Setelah semua baris dimasukkan, komit transaksi menggunakan SQLEndTran(SQL_HANDLE_DBC, conn, SQL_COMMIT);. Tidak perlu secara eksplisit membuka transaksi.

Sayangnya, psqlodbc "mengimplementasikan" SQLBulkOperationsdengan mengeluarkan serangkaian pernyataan sisipan yang tidak siap, sehingga untuk mencapai insert tercepat, seseorang perlu mengkodekan langkah-langkah di atas secara manual.

Maxim Egorushkin
sumber
Ukuran buffer socket yang besar, A8=30000000dalam string koneksi juga harus digunakan untuk mempercepat pemasangan.
Andrus
9

Saya menghabiskan sekitar 6 jam untuk masalah yang sama hari ini. Sisipan berjalan pada kecepatan 'reguler' (kurang dari 3detik per 100K) hingga 5MI (dari total 30MI) baris dan kemudian kinerja tenggelam secara drastis (hingga 1 menit per 100K).

Saya tidak akan mencantumkan semua hal yang tidak berhasil dan memotong langsung ke daging.

Saya menjatuhkan kunci utama pada tabel target (yang merupakan GUID) dan 30MI atau baris saya dengan gembira mengalir ke tujuan mereka dengan kecepatan konstan kurang dari 3detik per 100K.

Dennis
sumber
6

Jika Anda happend untuk memasukkan colums dengan UUIDs (yang tidak persis kasus Anda) dan untuk menambah @Dennis jawaban (aku tidak bisa komentar belum), menjadi nasihat daripada menggunakan gen_random_uuid () (membutuhkan PG 9.4 dan modul pgcrypto) adalah (a banyak) lebih cepat dari uuid_generate_v4 ()

=# explain analyze select uuid_generate_v4(),* from generate_series(1,10000);
                                                        QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------
 Function Scan on generate_series  (cost=0.00..12.50 rows=1000 width=4) (actual time=11.674..10304.959 rows=10000 loops=1)
 Planning time: 0.157 ms
 Execution time: 13353.098 ms
(3 filas)

vs.


=# explain analyze select gen_random_uuid(),* from generate_series(1,10000);
                                                        QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------
 Function Scan on generate_series  (cost=0.00..12.50 rows=1000 width=4) (actual time=252.274..418.137 rows=10000 loops=1)
 Planning time: 0.064 ms
 Execution time: 503.818 ms
(3 filas)

Juga, ini adalah cara resmi yang disarankan untuk melakukannya

Catatan

Jika Anda hanya memerlukan UUID yang dibuat secara acak (versi 4), pertimbangkan untuk menggunakan fungsi gen_random_uuid () dari modul pgcrypto.

Ini memasukkan waktu memasukkan dari ~ 2 jam hingga ~ 10 menit untuk 3,7M baris.

Francisco Reynoso
sumber
1

Untuk kinerja Penyisipan yang optimal, nonaktifkan indeks jika itu opsi untuk Anda. Selain itu, perangkat keras yang lebih baik (disk, memori) juga bermanfaat

Icarus
sumber
-1

Saya mengalami masalah kinerja penyisipan ini juga. Solusi saya adalah menelurkan beberapa rutinitas go untuk menyelesaikan pekerjaan penyisipan. Sementara itu, SetMaxOpenConnsharus diberi nomor yang benar jika tidak terlalu banyak kesalahan koneksi terbuka akan diperingatkan.

db, _ := sql.open() 
db.SetMaxOpenConns(SOME CONFIG INTEGER NUMBER) 
var wg sync.WaitGroup
for _, query := range queries {
    wg.Add(1)
    go func(msg string) {
        defer wg.Done()
        _, err := db.Exec(msg)
        if err != nil {
            fmt.Println(err)
        }
    }(query)
}
wg.Wait()

Kecepatan memuat jauh lebih cepat untuk proyek saya. Cuplikan kode ini hanya memberi gambaran bagaimana cara kerjanya. Pembaca harus dapat memodifikasinya dengan mudah.

Patrick
sumber
Nah, bisa dibilang begitu. Tapi itu mengurangi waktu berjalan dari beberapa jam menjadi beberapa menit untuk jutaan baris untuk kasus saya. :)
Patrick