PostgreSQL
 sql >> база данни >  >> RDS >> PostgreSQL

Как да индексирам колона от низов масив за pg_trgm `'term' % ANY (array_column)` заявка?

Защо това не работи

Типът индекс (т.е. клас оператор) gin_trgm_ops се базира на % оператор, който работи върху два text аргументи:

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

Не можете да използвате gin_trgm_ops за масиви. Индекс, дефиниран за колона от масив, никога няма да работи с any(array[...]) тъй като отделните елементи на масивите не са индексирани. Индексирането на масив би изисквало различен тип индекс, а именно индекс на gin масив.

За щастие, индексът gin_trgm_ops е проектиран толкова умело, че работи с оператори like и ilike , което може да се използва като алтернативно решение (примерът е описан по-долу).

Тестова таблица

има две колони (id serial primary key, names text[]) и съдържа 100 000 латински изречения, разделени на елементи от масив.

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}

Търсене на дума фрагмент praesent дава 7051 реда за 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

Материализиран изглед

Едно решение е да се нормализира моделът, включвайки създаването на нова таблица с едно име в един ред. Такова преструктуриране може да бъде трудно за изпълнение и понякога невъзможно поради съществуващи заявки, изгледи, функции или други зависимости. Подобен ефект може да се постигне без промяна на структурата на таблицата, като се използва материализиран изглед.

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 не е необходимо, но може да бъде полезно, когато се агрегират имената в същия ред като в основната таблица. Запитване за test_names дава същите резултати като основната таблица за същото време.

След създаването на индекса времето за изпълнение намалява многократно:

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

Решението има няколко недостатъка. Тъй като изгледът е материализиран, данните се съхраняват два пъти в базата данни. Трябва да запомните да опресните изгледа след промени в главната таблица. И заявките може да са по-сложни поради необходимостта от присъединяване на изгледа към главната таблица.

Използване на ilike

Можем да използваме ilike върху масивите, представени като текст. Имаме нужда от неизменна функция, за да създадем индекса на масива като цяло:

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

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

и използвайте функцията в заявки:

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 срещу 2400 ms, доста добър резултат без необходимост от създаване на допълнителни отношения.

Това решение изглежда по-просто и изисква по-малко работа, при условие обаче, че ilike , който е по-малко прецизен инструмент от trgm % оператор, е достатъчно.

Защо трябва да използваме ilike вместо % за цели масиви като текст? Сходството зависи до голяма степен от дължината на текстовете. Много е трудно да се избере подходяща граница за търсене на дума в дълги текстове с различна дължина. Напр. с limit = 0.3 имаме резултатите:

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. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Как да създам дефинирана от потребителя функция в AWS Aurora RDS Postgres

  2. Всички групи имат ли еднаква обща мощност за дадена подгрупа?

  3. Рекурсивна заявка, използвана за преходно затваряне

  4. Как да прехвърлите данни от AWS Postgres RDS към S3 (след това Redshift)?

  5. Как да получите сумиране с брой, по-голям от определена сума