Нужно проиндексировать огромный столбец текста в таблице postgres, который подходит для поиска по подстановочным знакам как в начале, так и в конце "% xyz%" - PullRequest
0 голосов
/ 05 апреля 2019

У меня работает база данных Postgres 9.6.У него есть таблица, которая насчитывает около 98 000 000 строк и растет.

Имеется столбец file_path, в котором хранится относительный путь к файлу.Пример: /directory1/123456/CT1_1111_111_111-CT2_2222_222_222-fail.xml.Значения CTx_xxx постоянно меняются.

В настоящее время в этом столбце нет индексов, поскольку мы не выполняли поиск с его использованием.Однако возникла необходимость в выборке с использованием этого столбца без других поддерживаемых индексированных столбцов.Что усложняет мою проблему, так это то, что поиск должен поддерживать поиск по шаблону, где file_path, например, '%CT1_1111%'.

Выполнение этого в запросе занимает всегда, как и ожидалось.Мне нужно проиндексировать этот столбец, но не могу найти решение для этого.

Простой индекс b-дерева, очевидно, не работал, так как он не поддерживает LIKE.Тогда я тоже попробовал text_pattern_ops.Это тоже не сработает из-за предыдущего символа подстановки.

Я тоже попробовал индекс gin_trgm_ops, но поиск также был очень медленным.Эта таблица имеет мощность 1.14793103E10

Я ожидаю, что у меня будет запрос, который сможет вернуть результат, скажем, через 2-3 секунды.Моя проблема в том, что это очень старая структура базы данных с большим количеством строк.Я бы хотел избежать реструктуризации БД по той же причине.

Ответы [ 3 ]

0 голосов
/ 05 апреля 2019

Скорее всего, не будет гарантии на время ответа 2-3 секунды. По крайней мере, до тех пор, пока задействован дисковый ввод-вывод, и вы не используете последний SSD (или даже лучше: NVMe) с высоким IOPS и самой низкой задержкой. Также достаточно оперативной памяти является требованием здесь. Пожалуйста, учтите это, прежде чем принять решение о стратегии индексации.

Если ни ваши данные, ни индексы не помещаются в память, вы должны максимально уменьшить количество дисковых операций ввода-вывода для каждого запроса или позволить PostgreSQL использовать стратегии, которые помогают хотя бы уменьшить случайные операции ввода-вывода (например, для чего выполняется сканирование растрового индекса).

Текстовый поиск с использованием LIKE в содержит подстроку , способ не будет хорошо работать на любом большом столе.

Альтернативой (будет работать, только если запросы ищут одинаковые детали в file_path) может быть (для вашего примера поиск CTX_XXXX):

-- create a function to extract the specific file_path substring
CREATE OR REPLACE FUNCTION get_filename_part(file_path text, idx int)
    RETURNS text
    LANGUAGE SQL
    IMMUTABLE
AS $$
    SELECT regexp_replace(file_path, '.*/(CT.{6}).*-(CT.{6}).*', E'\\' || idx);
$$;

-- create a helper function for querying...
CREATE OR REPLACE FUNCTION check_filename_parts(file_path text, c_value text)
    RETURNS boolean
    LANGUAGE SQL
    IMMUTABLE
AS $$
    SELECT get_filename_part(file_path, 1) = c_value OR get_filename_part(file_path, 2) = c_value;
$$;

-- create indexes...
CREATE INDEX idx_filename_ct_first ON text_search (get_filename_part(file_path, 1));
CREATE INDEX idx_filename_ct_second ON text_search (get_filename_part(file_path, 2));

... и используйте запрос, такой как:

SELECT *
FROM text_search
WHERE check_filename_parts(file_path, 'CT1_1111');

Поясняется с данными испытаний

Обратите внимание, что следующие тесты проводились на 8-летнем оборудовании потребительского класса (но, по крайней мере, на SSD).

Создание тестовых данных (8 000 000 строк - почти случайных):

CREATE TABLE text_search (id serial PRIMARY KEY, file_path text);

INSERT INTO text_search (file_path)
SELECT '/directory1/123456/CT' || (random() * 8 + 1)::int || '_' || (random() * 8999 + 1000)::int || '_' || (random() * 899 + 100)::int || '_' || (random() * 899 + 100)::int || '-CT' || (random() * 8 + 1)::int || '_' || (random() * 8999 + 1000)::int || '_' || (random() * 899 + 100)::int || '_' || (random() * 899 + 100)::int || '-fail.xml'
FROM generate_series(1, 8000000);

--- and analyze...
ANALYZE text_search;

... объясненный выше запрос выбора (после перезапуска сервера):

                                                               QUERY PLAN                                                               
