Perché non funziona
Il tipo di indice (ovvero la classe dell'operatore) gin_trgm_ops
si basa su %
operatore, che funziona su due text
argomenti:
CREATE OPERATOR trgm.%(
PROCEDURE = trgm.similarity_op,
LEFTARG = text,
RIGHTARG = text,
COMMUTATOR = %,
RESTRICT = contsel,
JOIN = contjoinsel);
Non puoi usare gin_trgm_ops
for arrays.Un indice definito per una colonna di array non funzionerà mai con any(array[...])
perché i singoli elementi degli array non sono indicizzati. L'indicizzazione di un array richiederebbe un diverso tipo di indice, ovvero l'indice dell'array gin.
Fortunatamente, l'indice gin_trgm_ops
è stato progettato in modo così intelligente da funzionare con operatori like
e ilike
, che può essere utilizzata come soluzione alternativa (esempio descritto di seguito).
Tabella di prova
ha due colonne (id serial primary key, names text[])
e contiene 100000 frasi latine suddivise in elementi dell'array.
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}
Ricerca del frammento di parola praesent
fornisce 7051 righe 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
Vista materializzata
Una soluzione è normalizzare il modello, comportando la creazione di una nuova tabella con un unico nome in una riga. Tale ristrutturazione può essere difficile da implementare e talvolta impossibile a causa di query, viste, funzioni o altre dipendenze esistenti. Un effetto simile può essere ottenuto senza modificare la struttura del tavolo, utilizzando una vista materializzata.
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
non è necessario, ma può essere utile quando si aggregano i nomi nello stesso ordine della tabella principale. Interrogazione di test_names
fornisce gli stessi risultati della tabella principale nello stesso tempo.
Dopo aver creato l'indice il tempo di esecuzione diminuisce ripetutamente:
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
La soluzione presenta alcuni inconvenienti. Poiché la vista è materializzata, i dati vengono archiviati due volte nel database. Devi ricordarti di aggiornare la vista dopo le modifiche alla tabella principale. E le query potrebbero essere più complicate a causa della necessità di unire la vista alla tabella principale.
Utilizzo di ilike
Possiamo usare ilike
sugli array rappresentati come testo. Abbiamo bisogno di una funzione immutabile per creare l'indice sull'array nel suo insieme:
create function text(text[])
returns text language sql immutable as
$$ select $1::text $$
create index on test using gin (text(names) gin_trgm_ops);
e usa la funzione nelle query:
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 contro 2400 ms, risultato abbastanza bello senza la necessità di creare relazioni aggiuntive.
Questa soluzione sembra più semplice e richiede meno lavoro, a patto però che ilike
, che è uno strumento meno preciso del trgm %
operatore, è sufficiente.
Perché dovremmo usare ilike
anziché %
per interi array come testo?La somiglianza dipende in gran parte dalla lunghezza dei testi.È molto difficile scegliere un limite appropriato per la ricerca di una parola in testi lunghi di varia lunghezza.Es. con limit = 0.3
abbiamo i risultati:
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)