Mempercepat pembuatan indeks parsial Postgres

8

Saya mencoba membuat indeks parsial untuk tabel statis besar (1.2TB) di Postgres 9.4.

Data saya benar-benar statis, jadi saya bisa memasukkan semua data, lalu membuat semua indeks.

Dalam tabel 1.2TB ini, saya memiliki kolom bernama run_idyang membagi data dengan bersih. Kami mendapatkan kinerja luar biasa dengan membuat indeks yang mencakup kisaran run_ids. Ini sebuah contoh:

CREATE INDEX perception_run_frame_idx_run_266_thru_270
ON run.perception
(run_id, frame)
WHERE run_id >= 266 AND run_id <= 270;

Indeks parsial ini memberi kami kecepatan permintaan yang diinginkan. Sayangnya, pembuatan setiap indeks parsial membutuhkan waktu sekitar 70 menit.

Sepertinya kami adalah CPU terbatas ( topmenunjukkan 100% untuk proses).
Adakah yang bisa saya lakukan untuk mempercepat pembuatan indeks parsial kami?

Spesifikasi sistem:

  • 18 inti Xeon
  • RAM 192 GB
  • 12 SSD dalam RAID
  • Autovacuum dimatikan
  • maintenance_work_mem: 64GB (Terlalu tinggi?)

Spesifikasi tabel:

  • Ukuran: 1,26 TB
  • Jumlah baris: 10,537 Miliar
  • Ukuran indeks umum: 3.2GB (ada varian ~ .5GB)

Definisi tabel:

CREATE TABLE run.perception(
id bigint NOT NULL,
run_id bigint NOT NULL,
frame bigint NOT NULL,
by character varying(45) NOT NULL,
by_anyone bigint NOT NULL,
by_me bigint NOT NULL,
by_s_id integer,
owning_p_id bigint NOT NULL,
obj_type_set bigint,
seq integer,
subj_id bigint NOT NULL,
subj_state_frame bigint NOT NULL,
CONSTRAINT perception_pkey PRIMARY KEY (id))

(Jangan terlalu banyak membaca nama kolom - Saya agak mengaburkannya.)

Info latar belakang:

  • Kami memiliki tim tamu di tempat yang menggunakan data ini, tetapi sebenarnya hanya ada satu atau dua pengguna. (Data ini semua dihasilkan melalui simulasi.) Pengguna hanya mulai menganalisis data setelah sisipan selesai dan indeks dibuat sepenuhnya. Perhatian utama kami adalah mengurangi waktu yang diperlukan untuk menghasilkan data yang dapat digunakan, dan saat ini bottleneck adalah waktu pembuatan indeks.
  • Kecepatan permintaan sudah sepenuhnya memadai saat menggunakan sebagian. Bahkan, saya pikir kita bisa meningkatkan jumlah proses yang dicakup oleh masing-masing indeks, dan masih mempertahankan kinerja kueri yang cukup baik.
  • Dugaan saya adalah bahwa kita harus mempartisi tabel. Kami mencoba menguras semua opsi lain sebelum mengambil rute itu.
terbakar
sumber
Informasi tambahan ini akan berperan: tipe data kolom yang terlibat, kueri tipikal, kardinalitas (jumlah baris), berapa banyak perbedaan run_id? Didistribusikan secara merata? Ukuran indeks yang dihasilkan pada disk? Data statis, ok. Tetapi apakah Anda satu-satunya pengguna?
Erwin Brandstetter
Diperbarui dengan lebih banyak info.
burnsy
1
" Autovacuums dimatikan " - mengapa? Itu ide yang sangat buruk. Ini mencegah pengumpulan statistik dan karenanya akan menghasilkan rencana kueri yang buruk
a_horse_with_no_name
@a_horse_with_no_name Kami secara manual memulai analisis setelah semua data dimasukkan
burnsy
Situasi Anda masih belum jelas bagi saya. Seperti apa tampilan pertanyaan Anda? Jika meja Anda completely static, lalu apa yang Anda maksud We have a separate team onsite that consumes this data? Apakah Anda hanya mengindeks rentang run_id >= 266 AND run_id <= 270atau seluruh tabel? Berapa harapan hidup setiap indeks / berapa banyak permintaan akan menggunakannya? Berapa banyak nilai yang berbeda untuk run_id? Kedengarannya seperti ~ 15 Mio. baris per run_id, yang akan membuatnya sekitar 800 nilai berbeda untuk run_id? Mengapa obj_type_set, by_s_id, seqtidak didefinisikan NOT NULL? Berapa persentase kasar nilai NULL untuk masing-masing?
Erwin Brandstetter

Jawaban:

8

Indeks BRIN

Tersedia sejak Postgres 9.5 dan mungkin hanya apa yang Anda cari. Pembuatan indeks jauh lebih cepat, indeks jauh lebih kecil. Tetapi pertanyaan biasanya tidak secepat. Manual:

