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

94

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 ordan / atau like?

Lucas Kauffman
sumber
Mengapa Anda mencoba menulis ulang? Performa? Kerapian? Apakah s.namediindeks?
Martin Smith
Saya ingin menulis untuk kinerja, s.name tidak diindeks.
Lucas Kauffman
8
Baik saat Anda mencari tanpa kartu liar terkemuka dan tidak memilih kolom tambahan, indeks apa namepun bisa berguna di sini jika Anda peduli dengan kinerja.
Martin Smith

Jawaban:

161

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

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

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

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

Atau sedikit lebih cepat, dengan kelas karakter :

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

Pengujian cepat tanpa indeks menghasilkan hasil yang lebih cepat daripada SIMILAR TOdalam kasus mana pun untuk saya.
Dengan indeks B-Tree yang sesuai di tempat, LIKEmemenangkan perlombaan ini dengan urutan besarnya.

Baca dasar-dasar 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 perintah besarnya. Pertimbangan khusus berlaku untuk urutan sortir khusus-lokal. Baca lebih lanjut tentang kelas operator di manual . Jika Anda menggunakan lokal "C" standar (kebanyakan orang tidak), indeks biasa (dengan kelas operator default) akan dilakukan.

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

SIMILAR TOatau ekspresi reguler dengan ekspresi dasar berlabuh kiri dapat menggunakan indeks ini juga. Tetapi tidak dengan cabang (B|D)atau kelas karakter [BD](setidaknya dalam tes saya pada 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 sensitif.
    pg_trgm memperluas dukungan indeks untuk keduanya.

  • ~ (pencocokan ekspresi reguler) sangat kuat tetapi lebih kompleks dan mungkin lambat untuk apa pun selain ekspresi dasar.

  • SIMILAR TOtidak ada gunanya . Halfbreed aneh LIKEdan ekspresi reguler. Saya tidak pernah menggunakannya. Lihat di bawah.

  • % adalah operator "kesamaan", yang disediakan oleh modul tambahanpg_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_trgmuntuk memberikan dukungan indeks untuk pola / apa saja (dan pola regexp sederhana ) menggunakan indeks GIN atau GiST.LIKEILIKE~

Detail, contoh, dan tautan:

pg_trgmjuga menyediakan operator ini :

  • % - operator "kesamaan"
  • <%(komutator %>:) - operator "word_similarity" di Postgres 9.6 atau lebih baru
  • <<%(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 fuzzystrmatch tambahan menawarkan beberapa opsi lebih banyak, tetapi kinerja umumnya lebih rendah daripada semua yang di atas.

Secara khusus, berbagai implementasi levenshtein()fungsi dapat berperan.

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

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

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

SIMILAR TOhanya 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 ANALYZEmengungkapkannya. 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 TOtelah ditulis ulang dengan ekspresi reguler ( ~).

Kinerja terbaik untuk kasus khusus ini

Tetapi EXPLAIN ANALYZEmengungkapkan lebih banyak. Coba, dengan indeks yang disebutkan sebelumnya 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 locale-sadar ( text_pattern_opsatau menggunakan lokal C) ekspresi kiri-berlabuh sederhana yang ditulis ulang dengan operator pola teks ini: ~>=~, ~<=~, ~>~, ~<~. Ini adalah kasus untuk ~, ~~atau SIMILAR TOsama.

Hal yang sama berlaku untuk indeks pada varchartipe dengan varchar_pattern_opsatau chardengan 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 sederhana ~atau ~~kecil. Jika kinerja bukan persyaratan utama Anda, Anda harus tetap menggunakan operator standar - sampai pada apa yang sudah Anda miliki dalam pertanyaan.

Erwin Brandstetter
sumber
OP tidak memiliki indeks atas nama tetapi apakah Anda tahu, jika mereka melakukannya, apakah permintaan awal mereka melibatkan 2 rentang pencarian dan similarpemindaian?
Martin Smith
2
@ MartinSmith: Tes cepat dengan EXPLAIN ANALYZEmenunjukkan 2 scan indeks bitmap. Beberapa pemindaian indeks bitmap dapat digabungkan dengan lebih cepat.
Erwin Brandstetter
Terima kasih. Jadi apakah akan ada jarak tempuh dengan mengganti ORdengan UNION ALLatau mengganti name LIKE 'B%'dengan name >= 'B' AND name <'C'di Postgres?
Martin Smith
1
@ MartinSmith: UNIONtidak akan tetapi, ya, menggabungkan rentang menjadi satu WHEREklausa akan mempercepat permintaan. Saya telah menambahkan lebih banyak jawaban saya. Tentu saja, Anda harus memperhitungkan lokal Anda. Pencarian sadar-lokal selalu lebih lambat.
Erwin Brandstetter
2
@a_horse_with_no_name: Saya harap tidak. Kemampuan baru pg_tgrm dengan indeks GIN adalah suguhan untuk pencarian teks umum. Pencarian berlabuh di awal sudah lebih cepat dari itu.
Erwin Brandstetter
11

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, 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 ketika data dibuat atau diubah, jadi mungkin hanya sesuai untuk lingkungan aktivitas rendah (yaitu menulis lebih sedikit daripada membaca).

suatu hari nanti
sumber
8

Anda bisa mencoba

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
Martin Smith
sumber
1
Itu berhasil dan saya mendapat biaya 1,19 di mana saya punya 1,25. Terima kasih!
Lucas Kauffman
2

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

Mel Padden
sumber
2

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 () hanya terlihat pada karakter pertama dari string.

Sole021
sumber
1
Apakah ini menggunakan indeks (name)?
ypercubeᵀᴹ
2

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 ascii()slution oleh @ Sole021, tetapi itu tidak kompatibel dengan UTF8 (atau pengkodean lainnya dalam hal ini), hanya mengembalikan byte pertama, jadi sebaiknya hanya digunakan dalam kasus-kasus di mana perbandingannya terhadap plain old 7 -bit ASCII karakter.

Ziggy Crueltyfree Zeitgeister
sumber
1

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 pemartisian) - 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).

Tomasz Pala
sumber
-4

Mungkin lebih cepat untuk melakukan perbandingan satu karakter:

SUBSTR(s.name,1,1)='B' OR SUBSTR(s.name,1,1)='D'
pengguna2653985
sumber
1
Tidak juga. column LIKE 'B%'akan lebih efisien daripada menggunakan fungsi substring pada kolom.
ypercubeᵀᴹ