sql >> Base de Datos >  >> RDS >> PostgreSQL

¿Cómo indexar una columna de matriz de cadenas para la consulta pg_trgm `'term' % ANY (array_column)`?

Por qué esto no funciona

El tipo de índice (es decir, clase de operador) gin_trgm_ops se basa en % operador, que funciona en dos text argumentos:

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

No puedes usar gin_trgm_ops para matrices. Un índice definido para una columna de matriz nunca funcionará con any(array[...]) porque los elementos individuales de las matrices no están indexados. La indexación de una matriz requeriría un tipo diferente de índice, a saber, el índice de matriz gin.

Afortunadamente, el índice gin_trgm_ops ha sido tan ingeniosamente diseñado que funciona con operadores like y ilike , que se puede utilizar como una solución alternativa (ejemplo descrito a continuación).

Mesa de prueba

tiene dos columnas (id serial primary key, names text[]) y contiene 100000 oraciones latinas divididas en elementos de matriz.

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}

Buscando el fragmento de palabra praesent da 7051 filas 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

Vista materializada

Una solución es normalizar el modelo, lo que implica la creación de una nueva tabla con un solo nombre en una fila. Dicha reestructuración puede ser difícil de implementar y, a veces, imposible debido a consultas, vistas, funciones u otras dependencias existentes. Se puede lograr un efecto similar sin cambiar la estructura de la mesa, usando una vista materializada.

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 no es necesario, pero puede ser útil al agregar los nombres en el mismo orden que en la tabla principal. Consultando test_names da los mismos resultados que la tabla principal en el mismo tiempo.

Después de crear el índice, el tiempo de ejecución disminuye repetidamente:

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 solución tiene algunos inconvenientes. Debido a que la vista se materializa, los datos se almacenan dos veces en la base de datos. Debe recordar actualizar la vista después de los cambios en la tabla principal. Y las consultas pueden ser más complicadas debido a la necesidad de unir la vista a la tabla principal.

Usando ilike

Podemos usar ilike en las matrices representadas como texto. Necesitamos una función inmutable para crear el índice en la matriz como un todo:

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

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

y usa la función en las consultas:

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 contra 2400 ms, resultado bastante bueno sin necesidad de crear relaciones adicionales.

Esta solución parece más simple y requiere menos trabajo, siempre que ilike , que es una herramienta menos precisa que el trgm % operador, es suficiente.

¿Por qué deberíamos usar ilike? en lugar de % para matrices completas como texto? La similitud depende en gran medida de la longitud de los textos. Es muy difícil elegir un límite apropiado para la búsqueda de una palabra en textos largos de varias longitudes. con limit = 0.3 tenemos los resultados:

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)