BRIN adalah singkatan dari Block Range Index. BRIN dirancang untuk menangani tabel yang sangat besar di mana kolom tertentu memiliki korelasi alami dengan lokasi fisiknya di dalam tabel. Sebuah blok rentang adalah sekelompok halaman yang berdekatan secara fisik dalam tabel; untuk setiap rentang blok, beberapa info ringkasan disimpan oleh indeks.

Baca terus, masih ada lagi.
Depesz melakukan tes pendahuluan.

Optimum untuk kasus Anda: Jika Anda dapat menulis baris berkerumun di run_id, indeks Anda menjadi sangat kecil dan penciptaan jauh lebih murah.

CREATE INDEX foo ON run.perception USING brin (run_id, frame)
WHERE run_id >= 266 AND run_id <= 270;

Anda bahkan mungkin hanya mengindeks seluruh tabel .

Tata letak meja

Apa pun yang Anda lakukan, Anda dapat menyimpan 8 byte yang hilang karena pengisian karena persyaratan pelurusan per baris dengan menyusun kolom seperti ini:

CREATE TABLE run.perception(
  id               bigint NOT NULL PRIMARY KEY
, run_id           bigint NOT NULL
, frame            bigint NOT NULL
, by_anyone        bigint NOT NULL
, by_me            bigint NOT NULL
, owning_p_id      bigint NOT NULL
, subj_id          bigint NOT NULL
, subj_state_frame bigint NOT NULL
, obj_type_set     bigint
, by_s_id          integer
, seq              integer
, by               varchar(45) NOT NULL -- or just use type text
);

Jadikan meja Anda 79 GB lebih kecil jika tidak ada kolom yang memiliki nilai NULL. Detail:

Selain itu, Anda hanya memiliki tiga kolom yang bisa NULL. Bitmap NULL menempati 8 byte untuk 9 - 72 kolom. Jika hanya satu kolom integer adalah NULL, ada kasus sudut untuk paradoks penyimpanan: akan lebih murah untuk menggunakan nilai dummy sebagai gantinya: 4 byte terbuang tetapi 8 byte disimpan dengan tidak memerlukan bitmap NULL untuk baris. Lebih detail di sini:

Indeks sebagian

Bergantung pada permintaan Anda yang sebenarnya, mungkin lebih efisien untuk memiliki lima indeks parsial ini daripada yang di atas:

CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 266;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 267;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 268;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 269;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 270;

Jalankan satu transaksi untuk masing-masing.

Menghapus run_idsebagai kolom indeks dengan cara ini menghemat 8 byte per entri indeks - 32 bukannya 40 byte per baris. Setiap indeks juga lebih murah untuk dibuat, tetapi membuat lima alih-alih hanya satu membutuhkan waktu yang jauh lebih lama untuk tabel yang terlalu besar untuk disimpan dalam cache (seperti @ Jürgen dan @Chris berkomentar). Sehingga mungkin atau mungkin tidak bermanfaat bagi Anda.

Partisi

Berdasarkan warisan - satu-satunya pilihan hingga Postgres 9.5.
(Partisi deklaratif baru di Postgres 11 atau, lebih disukai, 12 lebih pintar.)

Manual:

Semua kendala pada semua anak dari tabel induk diperiksa selama pengecualian kendala, sehingga sejumlah besar partisi cenderung meningkatkan waktu perencanaan kueri. Jadi partisi berdasarkan warisan akan bekerja dengan baik hingga mungkin seratus partisi ; jangan mencoba menggunakan ribuan partisi.

Penekanan berani saya. Akibatnya, memperkirakan 1000 nilai yang berbeda untuk run_id, Anda akan membuat partisi yang masing-masing sekitar 10 nilai.


maintenance_work_mem

Saya melewatkan bahwa Anda sudah menyesuaikan untuk maintenance_work_memdi baca pertama saya. Saya akan meninggalkan kutipan dan saran dalam jawaban saya untuk referensi. Per dokumentasi:

maintenance_work_mem (bilangan bulat)

Menentukan jumlah maksimum memori yang akan digunakan oleh operasi pemeliharaan, seperti VACUUM, CREATE INDEX, dan ALTER TABLE ADD FOREIGN KEY. Standarnya adalah 64 megabita ( 64MB). Karena hanya satu dari operasi ini yang dapat dieksekusi pada suatu waktu oleh sesi database, dan instalasi biasanya tidak memiliki banyak dari mereka yang berjalan secara bersamaan, aman untuk menetapkan nilai ini secara signifikan lebih besar daripada work_mem. Pengaturan yang lebih besar dapat meningkatkan kinerja untuk menyedot debu dan untuk memulihkan kesedihan basis data.

