PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

Wie indiziert man eine String-Array-Spalte für die Abfrage pg_trgm `'term' % ANY (array_column)`?

Warum das nicht funktioniert

Der Indextyp (d. h. die Operatorklasse) gin_trgm_ops basiert auf % -Operator, der mit zwei text arbeitet Argumente:

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

Sie können gin_trgm_ops nicht verwenden für Arrays. Ein für eine Array-Spalte definierter Index funktioniert niemals mit any(array[...]) da einzelne Elemente von Arrays nicht indiziert werden. Das Indizieren eines Arrays würde einen anderen Indextyp erfordern, nämlich gin array index.

Glücklicherweise ist der Index gin_trgm_ops ist so clever gestaltet, dass es mit Operatoren like arbeitet und ilike , die als alternative Lösung verwendet werden kann (Beispiel unten beschrieben).

Testtabelle

hat zwei Spalten (id serial primary key, names text[]) und enthält 100000 lateinische Sätze, die in Array-Elemente aufgeteilt sind.

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}

Suche nach dem Wortfragment praesent ergibt 7051 Zeilen 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

Materialisierte Ansicht

Eine Lösung besteht darin, das Modell zu normalisieren, indem eine neue Tabelle mit einem einzigen Namen in einer Zeile erstellt wird. Eine solche Umstrukturierung kann aufgrund vorhandener Abfragen, Ansichten, Funktionen oder anderer Abhängigkeiten schwierig zu implementieren und manchmal unmöglich sein. Ein ähnlicher Effekt kann mit einer materialisierten Ansicht erzielt werden, ohne die Tabellenstruktur zu ändern.

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 ist nicht notwendig, kann aber nützlich sein, wenn die Namen in der gleichen Reihenfolge wie in der Haupttabelle aggregiert werden. Abfrage von test_names liefert die gleichen Ergebnisse wie die Haupttabelle in der gleichen Zeit.

Nach dem Erstellen des Indexes verringert sich die Ausführungszeit wiederholt:

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

Die Lösung hat einige Nachteile. Da die Ansicht materialisiert ist, werden die Daten zweimal in der Datenbank gespeichert. Sie müssen daran denken, die Ansicht nach Änderungen an der Haupttabelle zu aktualisieren. Und Abfragen können komplizierter sein, da die Ansicht mit der Haupttabelle verknüpft werden muss.

Mit ilike

Wir können ilike verwenden auf den als Text dargestellten Arrays. Wir brauchen eine unveränderliche Funktion, um den Index für das Array als Ganzes zu erstellen:

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

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

und verwenden Sie die Funktion in Abfragen:

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 vs. 2400 ms, ganz nettes Ergebnis ohne die Notwendigkeit, zusätzliche Relationen zu erstellen.

Diese Lösung erscheint einfacher und erfordert weniger Arbeit, vorausgesetzt jedoch, dass ilike , das weniger präzise ist als das trgm % Operator, ist ausreichend.

Warum sollten wir ilike verwenden? statt % für ganze Arrays als Text? Die Ähnlichkeit hängt stark von der Länge der Texte ab. Es ist sehr schwierig, in langen Texten unterschiedlicher Länge eine geeignete Grenze für die Suche nach einem Wort zu wählen. mit limit = 0.3 wir haben die Ergebnisse:

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)