it-swarm-id.com

Pencocokan pola dengan LIKE, SIMILAR TO atau ekspresi reguler di PostgreSQL

Saya harus menulis kueri sederhana tempat saya mencari nama orang yang dimulai dengan B atau D:

SELECT s.name 
FROM spelers s 
WHERE s.name LIKE 'B%' OR s.name LIKE 'D%'
ORDER BY 1

Saya bertanya-tanya apakah ada cara untuk menulis ulang ini menjadi lebih banyak performan. Jadi saya bisa menghindari or dan/atau like?

103
Lucas Kauffman

Permintaan Anda cukup optimal. Sintaks tidak akan menjadi jauh lebih pendek, permintaan tidak akan jauh lebih cepat:

SELECT name
FROM   spelers
WHERE  name LIKE 'B%' OR name LIKE 'D%'
ORDER  BY 1;

Jika Anda benar-benar ingin memperpendek sintaks, gunakan ekspresi reguler dengan cabang:

...
WHERE  name ~ '^(B|D).*'

Atau sedikit lebih cepat, dengan kelas karakter:

...
WHERE  name ~ '^[BD].*'

Tes cepat tanpa indeks menghasilkan hasil lebih cepat daripada untuk SIMILAR TO Dalam kedua kasus untuk saya.
Dengan indeks B-Tree yang sesuai, LIKE memenangkan perlombaan ini berdasarkan urutan besarnya.

Baca dasar-dasarnya tentang pencocokan pola dalam manual .

Indeks untuk kinerja yang unggul

Jika Anda khawatir dengan kinerja, buat indeks seperti ini untuk tabel yang lebih besar:

CREATE INDEX spelers_name_special_idx ON spelers (name text_pattern_ops);

Membuat kueri semacam ini lebih cepat dengan pesanan besar. Pertimbangan khusus berlaku untuk urutan sortir khusus-lokal. Baca lebih lanjut tentang kelas operator dalam manual . Jika Anda menggunakan lokal "C" standar (kebanyakan orang tidak), indeks biasa (dengan kelas operator default) akan melakukannya.

Indeks semacam itu hanya baik untuk pola berlabuh kiri (cocok dari awal string).

SIMILAR TO Atau ekspresi reguler dengan ekspresi dasar berlabuh kiri juga dapat menggunakan indeks ini. Tetapi tidak dengan cabang (B|D) Atau kelas karakter [BD] (Setidaknya dalam tes saya di PostgreSQL 9.0).

Pencocokan trigram atau pencarian teks menggunakan indeks GIN atau Gist khusus.

Gambaran umum operator pencocokan pola

  • LIKE (~~) sederhana dan cepat tetapi terbatas dalam kemampuannya.
    ILIKE (~~*) varian case case-sensitive.
    pg_trgm memperluas dukungan indeks untuk keduanya.

  • ~ (kecocokan ekspresi reguler) sangat kuat tetapi lebih kompleks dan mungkin lambat untuk hal lain selain dasar ekspresi.

  • SIMILAR TO hanya pointless. Halfbreed khusus LIKE dan ekspresi reguler. Saya tidak pernah menggunakannya. Lihat di bawah.

  • % adalah operator "kesamaan", disediakan oleh modul tambahan pg_trgm. Lihat di bawah.

  • @@ adalah operator pencarian teks. Lihat di bawah.

pg_trgm - pencocokan trigram

Dimulai dengan PostgreSQL 9.1 Anda dapat memfasilitasi ekstensi pg_trgm untuk memberikan dukungan indeks untuk apa saja LIKE/ILIKE pola (dan pola regexp sederhana dengan ~) menggunakan indeks GIN atau Gist.

Detail, contoh, dan tautan:

pg_trgm Juga menyediakan operator ini :

  • % - operator "kesamaan"
  • <% (komutator: %>) - operator "Word_similarity" di Postgres 9.6 atau lebih tinggi
  • <<% (komutator: %>>) - operator "strict_Word_similarity" di Postgres 11 atau lebih baru

Pencarian Teks