Perhatikan bahwa saat autovacuumdijalankan, autovacuum_max_workersmemori ini mungkin dialokasikan hingga kali, jadi berhati-hatilah untuk tidak menetapkan nilai standar terlalu tinggi. Mungkin bermanfaat untuk mengontrolnya secara terpisah setting autovacuum_work_mem.

Saya hanya akan mengaturnya setinggi yang diperlukan - yang tergantung pada ukuran indeks yang tidak diketahui (untuk kami). Dan hanya secara lokal untuk sesi eksekusi. Seperti yang dijelaskan dalam kutipan, pengaturan umum yang terlalu tinggi dapat membuat server kelaparan, karena autovacuum dapat mengklaim lebih banyak RAM. Juga, jangan mengaturnya jauh lebih tinggi dari yang dibutuhkan, bahkan dalam sesi eksekusi, RAM bebas mungkin digunakan dengan baik dalam data caching.

Itu bisa terlihat seperti ini:

BEGIN;

SET LOCAL maintenance_work_mem = 10GB;  -- depends on resulting index size

CREATE INDEX perception_run_frame_idx_run_266_thru_270 ON run.perception(run_id, frame)
WHERE run_id >= 266 AND run_id <= 270;

COMMIT;

Tentang SET LOCAL:

Efek yang SET LOCALbertahan hanya sampai akhir transaksi saat ini, baik yang dilakukan maupun tidak.

Untuk mengukur ukuran objek:

Server umumnya harus dikonfigurasi secara wajar jika tidak, jelas.

Erwin Brandstetter
sumber
Saya yakin pekerjaannya terikat IO karena meja jauh lebih besar dari RAM. Membaca tabel lebih sering akan memperburuk masalah, terlepas dari apakah ada cukup memori untuk mengurutkan setiap indeks yang dibuat dalam memori atau tidak.
Jürgen Strobel
Saya dengan Jurgen untuk yang ini. Saya percaya bahwa karena ukuran tabel, pada dasarnya Anda harus melakukan pemindaian sekuensial penuh pada tabel per indeks yang dibuat. Plus, saya tidak yakin Anda akan melihat semua peningkatan kinerja dari membuat indeks parsial yang terpisah (saya 90% yakin Anda tidak akan melihat peningkatan apa pun, tetapi dalam hal ini saya bisa mati.) Saya percaya lebih baik solusi untuk pembuatan indeks akan melibatkan pembuatan satu indeks pada rentang penuh yang ingin Anda query sebagai "indeks parsial tunggal" untuk menjaga waktu build keseluruhan turun.
Chris
@ Chris: Saya setuju, 5 indeks akan membutuhkan waktu lebih lama untuk dibuat daripada hanya satu (meskipun semuanya secara keseluruhan lebih kecil, membuat setiap indeks lebih murah dan kueri mungkin lebih cepat). Kalau dipikir-pikir lagi, ini harus menjadi kasus penggunaan yang sempurna untuk indeks BRIN di Postgres 9.5.
Erwin Brandstetter
3

Mungkin ini hanya rekayasa berlebihan. Sudahkah Anda benar-benar mencoba menggunakan satu indeks penuh? Indeks parsial yang mencakup seluruh tabel bersama-sama tidak memberikan banyak keuntungan, jika ada, untuk pencarian indeks, dan dari teks Anda, saya menyimpulkan bahwa Anda memiliki indeks untuk semua run_ids? Mungkin ada beberapa keuntungan untuk memindai indeks dengan indeks parsial, masih saya akan membandingkan solusi satu indeks sederhana terlebih dahulu.

Untuk setiap pembuatan indeks, Anda memerlukan pemindaian IO terikat penuh melalui tabel. Jadi membuat beberapa indeks parsial membutuhkan jauh lebih banyak IO membaca tabel daripada untuk indeks tunggal, meskipun pengurutan akan tumpah ke disk untuk indeks tunggal besar. Jika Anda bersikeras pada indeks parsial, Anda dapat mencoba membangun semua (atau beberapa) indeks pada saat yang bersamaan secara paralel (memungkinkan memori).

Untuk perkiraan kasar tentang maintenance_work_mem diperlukan untuk mengurutkan semua run_ids, yang merupakan bigint 8-byte, dalam memori Anda akan memerlukan 10,5 * 8 GB + beberapa overhead.

Jürgen Strobel
sumber
0

Anda juga bisa membuat indeks pada tablespace lain selain default. Tablespace ini bisa mengarah ke disk yang tidak berlebihan (hanya membuat ulang indeks jika gagal), atau berada di array yang lebih cepat.

Anda juga dapat mempertimbangkan mempartisi tabel menggunakan kriteria yang sama dengan indeks parsial Anda. Ini akan memungkinkan untuk kecepatan yang sama dengan indeks saat query, tanpa benar-benar membuat indeks apa pun.

Kirk Roybal
sumber