Bagaimana saya bisa menghasilkan semua substring trailing mengikuti delimeter?


8

Diberikan string yang mungkin berisi beberapa contoh pembatas, saya ingin membuat semua substring dimulai setelah karakter itu.

Sebagai contoh, diberi string seperti 'a.b.c.d.e'(atau array {a,b,c,d,e}, saya kira), saya ingin menghasilkan array seperti:

{a.b.c.d.e, b.c.d.e, c.d.e, d.e, e}

Penggunaan yang dimaksud adalah sebagai pemicu untuk mengisi kolom agar lebih mudah untuk menanyakan bagian nama domain (mis. Temukan semua q.x.t.comuntuk kueri t.com) setiap kali kolom lain ditulis.

Sepertinya cara yang canggung untuk menyelesaikan ini (dan itu mungkin sangat baik), tapi sekarang saya ingin tahu bagaimana fungsi seperti ini dapat ditulis dalam SQL (Postgres ').

Ini adalah nama domain email sehingga sulit untuk mengatakan berapa jumlah elemen maksimum yang mungkin, tetapi tentu saja sebagian besar adalah <5.


@ ErwinBrandstetter ya. Maaf atas keterlambatan (hari libur dll). Saya memilih jawaban indeks trigram karena itu benar-benar memecahkan masalah saya yang sebenarnya. Namun saya peka terhadap fakta bahwa pertanyaan saya secara khusus tentang bagaimana saya dapat memecah string dengan cara ini (demi rasa ingin tahu) jadi saya tidak yakin apakah saya telah menggunakan metrik terbaik untuk memilih jawaban yang diterima.
Bo Jeanes

Jawaban terbaik adalah yang terbaik untuk menjawab pertanyaan yang diberikan. Pada akhirnya, itu pilihan Anda. Dan yang terpilih sepertinya adalah kandidat yang sah untuk saya.
Erwin Brandstetter

Jawaban:


3

Saya tidak berpikir Anda perlu kolom terpisah di sini; ini adalah masalah XY. Anda hanya mencoba melakukan pencarian sufiks. Ada dua cara utama untuk mengoptimalkannya.

Ubah kueri sufiks menjadi kueri awalan

Anda pada dasarnya melakukan ini dengan membalikkan semuanya.

Pertama-tama buat indeks di bagian belakang kolom Anda:

CREATE INDEX ON yourtable (reverse(yourcolumn) text_pattern_ops);

Kemudian kueri menggunakan yang sama:

SELECT * FROM yourtable WHERE reverse(yourcolumn) LIKE reverse('%t.com');

Anda dapat melakukan UPPERpanggilan jika Anda ingin membuatnya case-sensitive:

CREATE INDEX ON yourtable (reverse(UPPER(yourcolumn)) text_pattern_ops);
SELECT * FROM yourtable WHERE reverse(UPPER(yourcolumn)) LIKE reverse(UPPER('%t.com'));

Indeks Trigram

Pilihan lainnya adalah indeks trigram. Anda harus menggunakan ini jika Anda membutuhkan infiks kueri ( LIKE 'something%something'atau LIKE '%something%'ketik kueri).

Pertama-tama aktifkan ekstensi indeks trigram:

CREATE EXTENSION pg_trgm;

(Seharusnya PostgreSQL keluar dari kotak tanpa instalasi tambahan.)

Kemudian buat indeks trigram pada kolom Anda:

CREATE INDEX ON yourtable USING GIST(yourcolumn gist_trgm_ops);

Kemudian cukup pilih:

SELECT * FROM yourtable WHERE yourcolumn LIKE '%t.com';

Sekali lagi, Anda bisa melempar dalam UPPERuntuk membuatnya case sensitif jika Anda suka:

CREATE INDEX ON yourtable USING GIST(UPPER(yourcolumn) gist_trgm_ops);
SELECT * FROM yourtable WHERE UPPER(yourcolumn) LIKE UPPER('%t.com');

Pertanyaan Anda ditulis

Indeks trigram sebenarnya bekerja menggunakan bentuk yang agak lebih umum dari apa yang Anda minta di bawah tenda. Ini memecah string menjadi beberapa bagian (trigram) dan membangun indeks berdasarkan itu. Indeks kemudian dapat digunakan untuk mencari kecocokan yang jauh lebih cepat daripada pemindaian berurutan, tetapi untuk infiks serta kueri dan awalan kueri. Selalu berusaha untuk menghindari menciptakan kembali apa yang telah dikembangkan orang lain ketika Anda bisa.

Kredit

Kedua solusi tersebut cukup banyak kata demi kata dari Memilih metode pencarian teks PostgreSQL . Saya sangat merekomendasikan untuk membaca analisis rinci opsi pencarian teks yang tersedia di PotsgreSQL.


Komentar bukan untuk diskusi panjang; percakapan ini telah dipindahkan ke obrolan .
Paul White 9

Saya tidak kembali ke ini sampai setelah Natal, jadi minta maaf atas keterlambatan dalam memilih jawaban. Indeks trigram akhirnya menjadi hal termudah dalam kasus saya dan paling membantu saya, meskipun itu adalah jawaban yang paling literal untuk pertanyaan yang diajukan, jadi saya tidak yakin kebijakan SE apa yang ada untuk memilih jawaban yang tepat. Bagaimanapun, terima kasih atas bantuan Anda.
Bo Jeanes

5

Saya pikir ini adalah favorit saya.


create table t (id int,str varchar(100));
insert into t (id,str) values (1,'a.b.c.d.e'),(2,'xxx.yyy.zzz');

BARIS

select      id
           ,array_to_string((string_to_array(str,'.'))[i:],'.')

from        t,unnest(string_to_array(str,'.')) with ordinality u(token,i)
;

+----+-----------------+
| id | array_to_string |
+----+-----------------+
|  1 | a.b.c.d.e       |
|  1 | b.c.d.e         |
|  1 | c.d.e           |
|  1 | d.e             |
|  1 | e               |
|  2 | xxx.yyy.zzz     |
|  2 | yyy.zzz         |
|  2 | zzz             |
+----+-----------------+

ARRAYS

select      id
           ,array_agg(array_to_string((string_to_array(str,'.'))[i:],'.'))

from        t,unnest(string_to_array(str,'.')) with ordinality u(token,i)

group by    id
;

+----+-------------------------------------------+
| id |                 array_agg                 |
+----+-------------------------------------------+
|  1 | {"a.b.c.d.e","b.c.d.e","c.d.e","d.e","e"} |
|  2 | {"xxx.yyy.zzz","yyy.zzz","zzz"}           |
+----+-------------------------------------------+

4
create table t (id int,str varchar(100));
insert into t (id,str) values (1,'a.b.c.d.e'),(2,'xxx.yyy.zzz');

BARIS

select  id
       ,regexp_replace(str,'^([^\.]+\.?){' || gs.i || '}','') as suffix

from    t,generate_series(0,cardinality(string_to_array(str,'.'))-1) gs(i)
;

ATAU

select  id
       ,substring(str from '(([^.]*?\.?){' || gs.i+1 || '})$') as suffix

from    t,generate_series(0,cardinality(string_to_array(str,'.'))-1) gs(i)
;

+----+-------------+
| id | suffix      |
+----+-------------+
| 1  | a.b.c.d.e   |
+----+-------------+
| 1  | b.c.d.e     |
+----+-------------+
| 1  | c.d.e       |
+----+-------------+
| 1  | d.e         |
+----+-------------+
| 1  | e           |
+----+-------------+
| 2  | xxx.yyy.zzz |
+----+-------------+
| 2  | yyy.zzz     |
+----+-------------+
| 2  | zzz         |
+----+-------------+

ARRAYS

select      id
           ,array_agg(regexp_replace(str,'^([^\.]+\.?){' || gs.i || '}','')) as suffixes

from        t,generate_series(0,cardinality(string_to_array(str,'.'))-1) gs(i)

group by    id
;

ATAU

select      id
           ,array_agg(substring(str from '(([^.]*?\.?){' || gs.i+1 || '})$')) as suffixes

from        t,generate_series(0,cardinality(string_to_array(str,'.'))-1) gs(i)

group by    id
;

+----+-------------------------------------------+
| id |                 suffixes                  |
+----+-------------------------------------------+
|  1 | {"a.b.c.d.e","b.c.d.e","c.d.e","d.e","e"} |
|  2 | {"xxx.yyy.zzz","yyy.zzz","zzz"}           |
+----+-------------------------------------------+

3

Pertanyaan ditanyakan

Meja tes:

CREATE TABLE tbl (id int, str text);
INSERT INTO tbl VALUES
  (1, 'a.b.c.d.e')
, (2, 'x1.yy2.zzz3')     -- different number & length of elements for testing
, (3, '')                -- empty string
, (4, NULL);             -- NULL

CTE rekursif dalam subquery LATERAL

SELECT *
FROM   tbl, LATERAL (
   WITH RECURSIVE cte AS (
      SELECT str
      UNION ALL
      SELECT right(str, strpos(str, '.') * -1)  -- trim leading name
      FROM   cte
      WHERE  str LIKE '%.%'  -- stop after last dot removed
      )
   SELECT ARRAY(TABLE cte) AS result
   ) r;

The CROSS JOIN LATERAL( , LATERALuntuk pendek) adalah aman, karena hasil agregat subquery selalu mengembalikan berturut-turut. Anda mendapatkan ...

  • ... sebuah array dengan elemen string kosong str = ''di dalam tabel dasar
  • ... sebuah array dengan elemen NULL untuk str IS NULLdi tabel dasar

Dibungkus dengan konstruktor array murah di subquery, jadi tidak ada agregasi di kueri luar.

Pameran fitur SQL, tetapi overhead rCTE dapat mencegah kinerja terbaik.

Brute force untuk sejumlah elemen sepele

Untuk kasus Anda dengan sejumlah kecil elemen , pendekatan sederhana tanpa subquery mungkin lebih cepat:

SELECT id, array_remove(ARRAY[substring(str, '(?:[^.]+\.){4}[^.]+$')
                            , substring(str, '(?:[^.]+\.){3}[^.]+$')
                            , substring(str, '(?:[^.]+\.){2}[^.]+$')
                            , substring(str,        '[^.]+\.[^.]+$')
                            , substring(str,               '[^.]+$')], NULL)
FROM   tbl;

Dengan asumsi maksimum 5 elemen seperti Anda berkomentar. Anda dapat dengan mudah memperluas untuk lebih banyak.

Jika domain tertentu memiliki lebih sedikit elemen, substring()ekspresi berlebih mengembalikan NULL dan dihapus oleh array_remove().

Sebenarnya, ekspresi dari atas ( right(str, strpos(str, '.')), bersarang beberapa kali mungkin lebih cepat (meskipun canggung untuk dibaca) karena fungsi ekspresi reguler lebih mahal.

Garpu dari permintaan @ Dudu

@ Permintaan pintar Dudu dapat ditingkatkan dengan generate_subscripts():

SELECT id, array_agg(array_to_string(arr[i:], '.')) AS result
FROM  (SELECT id, string_to_array(str,'.') AS arr FROM tbl) t
LEFT   JOIN LATERAL generate_subscripts(arr, 1) i ON true
GROUP  BY id;

Juga menggunakan LEFT JOIN LATERAL ... ON trueuntuk mempertahankan kemungkinan baris dengan nilai NULL.

Fungsi PL / pgSQL

Logika serupa dengan rCTE. Lebih sederhana dan lebih cepat dari yang Anda miliki:

CREATE OR REPLACE FUNCTION string_part_seq(input text, OUT result text[]) AS
$func$
BEGIN
   LOOP
      result := result || input;  -- text[] || text array concatenation
      input  := right(input, strpos(input, '.') * -1);
      EXIT WHEN input = '';
   END LOOP;
END
$func$  LANGUAGE plpgsql IMMUTABLE STRICT;

The OUTparameter dikembalikan pada akhir fungsi otomatis.

Tidak perlu menginisialisasi result, karena NULL::text[] || text 'a' = '{a}'::text[].
Ini hanya berfungsi dengan 'a'diketik dengan benar. NULL::text[] || 'a'(string literal) akan memunculkan kesalahan karena Postgres memilih array || arrayoperator.

strpos()kembali 0jika tidak ada titik yang ditemukan, jadi right()mengembalikan string kosong dan loop berakhir.

Ini mungkin yang tercepat dari semua solusi di sini.

Semuanya bekerja di Postgres 9.3+
(kecuali untuk notasi array pendek arr[3:]. Saya menambahkan batas atas pada biola untuk membuatnya bekerja di halaman 9.3:. arr[3:999])

SQL Fiddle.

Pendekatan berbeda untuk mengoptimalkan pencarian

Saya dengan @ jpmc26 (dan diri Anda sendiri): pendekatan yang sama sekali berbeda lebih disukai. Saya suka kombinasi jpmc26 reverse()dan a text_pattern_ops.

Indeks trigram akan lebih baik untuk pertandingan parsial atau fuzzy. Tetapi karena Anda hanya tertarik pada seluruh kata , Pencarian Teks Lengkap adalah pilihan lain. Saya mengharapkan ukuran indeks yang jauh lebih kecil dan dengan demikian kinerja yang lebih baik.

pg_trgm serta FTS mendukung kueri tidak sensitif kasus , btw.

Nama host seperti q.x.t.comatau t.com(kata-kata dengan titik sebaris) diidentifikasi sebagai tipe "host" dan diperlakukan sebagai satu kata. Tetapi ada juga pencocokan awalan di FTS (yang tampaknya kadang-kadang diabaikan). Manual:

Juga, *dapat dilampirkan ke leksem untuk menentukan pencocokan awalan:

Menggunakan ide cerdas @ jpmc26 dengan reverse(), kita dapat membuat ini berfungsi:

SELECT *
FROM   tbl
WHERE  to_tsvector('simple', reverse(str))
    @@ to_tsquery ('simple', reverse('c.d.e') || ':*');
-- or with reversed prefix:  reverse('*:c.d.e')

Yang didukung oleh indeks:

CREATE INDEX tbl_host_idx ON tbl USING GIN (to_tsvector('simple', reverse(str)));

Perhatikan 'simple'konfigurasi: Kami tidak ingin stemming atau tesaurus digunakan dengan 'english'konfigurasi default .

Atau (dengan variasi yang lebih besar dari kemungkinan pertanyaan) kita bisa menggunakan kemampuan pencarian frasa baru dari pencarian teks di Postgres 9.6. Catatan rilis:

Permintaan pencarian frase dapat ditentukan dalam input tsquery menggunakan operator baru <->dan . Yang pertama berarti bahwa leksem sebelum dan sesudahnya harus tampak berdekatan satu sama lain dalam urutan itu. Yang terakhir berarti mereka harus terpisah persis leksem.<N>N

Pertanyaan:

SELECT *
FROM   tbl
WHERE  to_tsvector     ('simple', replace(str, '.', ' '))
    @@ phraseto_tsquery('simple', 'c d e');

Ganti dot ( '.') dengan spasi ( ' ') untuk mencegah pengurai mengklasifikasikan 't.com' sebagai nama host dan alih-alih gunakan setiap kata sebagai leksem yang terpisah.

Dan indeks yang cocok untuk digunakan:

CREATE INDEX tbl_phrase_idx ON tbl USING GIN (to_tsvector('simple', replace(str, '.', ' ')));

2

Saya datang dengan sesuatu yang semi-bisa diterapkan, tapi saya suka umpan balik tentang pendekatan. Saya telah menulis sangat sedikit PL / pgSQL jadi merasa seperti semua yang saya lakukan cukup meretas dan saya terkejut ketika bekerja.

Meskipun demikian, di sinilah saya harus:

CREATE OR REPLACE FUNCTION string_part_sequences(input text, separator text)
RETURNS text[]
LANGUAGE plpgsql
AS $$
  DECLARE
    parts text[] := string_to_array(input, separator);
    result text[] := '{}';
    i int;
  BEGIN
    FOR i IN SELECT generate_subscripts(parts, 1) - 1
    LOOP
      SELECT array_append(result, (
          SELECT array_to_string(array_agg(x), separator)
          FROM (
            SELECT *
            FROM unnest(parts)
            OFFSET i
          ) p(x)
        )
      )
      INTO result;
    END LOOP;
    RETURN result;
  END;
$$
STRICT IMMUTABLE;

Ini berfungsi seperti ini:

# SELECT string_part_sequences('mymail.unisa.edu.au', '.');
┌──────────────────────────────────────────────┐
            string_part_sequences             
├──────────────────────────────────────────────┤
 {mymail.unisa.edu.au,unisa.edu.au,edu.au,au} 
└──────────────────────────────────────────────┘
(1 row)

Time: 1.168 ms

Saya menambahkan fungsi plpgsql sederhana untuk jawaban saya.
Erwin Brandstetter

1

Saya menggunakan fungsi jendela:

with t1 as (select regexp_split_to_table('ab.ac.xy.yx.md','\.') as str),
     t2 as (select string_agg(str,'.') over ( rows between current row and unbounded following) as str from t1 ),
     t3 as (select array_agg(str) from t2)
     select * from t3 ;

Hasil:

postgres=# with t1 as (select regexp_split_to_table('ab.ac.xy.yx.md','\.') as str),
postgres-#      t2 as (select string_agg(str,'.') over ( rows between current row and unbounded following) as str from t1 ),
postgres-#      t3 as (select array_agg(str) from t2)
postgres-#      select * from t3 ;
                   array_agg
------------------------------------------------
 {ab.ac.xy.yx.md,ac.xy.yx.md,xy.yx.md,yx.md,md}
(1 row)

Time: 0.422 ms
postgres=# with t1 as (select regexp_split_to_table('mymail.unisa.edu.au','\.') as str),
postgres-#      t2 as (select string_agg(str,'.') over ( rows between current row and unbounded following) as str from t1 ),
postgres-#      t3 as (select array_agg(str) from t2)
postgres-#      select * from t3 ;
                  array_agg
----------------------------------------------
 {mymail.unisa.edu.au,unisa.edu.au,edu.au,au}
(1 row)

Time: 0.328 ms

1

Varian dari solusi oleh @Dudu Markovitz, yang juga berfungsi dengan versi PostgreSQL yang belum (belum) mengenali [i:]:

create table t (id int,str varchar(100));
insert into t (id,str) values (1,'a.b.c.d.e'),(2,'xxx.yyy.zzz');

SELECT    
    id, array_to_string(the_array[i:upper_bound], '.')
FROM     
    (
    SELECT
        id, 
        string_to_array(str, '.') the_array, 
        array_upper(string_to_array(str, '.'), 1) AS upper_bound
    FROM
        t
    ) AS s0, 
    generate_series(1, upper_bound) AS s1(i)
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.