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)