Saya punya dua meja.
Yang pertama adalah tabel dengan awalan
code name price
343 ek1 10
3435 nt 4
3432 ek2 2
Kedua adalah catatan panggilan dengan nomor telepon
number time
834353212 10
834321242 20
834312345 30
Saya perlu menulis skrip yang menemukan awalan terpanjang dari awalan untuk setiap catatan, dan menulis semua data ini ke tabel ketiga, seperti ini:
number code ....
834353212 3435
834321242 3432
834312345 343
Untuk nomor 834353212 kita harus memotong '8', dan kemudian menemukan kode terpanjang dari tabel awalan, 3435-nya.
Kita harus selalu drop dulu '8' dan awalan harus di awal.
Saya menyelesaikan tugas ini sejak lama, dengan cara yang sangat buruk. Script perl yang mengerikan yang melakukan banyak pertanyaan untuk setiap record. Skrip ini:
Ambil nomor dari tabel panggilan, lakukan substring dari panjang (angka) ke 1 => $ awalan dalam loop
Lakukan kueri: pilih hitungan (*) dari awalan dengan kode seperti '$ awalan'
- Jika hitung> 0 maka ambil awalan pertama dan tulis ke dalam tabel
Masalah pertama adalah jumlah permintaan - itu call_records * length(number)
. Masalah kedua adalah LIKE
ekspresi. Saya khawatir itu lambat.
Saya mencoba memecahkan masalah kedua dengan:
CREATE EXTENSION pg_trgm;
CREATE INDEX prefix_idx ON prefix USING gist (code gist_trgm_ops);
Itu mempercepat setiap permintaan, tetapi tidak memecahkan masalah secara umum.
Saya memiliki 20k awalan dan angka 170k sekarang, dan solusi lama saya buruk. Sepertinya saya butuh solusi baru tanpa loop.
Hanya satu permintaan untuk setiap catatan panggilan atau sesuatu seperti ini.
sumber
code
pada tabel pertama sama dengan awalan nanti. Bisakah Anda menjelaskannya? Dan beberapa perbaikan dari contoh data dan output yang diinginkan (sehingga lebih mudah untuk mengikuti masalah Anda) juga akan diterima.Jawaban:
Saya mengasumsikan tipe data
text
untuk kolom yang relevan.Solusi "Sederhana"
Elemen kunci:
DISTINCT ON
adalah ekstensi Postgres dari standar SQLDISTINCT
. Temukan penjelasan terperinci untuk teknik kueri yang digunakan dalam jawaban terkait ini di SO .ORDER BY p.code DESC
memilih pertandingan terlama, karena urutan'1234'
setelah'123'
(dalam urutan menaik).Biola SQL Sederhana .
Tanpa indeks, kueri akan berjalan untuk waktu yang sangat lama (tidak menunggu sampai selesai). Untuk mempercepat ini, Anda memerlukan dukungan indeks. Indeks trigram yang Anda sebutkan, yang disediakan oleh modul tambahan
pg_trgm
adalah kandidat yang baik. Anda harus memilih antara indeks GIN dan GiST. Karakter pertama dari angka-angka hanyalah noise dan dapat dikecualikan dari indeks, menjadikannya sebagai indeks fungsional sebagai tambahan.Dalam tes saya, indeks GIN trigram fungsional memenangkan perlombaan atas indeks trigram GiST (seperti yang diharapkan):
Dbfiddle tingkat lanjut di sini .
Semua hasil tes berasal dari instalasi tes Postgres 9.1 lokal dengan pengaturan yang dikurangi: angka 17k dan kode 2k:
Jauh lebih cepat
Upaya gagal dengan
text_pattern_ops
Setelah kita mengabaikan karakter noise pertama yang mengganggu, itu turun ke pertandingan pola dasar berlabuh kiri . Oleh karena itu saya mencoba indeks B-tree fungsional dengan kelas operator
text_pattern_ops
(dengan asumsi tipe kolomtext
).Ini berfungsi baik untuk kueri langsung dengan satu istilah pencarian dan membuat indeks trigram terlihat buruk sebagai perbandingan:
Namun , perencana kueri tidak akan mempertimbangkan indeks ini untuk bergabung dengan dua tabel. Saya telah melihat batasan ini sebelumnya. Saya belum memiliki penjelasan yang berarti untuk ini.
Indeks pohon-B sebagian / fungsional
Alternatifnya menggunakan pemeriksaan kesetaraan pada string parsial dengan indeks parsial. Ini dapat digunakan dalam
JOIN
.Karena biasanya kami hanya memiliki sejumlah terbatas
different lengths
untuk awalan, kami dapat membangun solusi yang mirip dengan yang disajikan di sini dengan indeks parsial.Katakanlah, kami memiliki awalan mulai dari 1 hingga 5 karakter. Buat sejumlah indeks fungsional parsial, satu untuk setiap panjang awalan yang berbeda:
Karena ini adalah sebagian indeks, semuanya bersama-sama hampir tidak lebih besar dari satu indeks lengkap.
Tambahkan indeks yang cocok untuk angka (dengan memperhitungkan karakter derau terkemuka):
Meskipun indeks ini hanya memiliki masing-masing substring dan sebagian, masing-masing mencakup sebagian besar atau seluruh tabel. Jadi mereka jauh lebih besar bersama daripada satu indeks total - kecuali untuk angka yang panjang. Dan mereka memaksakan lebih banyak pekerjaan untuk operasi penulisan. Itulah biaya untuk kecepatan luar biasa.
Jika biaya itu terlalu tinggi untuk Anda (kinerja penulisan penting / terlalu banyak operasi penulisan / ruang disk menjadi masalah), Anda dapat melewati indeks ini. Sisanya masih lebih cepat, jika tidak secepat ...
Jika angka tidak pernah lebih pendek dari
n
karakter, jatuhkanWHERE
klausa yang berlebihan dari beberapa atau semua, dan jatuhkan jugaWHERE
klausa yang sesuai dari semua kueri berikut.CTE rekursif
Dengan semua pengaturan sejauh ini saya berharap untuk solusi yang sangat elegan dengan CTE rekursif :
Namun, meskipun kueri ini tidak buruk - kinerjanya sebagus versi sederhana dengan indeks GIN trigram - itu tidak memberikan apa yang saya tuju. Istilah rekursif hanya direncanakan sekali, sehingga tidak dapat menggunakan indeks terbaik. Hanya istilah non-rekursif yang bisa.
UNION ALL
Karena kita berurusan dengan sejumlah kecil rekursi, kita bisa menguraikannya berulang-ulang. Ini memungkinkan rencana yang dioptimalkan untuk masing-masingnya. (Namun, kami kehilangan pengecualian rekursif dari angka yang sudah sukses. Jadi masih ada ruang untuk perbaikan, terutama untuk rentang panjang awalan yang lebih luas)):
Terobosan, akhirnya!
Fungsi SQL
Membungkus ini ke dalam fungsi SQL menghapus overhead perencanaan kueri untuk penggunaan berulang:
Panggilan:
PL / pgSQL berfungsi dengan SQL dinamis
Fungsi plpgsql ini sangat mirip dengan CTE rekursif di atas, tetapi SQL dinamis dengan
EXECUTE
memaksa kueri untuk direncanakan ulang untuk setiap iterasi. Sekarang ia menggunakan semua indeks yang disesuaikan.Selain itu ini berfungsi untuk berbagai rentang panjang awalan. Fungsi ini mengambil dua parameter untuk rentang, tetapi saya menyiapkannya dengan
DEFAULT
nilai, sehingga berfungsi tanpa parameter eksplisit juga:Langkah terakhir tidak bisa dimasukkan ke dalam satu fungsi dengan mudah. Entah sebut saja seperti ini:
Atau gunakan fungsi SQL lain sebagai pembungkus:
Panggilan:
Sedikit lebih lambat karena overhead perencanaan yang diperlukan. Tetapi lebih fleksibel daripada SQL dan lebih pendek untuk awalan yang lebih panjang.
sumber
String S adalah awalan dari string T iff T adalah antara S dan SZ di mana Z secara leksikografis lebih besar daripada string lain (misalnya 99999999 dengan cukup 9 untuk melebihi nomor telepon terpanjang yang mungkin dalam dataset, atau kadang-kadang 0xFF akan bekerja).
Awalan umum terpanjang untuk setiap T yang diberikan juga maksimal secara leksikografis, sehingga grup sederhana pada akhirnya akan menemukannya.
Jika ini lambat, kemungkinan karena ekspresi yang dihitung, jadi Anda juga dapat mencoba mematerialisasi p.code || '999999' ke dalam kolom di tabel kode dengan indeksnya sendiri, dll.
sumber