----------------------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on text_search  (cost=5.49..409.93 rows=203 width=66) (actual time=0.092..0.882 rows=110 loops=1)
   Recheck Cond: ((get_filename_part(file_path, 1) = 'CT1_1111'::text) OR (get_filename_part(file_path, 2) = 'CT1_1111'::text))
   Heap Blocks: exact=110
   Buffers: shared read=116
   I/O Timings: read=0.576
   ->  BitmapOr  (cost=5.49..5.49 rows=203 width=0) (actual time=0.071..0.072 rows=0 loops=1)
         Buffers: shared read=6
         I/O Timings: read=0.036
         ->  Bitmap Index Scan on idx_filename_ct_first  (cost=0.00..2.70 rows=102 width=0) (actual time=0.038..0.038 rows=48 loops=1)
               Index Cond: (get_filename_part(file_path, 1) = 'CT1_1111'::text)
               Buffers: shared read=3
               I/O Timings: read=0.017
         ->  Bitmap Index Scan on idx_filename_ct_second  (cost=0.00..2.69 rows=101 width=0) (actual time=0.032..0.032 rows=62 loops=1)
               Index Cond: (get_filename_part(file_path, 2) = 'CT1_1111'::text)
               Buffers: shared read=3
               I/O Timings: read=0.019
 Planning Time: 4.996 ms
 Execution Time: 0.922 ms
(18 rows)

Общий фильтр с использованием gin_trgm_ops

... по сравнению с общим запросом LIKE с использованием индекса gin_trgm_ops (после 3 запусков - данные в кеше):

-- create index...
CREATE INDEX idx_filename ON text_search USING gin (file_path gin_trgm_ops);

EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM text_search
WHERE file_path LIKE '%CT1_1111%';

                                                         QUERY PLAN                                                          
-----------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on text_search  (cost=94.70..1264.40 rows=800 width=66) (actual time=20.699..27.775 rows=110 loops=1)
   Recheck Cond: (file_path ~~ '%CT1_1111%'::text)
   Rows Removed by Index Recheck: 8207
   Heap Blocks: exact=7978
   Buffers: shared hit=8277
   ->  Bitmap Index Scan on idx_filename  (cost=0.00..94.50 rows=800 width=0) (actual time=19.328..19.328 rows=8317 loops=1)
         Index Cond: (file_path ~~ '%CT1_1111%'::text)
         Buffers: shared hit=299
 Planning Time: 0.722 ms
 Execution Time: 27.912 ms
(10 rows)

TL; DR

Если возможно, вкладывать средства в небольшую инфраструктуру, чтобы добиться максимальной производительности, используя внутреннее сравнение =. Это значительно сэкономит на вводе / выводе процессора, по сравнению с любым другим подходом. Но также следите за ухудшением производительности записи с ростом индексов. Вы можете просто прийти к компромиссу.

0 голосов
/ 05 апреля 2019

Если ваш шаблон должен совпадать с началом имени файла (без пути), тогда альтернативой будет индексирование последнего элемента пути, который затем можно будет найти с использованием правильного привязанного шаблона, например, CT1_111%:

create index idx_last_element on your_table 
    (((string_to_array(file_path,'/'))[cardinality(string_to_array(file_path,'/'))]) text_pattern_ops);

Затем вам нужно использовать это выражение в вашем запросе SQL:

select *
from your_table
where (string_to_array(file_path,'/'))[cardinality(string_to_array(file_path,'/'))] like 'CT1_111%';

Это будет использовать вышеуказанный индекс.


Вы можете упростить свой запрос, поместив это выражение в функцию:

create or replace function extract_file_name(p_path text)
  returns text
as
$$  
  select elements[cardinality(elements)] 
  from (select string_to_array(p_path,'/') elements ) t;
$$
language sql
immutable;

И используйте эту функцию для создания индекса:

create index idx_file_name on your_table( (extract_file_name(file_path)) text_pattern_ops);

Тогдаиспользуйте эту функцию в запросе:

select *
from your_table
where extract_file_name(file_path) like 'CT1_111%';

На моем ноутбуке с Windows, использующем Postgres 11 с 2 миллионами строк, это приводит к следующему плану выполнения:

Index Scan using last_element on public.file_paths  (cost=0.43..2.69 rows=200 width=82) (actual time=0.193..0.437 rows=36 loops=1)
  Output: id, created_at, path
  Index Cond: ((extract_file_name(file_paths.path) ~>=~ 'CT1_111'::text) AND (extract_file_name(file_paths.path) ~<~ 'a504'::text))
  Filter: (extract_file_name(file_paths.path) ~~ 'CT1_111%'::text)
  Buffers: shared hit=36 read=3
  I/O Timings: read=0.066
Planning Time: 0.918 ms
Execution Time: 0.459 ms
0 голосов
/ 05 апреля 2019

Индексы триграмм - ваша единственная надежда.

Вы использовали индекс GIN, а не индекс GiST, верно?

Убедитесь, что вы указали минимальную длину строки поиска, если набор результатов ограничен и поиск выполняется достаточно быстро.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...