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

Hvordan indekseres en string array kolonne for pg_trgm `'term' % ENHVER (array_column)` forespørgsel?

Hvorfor virker dette ikke

Indekstypen (dvs. operatorklassen) gin_trgm_ops er baseret på % operator, som fungerer på to text argumenter:

CREATE OPERATOR trgm.%(
  PROCEDURE = trgm.similarity_op,
  LEFTARG = text,
  RIGHTARG = text,
  COMMUTATOR = %,
  RESTRICT = contsel,
  JOIN = contjoinsel);

Du kan ikke bruge gin_trgm_ops for arrays. Et indeks defineret for en array-kolonne vil aldrig fungere med any(array[...]) fordi individuelle elementer i arrays ikke er indekseret. Indeksering af et array ville kræve en anden type indeks, nemlig gin array index.

Heldigvis er indekset gin_trgm_ops er blevet så smart designet, at det fungerer med operatører like og ilike , som kan bruges som en alternativ løsning (eksempel beskrevet nedenfor).

Testtabel

har to kolonner (id serial primary key, names text[]) og indeholder 100.000 latinske sætninger opdelt i array-elementer.

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øger efter ordet fragment praesent giver 7051 rækker 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

Materialiseret visning

En løsning er at normalisere modellen, hvilket involverer oprettelsen af ​​en ny tabel med et enkelt navn i én række. En sådan omstrukturering kan være vanskelig at implementere og nogle gange umulig på grund af eksisterende forespørgsler, synspunkter, funktioner eller andre afhængigheder. En lignende effekt kan opnås uden at ændre bordstrukturen ved at bruge en materialiseret visning.

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 er ikke nødvendigt, men kan være nyttigt, når man samler navnene i samme rækkefølge som i hovedtabellen. Forespørger på test_names giver samme resultater som hovedtabellen på samme tid.

Efter oprettelse af indekset falder udførelsestiden gentagne gange:

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 nogle få ulemper. Fordi visningen er materialiseret, gemmes data to gange i databasen. Du skal huske at opdatere visningen efter ændringer i hovedtabellen. Og forespørgsler kan være mere komplicerede på grund af behovet for at slutte visningen til hovedtabellen.

Brug af ilike

Vi kan bruge ilike på arrays repræsenteret som tekst. Vi har brug for en uforanderlig funktion for at skabe indekset på arrayet som helhed:

create function text(text[])
returns text language sql immutable as
$$ select $1::text $$

create index on test using gin (text(names) gin_trgm_ops);

og brug funktionen i forespørgsler:

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, ganske flot resultat uden behov for at skabe yderligere relationer.

Denne løsning virker enklere og kræver mindre arbejde, dog forudsat at ilike , som er mindre præcist værktøj end trgm % operatør, er tilstrækkelig.

Hvorfor skal vi bruge ilike i stedet for % for hele arrays som tekst?Ligheden afhænger i høj grad af teksternes længde.Det er meget vanskeligt at vælge en passende grænse for søgningen et ord i lange tekster af forskellig længde.F.eks. med limit = 0.3 vi har resultaterne:

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)


  1. Beverly Hills 90210 og ZIP+4:Håndtering af adresser i datamodeller

  2. Tjek, om den aktuelle dato er mellem to datoer Oracle SQL

  3. Fjern webadresse fra tekststreng

  4. VÆLG * FRA NYT TABEL tilsvarende i Postgres