Merupakan jenis khusus pencocokan pola dengan infrastruktur dan tipe indeks terpisah. Ini menggunakan kamus dan stemming dan merupakan alat yang hebat untuk menemukan kata-kata dalam dokumen, terutama untuk bahasa alami.

Pencocokan awalan juga didukung:

Serta pencarian frasa sejak Postgres 9.6:

Pertimbangkan pengantar dalam manual dan ikhtisar operator dan fungsi .

Alat tambahan untuk pencocokan string fuzzy

Modul tambahan fuzzystrmatch menawarkan beberapa opsi lagi, tetapi kinerja umumnya lebih rendah daripada semua yang di atas.

Secara khusus, berbagai implementasi fungsi levenshtein() dapat menjadi instrumen.

Mengapa ekspresi reguler (~) Selalu lebih cepat daripada SIMILAR TO?

Jawabannya sederhana. SIMILAR TO Ekspresi ditulis ulang menjadi ekspresi reguler secara internal. Jadi, untuk setiap ekspresi SIMILAR TO, Ada setidaknya satu ekspresi reguler yang lebih cepat (yang menghemat biaya penulisan ulang ekspresi). Tidak ada keuntungan kinerja dalam menggunakan SIMILAR TO lamanya .

Dan ekspresi sederhana yang dapat dilakukan dengan LIKE (~~) Lebih cepat dengan LIKE.

SIMILAR TO Hanya didukung di PostgreSQL karena berakhir pada konsep awal standar SQL. Mereka masih belum menyingkirkannya. Tapi ada rencana untuk menghapusnya dan memasukkan pertandingan regexp - atau begitulah yang saya dengar.

EXPLAIN ANALYZE Mengungkapkannya. Coba saja dengan meja apa saja sendiri!

EXPLAIN ANALYZE SELECT * FROM spelers WHERE name SIMILAR TO 'B%';

Mengungkapkan:

