PostgreSQL
 sql >> Database >  >> RDS >> PostgreSQL

Come indicizzare una colonna di array di stringhe per la query pg_trgm `'term' % ANY (array_column)`?

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)