Waarom dit niet werkt
Het indextype (d.w.z. operatorklasse) gin_trgm_ops
is gebaseerd op %
operator, die werkt op twee text
argumenten:
CREATE OPERATOR trgm.%(
PROCEDURE = trgm.similarity_op,
LEFTARG = text,
RIGHTARG = text,
COMMUTATOR = %,
RESTRICT = contsel,
JOIN = contjoinsel);
U kunt gin_trgm_ops
. niet gebruiken for arrays.Een index gedefinieerd voor een arraykolom zal nooit werken met any(array[...])
omdat individuele elementen van arrays niet worden geïndexeerd. Voor het indexeren van een array is een ander type index nodig, namelijk gin array-index.
Gelukkig is de index gin_trgm_ops
is zo slim ontworpen dat het werkt met operators like
en ilike
, die als alternatieve oplossing kan worden gebruikt (voorbeeld hieronder beschreven).
Testtabel
heeft twee kolommen (id serial primary key, names text[])
en bevat 100000 Latijnse zinnen opgesplitst in array-elementen.
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}
Zoeken naar het woordfragment praesent
geeft 7051 rijen in 2400 ms:
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
Gematerialiseerde weergave
Een oplossing is om het model te normaliseren door een nieuwe tabel te maken met één naam in één rij. Een dergelijke herstructurering kan moeilijk te implementeren zijn en soms onmogelijk vanwege bestaande vragen, weergaven, functies of andere afhankelijkheden. Een soortgelijk effect kan worden bereikt zonder de tafelstructuur te veranderen, met behulp van een gematerialiseerde weergave.
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
is niet noodzakelijk, maar kan handig zijn bij het aggregeren van de namen in dezelfde volgorde als in de hoofdtabel. Zoeken naar test_names
geeft dezelfde resultaten als de hoofdtabel in dezelfde tijd.
Na het aanmaken van de index neemt de uitvoeringstijd herhaaldelijk af:
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
De oplossing heeft een aantal nadelen. Doordat de view gematerialiseerd is, worden de gegevens twee keer opgeslagen in de database. U moet eraan denken om de weergave te vernieuwen na wijzigingen in de hoofdtabel. En query's kunnen ingewikkelder zijn vanwege de noodzaak om de weergave aan de hoofdtabel te koppelen.
ilike
gebruiken
We kunnen ilike
. gebruiken op de arrays weergegeven als tekst. We hebben een onveranderlijke functie nodig om de index op de array als geheel te maken:
create function text(text[])
returns text language sql immutable as
$$ select $1::text $$
create index on test using gin (text(names) gin_trgm_ops);
en gebruik de functie in zoekopdrachten:
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 versus 2400 ms, best mooi resultaat zonder de noodzaak om extra relaties te creëren.
Deze oplossing lijkt eenvoudiger en vereist minder werk, op voorwaarde echter dat ilike
, wat een minder nauwkeurig hulpmiddel is dan de trgm %
operator, is voldoende.
Waarom zouden we ilike
gebruiken? in plaats van %
voor hele arrays als tekst? De overeenkomst hangt grotendeels af van de lengte van de teksten. Het is erg moeilijk om een geschikte limiet te kiezen voor het zoeken naar een woord in lange teksten van verschillende lengte. met limit = 0.3
we hebben de resultaten:
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)