sql >> Database >  >> RDS >> PostgreSQL

Hoe indexeer ik een string-arraykolom voor pg_trgm `'term' % ANY (array_column)`-query?

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)


  1. Hoe onderbreek ik een RPostgresql-query in R

  2. MySQL-zelfstudie - SSL configureren en beheren op uw MySQL-server

  3. MySQL-echo aantal records met dezelfde sessie-ID

  4. mysql-query om alle rijen van de huidige maand te selecteren?