...  
Seq Scan on spelers  (cost= ...  
  Filter: (name ~ '^(?:B.*)$'::text)

SIMILAR TO Telah ditulis ulang dengan ekspresi reguler (~).

Kinerja terbaik untuk kasus khusus ini

Tetapi EXPLAIN ANALYZE Mengungkapkan lebih banyak. Coba, dengan indeks yang disebutkan di tempat:

EXPLAIN ANALYZE SELECT * FROM spelers WHERE name ~ '^B.*;

Mengungkapkan:

...
 ->  Bitmap Heap Scan on spelers  (cost= ...
       Filter: (name ~ '^B.*'::text)
        ->  Bitmap Index Scan on spelers_name_text_pattern_ops_idx (cost= ...
              Index Cond: ((prod ~>=~ 'B'::text) AND (prod ~<~ 'C'::text))

Secara internal, dengan indeks yang tidak menyadari lokal (text_pattern_ops Atau menggunakan lokal C) ekspresi berlabuh kiri sederhana ditulis ulang dengan operator pola teks ini: ~>=~, ~<=~, ~>~, ~<~. Ini adalah kasus untuk ~, ~~ Atau SIMILAR TO Sama.

Hal yang sama berlaku untuk indeks pada varchar jenis dengan varchar_pattern_ops Atau char dengan bpchar_pattern_ops.

Jadi, diterapkan pada pertanyaan awal, ini adalah cara tercepat yang mungkin :

SELECT name
FROM   spelers  
WHERE  name ~>=~ 'B' AND name ~<~ 'C'
    OR name ~>=~ 'D' AND name ~<~ 'E'
ORDER  BY 1;

Tentu saja, jika Anda kebetulan mencari inisial yang berdekatan , Anda dapat menyederhanakan lebih lanjut:

WHERE  name ~>=~ 'B' AND name ~<~ 'D'   -- strings starting with B or C

Keuntungan atas penggunaan ~ Atau ~~ Yang sederhana sangat kecil. Jika kinerja bukan persyaratan utama Anda, Anda harus tetap menggunakan operator standar - sampai pada apa yang sudah Anda miliki dalam pertanyaan.

171

Bagaimana menambahkan kolom ke tabel. Tergantung pada kebutuhan Anda yang sebenarnya:

person_name_start_with_B_or_D (Boolean)

person_name_start_with_char CHAR(1)

person_name_start_with VARCHAR(30)

PostgreSQL tidak mendukung kolom yang dikomputasi dalam tabel dasar ala SQL Server tetapi kolom baru dapat dipertahankan melalui pemicu. Jelas, kolom baru ini akan diindeks.

Atau, sebuah indeks pada ekspresi akan memberi Anda hal yang sama, lebih murah. Misalnya.:

CREATE INDEX spelers_name_initial_idx ON spelers (left(name, 1)); 

Kueri yang cocok dengan ekspresi dalam kondisi mereka dapat memanfaatkan indeks ini.

Dengan cara ini, hit kinerja diambil saat data dibuat atau diubah, jadi mungkin hanya sesuai untuk lingkungan aktivitas rendah (mis. Jauh lebih sedikit menulis daripada membaca).

11
onedaywhen

Anda dapat coba

SELECT s.name
FROM   spelers s
WHERE  s.name SIMILAR TO '(B|D)%' 
ORDER  BY s.name

Saya tidak tahu apakah ekspresi di atas atau asli Anda masuk dalam Postgres.

Jika Anda membuat indeks yang disarankan juga akan tertarik untuk mendengar bagaimana ini membandingkan dengan opsi lain.

SELECT name
FROM   spelers
WHERE  name >= 'B' AND name < 'C'
UNION ALL
SELECT name
FROM   spelers
WHERE  name >= 'D' AND name < 'E'
ORDER  BY name
8
Martin Smith

Untuk memeriksa inisial, saya sering menggunakan casting untuk "char" (Dengan tanda kutip ganda). Ini tidak portabel, tetapi sangat cepat. Secara internal, itu hanya detoasts teks dan mengembalikan karakter pertama, dan operasi perbandingan "char" sangat cepat karena jenisnya adalah 1 byte panjang tetap:

SELECT s.name 
FROM spelers s 
WHERE s.name::"char" =ANY( ARRAY[ "char" 'B', 'D' ] )
ORDER BY 1

Perhatikan bahwa casting ke "char" Lebih cepat daripada slution ascii() oleh @ Sole021, tetapi itu tidak kompatibel dengan UTF8 (atau enkode lain apa pun dalam hal ini), hanya mengembalikan byte pertama, jadi sebaiknya hanya digunakan dalam kasus-kasus di mana perbandingannya bertentangan dengan karakter 7-bit ASCII lama.

Pertanyaan yang sangat lama, tetapi saya menemukan solusi cepat untuk masalah ini:

SELECT s.name 
FROM spelers s 
WHERE ascii(s.name) in (ascii('B'),ascii('D'))
ORDER BY 1

Karena fungsi ascii () terlihat hanya pada karakter pertama dari string.

2
Sole021

Apa yang telah saya lakukan di masa lalu, dihadapkan dengan masalah kinerja yang serupa, adalah untuk meningkatkan karakter ASCII dari huruf terakhir, dan melakukan BETWEEN. Anda kemudian mendapatkan kinerja terbaik, untuk subset dari fungsi LIKE. Tentu saja, ini hanya berfungsi dalam situasi tertentu, tetapi untuk dataset ultra-besar di mana Anda mencari nama misalnya, itu membuat kinerja berubah dari buruk menjadi dapat diterima.

2
Mel Padden

Ada dua metode yang belum disebutkan untuk menangani kasus-kasus tersebut:

  1. sebagian (atau dipartisi - jika dibuat untuk rentang penuh secara manual) indeks - paling berguna ketika hanya sebagian dari data yang diperlukan (misalnya selama beberapa pemeliharaan atau sementara untuk beberapa pelaporan):

    CREATE INDEX ON spelers WHERE name LIKE 'B%'
    
  2. mempartisi tabel itu sendiri (menggunakan karakter pertama sebagai kunci pemartisi) - teknik ini sangat layak dipertimbangkan dalam PostgreSQL 10+ (partisi yang tidak terlalu menyakitkan) dan 11+ (pemangkasan partisi saat eksekusi query).

Selain itu, jika data dalam tabel diurutkan, orang bisa mendapat manfaat dari menggunakan indeks BRIN (lebih dari karakter pertama).

1
Tomasz Pala