PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Comment indexer une colonne de tableau de chaînes pour la requête pg_trgm `'term' % ANY (array_column)` ?

Pourquoi cela ne fonctionne pas

Le type d'index (c'est-à-dire la classe d'opérateur) gin_trgm_ops est basé sur % opérateur, qui fonctionne sur deux text arguments :

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

Vous ne pouvez pas utiliser gin_trgm_ops pour les tableaux. Un index défini pour une colonne de tableau ne fonctionnera jamais avec any(array[...]) car les éléments individuels des tableaux ne sont pas indexés. L'indexation d'un tableau nécessiterait un type d'index différent, à savoir l'index de tableau gin.

Heureusement, l'index gin_trgm_ops a été si intelligemment conçu qu'il fonctionne avec des opérateurs like et ilike , qui peut être utilisé comme solution alternative (exemple décrit ci-dessous).

Tableau d'essais

a deux colonnes (id serial primary key, names text[]) et contient 100 000 phrases latines divisées en éléments de tableau.

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}

Recherche du fragment de mot praesent donne 7051 lignes en 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

Vue matérialisée

Une solution consiste à normaliser le modèle, impliquant la création d'une nouvelle table avec un seul nom sur une ligne. Une telle restructuration peut être difficile à mettre en œuvre et parfois impossible en raison des requêtes, vues, fonctions ou autres dépendances existantes. Un effet similaire peut être obtenu sans changer la structure de la table, en utilisant une vue matérialisée.

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 n'est pas nécessaire, mais peut être utile lors de l'agrégation des noms dans le même ordre que dans la table principale. Interroger test_names donne les mêmes résultats que la table principale dans le même temps.

Après avoir créé l'index, le temps d'exécution diminue à plusieurs reprises :

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 solution présente quelques inconvénients. Comme la vue est matérialisée, les données sont stockées deux fois dans la base de données. Vous devez vous rappeler de rafraîchir la vue après les modifications apportées à la table principale. Et les requêtes peuvent être plus compliquées en raison de la nécessité de joindre la vue à la table principale.

Utiliser ilike

Nous pouvons utiliser ilike sur les tableaux représentés sous forme de texte. Nous avons besoin d'une fonction immuable pour créer l'index sur le tableau dans son ensemble :

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

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

et utilisez la fonction dans les requêtes :

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 contre 2400 ms, résultat plutôt sympa sans avoir besoin de créer des relations supplémentaires.

Cette solution semble plus simple et nécessitant moins de travail, à condition toutefois que ilike , qui est un outil moins précis que le trgm % opérateur, est suffisant.

Pourquoi devrions-nous utiliser ilike plutôt que % pour des tableaux entiers comme du texte ? La similarité dépend en grande partie de la longueur des textes. Il est très difficile de choisir une limite appropriée pour la recherche d'un mot dans des textes longs de différentes longueurs. avec limit = 0.3 nous avons les résultats :

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)