Làm thế nào tôi có thể tạo ra tất cả các chuỗi con sau một dấu phân cách?


8

Đưa ra một chuỗi có thể chứa nhiều phiên bản của một dấu phân cách, tôi muốn tạo tất cả các chuỗi con bắt đầu sau ký tự đó.

Ví dụ: được cho một chuỗi như 'a.b.c.d.e'(hoặc mảng {a,b,c,d,e}, tôi cho là), tôi muốn tạo một mảng như:

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

Việc sử dụng dự định là một trình kích hoạt để điền vào một cột để truy vấn các phần tên miền dễ dàng hơn (tức là tìm tất cả q.x.t.comcho truy vấn t.com) bất cứ khi nào một cột khác được ghi vào.

Có vẻ như là một cách khó xử để giải quyết vấn đề này (và rất có thể là như vậy), nhưng bây giờ tôi tò mò làm thế nào một chức năng như thế này có thể được viết bằng (Postgres ') SQL.

Đây là các tên miền email, vì vậy thật khó để nói số lượng phần tử tối đa có thể là bao nhiêu, nhưng chắc chắn phần lớn sẽ là <5.


@ErwinBrandstetter có. Xin lỗi vì sự chậm trễ (ngày lễ, vv). Tôi chọn câu trả lời chỉ số trigram vì nó thực sự giải quyết vấn đề thực sự của tôi tốt nhất. Tuy nhiên, tôi rất nhạy cảm với thực tế rằng câu hỏi của tôi cụ thể là làm thế nào tôi có thể tách một chuỗi theo cách này (vì tò mò) vì vậy tôi không chắc liệu tôi đã sử dụng số liệu tốt nhất để chọn câu trả lời được chấp nhận hay chưa.
Bo Jeanes

Câu trả lời tốt nhất nên là câu trả lời tốt nhất cho câu hỏi đã cho. Cuối cùng, đó là sự lựa chọn của bạn. Và người được chọn dường như là một ứng cử viên hợp lệ với tôi.
Erwin Brandstetter

Câu trả lời:


3

Tôi không nghĩ rằng bạn cần một cột riêng ở đây; đây là một vấn đề XY. Bạn chỉ đang cố gắng thực hiện tìm kiếm hậu tố. Có hai cách chính để tối ưu hóa điều đó.

Biến truy vấn hậu tố thành truy vấn tiền tố

Bạn cơ bản làm điều này bằng cách đảo ngược mọi thứ.

Đầu tiên tạo một chỉ mục ở mặt sau của cột của bạn:

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

Sau đó truy vấn bằng cách sử dụng tương tự:

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

Bạn có thể thực hiện một UPPERcuộc gọi nếu bạn muốn làm cho nó không nhạy cảm:

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

Chỉ số trigram

Tùy chọn khác là chỉ số trigram. Bạn chắc chắn nên sử dụng điều này nếu bạn cần truy vấn infix ( LIKE 'something%something'hoặc LIKE '%something%'loại truy vấn).

Đầu tiên kích hoạt phần mở rộng chỉ mục trigram:

CREATE EXTENSION pg_trgm;

(Điều này sẽ đi kèm với PostgreSQL ra khỏi hộp mà không cần cài đặt thêm.)

Sau đó tạo chỉ mục bát quái trên cột của bạn:

CREATE INDEX ON yourtable USING GIST(yourcolumn gist_trgm_ops);

Sau đó chỉ cần chọn:

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

Một lần nữa, bạn có thể ném vào UPPERđể làm cho nó không nhạy cảm nếu bạn thích:

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

Câu hỏi của bạn như được viết

Các chỉ mục trigram thực sự hoạt động bằng cách sử dụng một hình thức chung hơn một chút về những gì bạn yêu cầu dưới mui xe. Nó phá vỡ chuỗi thành từng mảnh (bát quái) và xây dựng một chỉ mục dựa trên những thứ đó. Chỉ mục sau đó có thể được sử dụng để tìm kiếm các kết quả khớp nhanh hơn nhiều so với quét tuần tự, nhưng đối với truy vấn hậu tố cũng như hậu tố và tiền tố. Luôn cố gắng tránh phát minh lại những gì người khác đã phát triển khi bạn có thể.

Tín dụng

Hai giải pháp có nhiều nguyên văn từ việc chọn phương pháp tìm kiếm văn bản PostgreSQL . Tôi đặc biệt khuyên bạn nên đọc để phân tích chi tiết các tùy chọn tìm kiếm văn bản có sẵn trong PotsgreSQL.


Bình luận không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện .
Paul White 9

Tôi đã không trở lại vấn đề này cho đến sau Giáng sinh, vì vậy xin lỗi vì sự chậm trễ trong việc chọn câu trả lời. Các chỉ số trigram cuối cùng là điều dễ nhất trong trường hợp của tôi và giúp tôi nhiều nhất, mặc dù đó là câu trả lời ít nhất cho câu hỏi được hỏi, vì vậy tôi không chắc chính sách của SE là gì để chọn câu trả lời phù hợp. Dù bằng cách nào, cảm ơn tất cả sự giúp đỡ của bạn.
Bo Jeanes

5

Tôi nghĩ rằng đây là yêu thích của tô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');

ROWS

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');

ROWS

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

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

HOẶC LÀ

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
;

HOẶC LÀ

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

Câu hỏi

Bảng kiểm tra:

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 đệ quy trong một truy vấn con LATITH

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;

Các CROSS JOIN LATERAL( , LATERALcho ngắn) là an toàn, bởi vì kết quả tổng hợp của subquery luôn trả về một hàng. Bạn lấy ...

  • ... một mảng có phần tử chuỗi rỗng str = ''trong bảng cơ sở
  • ... một mảng có phần tử NULL str IS NULLtrong bảng cơ sở

Được kết hợp với một hàm tạo mảng giá rẻ trong truy vấn con, do đó không có tổng hợp trong truy vấn bên ngoài.

Một biểu hiện của các tính năng SQL, nhưng chi phí rCTE có thể ngăn chặn hiệu suất cao nhất.

Lực lượng vũ phu cho số lượng yếu tố tầm thường

Đối với trường hợp của bạn có số lượng phần tử nhỏ , một cách tiếp cận đơn giản không có truy vấn con có thể nhanh hơn:

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

Giả sử tối đa 5 yếu tố như bạn đã nhận xét. Bạn có thể dễ dàng mở rộng để biết thêm.

Nếu một miền nhất định có ít thành phần hơn, các substring()biểu thức thừa sẽ trả về NULL và bị xóa bởi array_remove().

Trên thực tế, biểu thức từ trên ( right(str, strpos(str, '.')), được lồng nhiều lần có thể nhanh hơn (mặc dù khó đọc) vì các hàm biểu thức thông thường đắt hơn.

Một ngã ba truy vấn của @ Dudu

Truy vấn thông minh của @ Dudu có thể được cải thiện với 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;

Cũng sử dụng LEFT JOIN LATERAL ... ON trueđể bảo tồn các hàng có thể có giá trị NULL.

Hàm PL / pgSQL

Logic tương tự như rCTE. Thực chất đơn giản và nhanh hơn những gì bạn có:

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;

Các OUTtham số được trả vào cuối của hàm tự động.

Không cần phải khởi tạo result, bởi vì NULL::text[] || text 'a' = '{a}'::text[].
Điều này chỉ hoạt động với 'a'được gõ đúng. NULL::text[] || 'a'(chuỗi ký tự) sẽ phát sinh lỗi vì Postgres chọn array || arraytoán tử.

strpos()trả về 0nếu không tìm thấy dấu chấm nào, vì vậy right()trả về một chuỗi rỗng và vòng lặp kết thúc.

Đây có lẽ là nhanh nhất trong tất cả các giải pháp ở đây.

Tất cả chúng đều hoạt động trong Postgres 9.3+
(ngoại trừ ký hiệu lát cắt ngắn arr[3:]. Tôi đã thêm một giới hạn trên trong fiddle để làm cho nó hoạt động trong pg 9.3 : arr[3:999].)

Câu đố SQL.

Cách tiếp cận khác nhau để tối ưu hóa tìm kiếm

Tôi với @ jpmc26 (và chính bạn): một cách tiếp cận hoàn toàn khác sẽ được ưa thích hơn. Tôi thích sự kết hợp của jpmc26 reverse()và a text_pattern_ops.

Một chỉ số trigram sẽ vượt trội hơn cho các trận đấu một phần hoặc mờ. Nhưng vì bạn chỉ quan tâm đến toàn bộ từ , Tìm kiếm toàn văn bản là một lựa chọn khác. Tôi mong đợi một kích thước chỉ mục nhỏ hơn đáng kể và do đó hiệu suất tốt hơn.

pg_trgm cũng như các truy vấn không nhạy cảm trong trường hợp hỗ trợ FTS , btw.

Tên máy chủ như q.x.t.comhoặc t.com(từ có dấu chấm nội tuyến) được xác định là loại "máy chủ" và được coi là một từ. Nhưng cũng có kết hợp tiền tố trong FTS (đôi khi dường như bị bỏ qua). Hướng dẫn sử dụng:

Ngoài ra, *có thể được đính kèm với một từ vựng để chỉ định khớp tiền tố:

Sử dụng ý tưởng thông minh của @ jpmc26 với reverse(), chúng tôi có thể thực hiện công việc này:

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

Được hỗ trợ bởi một chỉ mục:

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

Lưu ý 'simple'cấu hình: Chúng tôi không muốn xuất phát hoặc từ điển đồng nghĩa với 'english'cấu hình mặc định .

Ngoài ra (với nhiều truy vấn có thể lớn hơn), chúng tôi có thể sử dụng khả năng tìm kiếm cụm từ mới của tìm kiếm văn bản trong Postgres 9.6. Ghi chú phát hành:

Một truy vấn tìm kiếm cụm từ có thể được chỉ định trong đầu vào tsquery bằng cách sử dụng các toán tử mới <->và . Cái trước có nghĩa là các từ vựng trước và sau nó phải xuất hiện liền kề nhau theo thứ tự đó. Điều thứ hai có nghĩa là chúng phải cách nhau chính xác .<N>N

Truy vấn:

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

Thay thế dấu chấm ( '.') bằng dấu cách ( ' ') để giữ trình phân tích cú pháp phân loại 't.com' làm tên máy chủ và thay vào đó sử dụng mỗi từ làm từ vựng riêng biệt.

Và một chỉ số phù hợp để đi với nó:

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

2

Tôi đã đưa ra một cái gì đó bán được, nhưng tôi thích phản hồi về cách tiếp cận. Tôi đã viết rất ít PL / pgSQL vì vậy cảm giác như mọi thứ tôi làm đều khá hack và tôi ngạc nhiên khi nó hoạt động.

Tuy nhiên, đây là nơi tôi đã đến:

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;

Điều này hoạt động như vậy:

# 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

Tôi đã thêm một hàm plpgsql đơn giản hơn vào câu trả lời của mình.
Erwin Brandstetter

1

Tôi sử dụng chức năng cửa sổ:

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 ;

Kết quả:

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

Một biến thể của giải pháp của @Dudu Markovitz, cũng hoạt động với các phiên bản PostgreSQL không (chưa) nhận ra [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)
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.