Varför detta inte fungerar
Indextypen (d.v.s. operatorklassen) gin_trgm_ops
baseras på %
operator, som fungerar på två text
argument:
CREATE OPERATOR trgm.%(
PROCEDURE = trgm.similarity_op,
LEFTARG = text,
RIGHTARG = text,
COMMUTATOR = %,
RESTRICT = contsel,
JOIN = contjoinsel);
Du kan inte använda gin_trgm_ops
för arrays.Ett index som definieras för en arraykolumn kommer aldrig att fungera med any(array[...])
eftersom enskilda element i arrayer inte indexeras. Indexering av en array skulle kräva en annan typ av index, nämligen gin array index.
Lyckligtvis är indexet gin_trgm_ops
har utformats så smart att den fungerar med operatörer like
och ilike
, som kan användas som en alternativ lösning (exempel beskrivs nedan).
Testtabell
har två kolumner (id serial primary key, names text[])
och innehåller 100 000 latinska meningar uppdelade i arrayelement.
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}
Söker efter ordet fragment praesent
ger 7051 rader på 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
Materialiserad vy
En lösning är att normalisera modellen, vilket innebär att man skapar en ny tabell med ett enda namn på en rad. Sådan omstrukturering kan vara svår att implementera och ibland omöjlig på grund av befintliga frågor, vyer, funktioner eller andra beroenden. En liknande effekt kan uppnås utan att ändra tabellstrukturen, med hjälp av en materialiserad vy.
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
är inte nödvändigt, men kan vara användbart när man aggregerar namnen i samma ordning som i huvudtabellen. Frågar test_names
ger samma resultat som huvudtabellen på samma tid.
Efter att ha skapat indexet minskar exekveringstiden upprepade gånger:
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
Lösningen har några nackdelar. Eftersom vyn materialiseras lagras data två gånger i databasen. Du måste komma ihåg att uppdatera vyn efter ändringar i huvudtabellen. Och frågor kan vara mer komplicerade på grund av behovet av att ansluta vyn till huvudtabellen.
Använder ilike
Vi kan använda ilike
på arrayerna representerade som text. Vi behöver en oföränderlig funktion för att skapa indexet på arrayen som helhet:
create function text(text[])
returns text language sql immutable as
$$ select $1::text $$
create index on test using gin (text(names) gin_trgm_ops);
och använd funktionen i frågor:
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 kontra 2400 ms, ganska bra resultat utan att behöva skapa ytterligare relationer.
Denna lösning verkar enklare och kräver mindre arbete, dock förutsatt att ilike
, vilket är mindre exakt verktyg än trgm %
operatör, är tillräcklig.
Varför ska vi använda ilike
istället för %
för hela arrayer som text?Likheten beror till stor del på längden på texterna.Det är mycket svårt att välja en lämplig gräns för sökningen ett ord i långa texter av olika längd.T.ex. med limit = 0.3
vi har 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)