Tại sao điều này không hoạt động
Loại chỉ mục (tức là lớp toán tử) gin_trgm_ops
dựa trên %
toán tử, hoạt động trên hai text
đối số:
CREATE OPERATOR trgm.%(
PROCEDURE = trgm.similarity_op,
LEFTARG = text,
RIGHTARG = text,
COMMUTATOR = %,
RESTRICT = contsel,
JOIN = contjoinsel);
Bạn không thể sử dụng gin_trgm_ops
cho mảng. Một chỉ mục được xác định cho một cột mảng sẽ không bao giờ hoạt động với any(array[...])
bởi vì các phần tử riêng lẻ của mảng không được lập chỉ mục. Việc lập chỉ mục một mảng sẽ yêu cầu một loại chỉ mục khác, cụ thể là chỉ số mảng gin.
May mắn thay, chỉ mục gin_trgm_ops
đã được thiết kế thông minh đến mức nó đang hoạt động với các toán tử like
và ilike
, có thể được sử dụng như một giải pháp thay thế (ví dụ được mô tả bên dưới).
Bảng kiểm tra
có hai cột (id serial primary key, names text[])
và chứa 100000 câu latin được chia thành các phần tử mảng.
select count(*), sum(cardinality(names))::int words from test;
count | words
--------+---------
100000 | 1799389
select * from test limit 1;
id | names
----+---------------------------------------------------------------------------------------------------------------
1 | {fugiat,odio,aut,quis,dolorem,exercitationem,fugiat,voluptates,facere,error,debitis,ut,nam,et,voluptatem,eum}
Tìm kiếm đoạn từ praesent
cho 7051 hàng trong 2400 mili giây:
explain analyse
select count(*)
from test
where 'praesent' % any(names);
QUERY PLAN
---------------------------------------------------------------------------------------------------------------
Aggregate (cost=5479.49..5479.50 rows=1 width=0) (actual time=2400.866..2400.866 rows=1 loops=1)
-> Seq Scan on test (cost=0.00..5477.00 rows=996 width=0) (actual time=1.464..2400.271 rows=7051 loops=1)
Filter: ('praesent'::text % ANY (names))
Rows Removed by Filter: 92949
Planning time: 1.038 ms
Execution time: 2400.916 ms
Chế độ xem cụ thể hóa
Một giải pháp là chuẩn hóa mô hình, liên quan đến việc tạo một bảng mới với một tên duy nhất trong một hàng. Việc tái cấu trúc như vậy có thể khó thực hiện và đôi khi không thể thực hiện được do các truy vấn, quan điểm, chức năng hiện có hoặc các yếu tố phụ thuộc khác. Có thể đạt được hiệu ứng tương tự mà không cần thay đổi cấu trúc bảng bằng cách sử dụng chế độ xem cụ thể hóa.
create materialized view test_names as
select id, name, name_id
from test
cross join unnest(names) with ordinality u(name, name_id)
with data;
With ordinality
là không cần thiết, nhưng có thể hữu ích khi tổng hợp các tên theo thứ tự như trong bảng chính. Truy vấn test_names
cho kết quả giống như bảng chính trong cùng một thời điểm.
Sau khi tạo chỉ mục, thời gian thực hiện chỉ mục giảm đi nhiều lần:
create index on test_names using gin (name gin_trgm_ops);
explain analyse
select count(distinct id)
from test_names
where 'praesent' % name
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------
Aggregate (cost=4888.89..4888.90 rows=1 width=4) (actual time=56.045..56.045 rows=1 loops=1)
-> Bitmap Heap Scan on test_names (cost=141.95..4884.39 rows=1799 width=4) (actual time=10.513..54.987 rows=7230 loops=1)
Recheck Cond: ('praesent'::text % name)
Rows Removed by Index Recheck: 7219
Heap Blocks: exact=8122
-> Bitmap Index Scan on test_names_name_idx (cost=0.00..141.50 rows=1799 width=0) (actual time=9.512..9.512 rows=14449 loops=1)
Index Cond: ('praesent'::text % name)
Planning time: 2.990 ms
Execution time: 56.521 ms
Giải pháp có một số hạn chế. Bởi vì chế độ xem được thực thể hóa, dữ liệu được lưu trữ hai lần trong cơ sở dữ liệu. Bạn phải nhớ làm mới chế độ xem sau khi thay đổi bảng chính. Và các truy vấn có thể phức tạp hơn vì cần phải nối chế độ xem vào bảng chính.
Sử dụng ilike
Chúng ta có thể sử dụng ilike
trên các mảng được biểu diễn dưới dạng văn bản. Chúng ta cần một hàm không thay đổi để tạo chỉ mục trên toàn bộ mảng:
create function text(text[])
returns text language sql immutable as
$$ select $1::text $$
create index on test using gin (text(names) gin_trgm_ops);
và sử dụng hàm trong các truy vấn:
explain analyse
select count(*)
from test
where text(names) ilike '%praesent%'
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------
Aggregate (cost=117.06..117.07 rows=1 width=0) (actual time=60.585..60.585 rows=1 loops=1)
-> Bitmap Heap Scan on test (cost=76.08..117.03 rows=10 width=0) (actual time=2.560..60.161 rows=7051 loops=1)
Recheck Cond: (text(names) ~~* '%praesent%'::text)
Heap Blocks: exact=2899
-> Bitmap Index Scan on test_text_idx (cost=0.00..76.08 rows=10 width=0) (actual time=2.160..2.160 rows=7051 loops=1)
Index Cond: (text(names) ~~* '%praesent%'::text)
Planning time: 3.301 ms
Execution time: 60.876 ms
60 so với 2400 ms, kết quả khá tốt mà không cần tạo quan hệ bổ sung.
Giải pháp này có vẻ đơn giản hơn và yêu cầu ít công việc hơn, tuy nhiên, miễn là ilike
, là công cụ kém chính xác hơn trgm %
toán tử, là đủ.
Tại sao chúng ta nên sử dụng ilike
thay vì %
đối với toàn bộ mảng dưới dạng văn bản? Sự giống nhau phụ thuộc phần lớn vào độ dài của văn bản. Rất khó để chọn một giới hạn thích hợp cho việc tìm kiếm một từ trong các văn bản dài có độ dài khác nhau. với limit = 0.3
chúng tôi có kết quả:
with data(txt) as (
values
('praesentium,distinctio,modi,nulla,commodi,tempore'),
('praesentium,distinctio,modi,nulla,commodi'),
('praesentium,distinctio,modi,nulla'),
('praesentium,distinctio,modi'),
('praesentium,distinctio'),
('praesentium')
)
select length(txt), similarity('praesent', txt), 'praesent' % txt "matched?"
from data;
length | similarity | matched?
--------+------------+----------
49 | 0.166667 | f <--!
41 | 0.2 | f <--!
33 | 0.228571 | f <--!
27 | 0.275862 | f <--!
22 | 0.333333 | t
11 | 0.615385 | t
(6 rows)