Pull to refresh

Comments 37

Раз уж Вы оптимизируете не только поисковый запрос, но и структуру исходных таблиц, постановка задачи подсказывает использование инструментов fts

Иногда действительно удобно полностью положиться на полнотекстовый поиск, но не всегда. Если нужно определять объекты в документе более аккуратно, то на мой взгляд лучше это делать более тонкими механизмами.

Не совсем понял «более аккуратно». Можно пример.

Простой пример: Иванов Иван Иванович = Иванов И.И. = Иванов Иван = И. Иванов = Иван Иванов = Иван наш дорогой Иванович!
Пример посложнее — тезки: Иванов Иван в одном контексте (например, политика) — один человек. В другом контексте (например, мотоспорт) — другой человек.
Еще: Владимир (город) != Владимир (имя человека).
И так далее.

Что если сделать так?
CREATE INDEX i_content_keyword_ref ON content_keyword_ref(keyword_id, content_id);

SELECT distinct r.id FROM content_keyword_ref r where r.keyword_id IN (4713, 5951);

Такой индекс я добавлял, он называется content_keyword_ref_keyword_content_idx. В EXPLAIN он есть, а в листинг я его почему-то не добавил. Спасибо, что заметили, добавлю его в текст.

На этой паре айдишников ваш запрос отрабатывает примерно в 3 раза быстрее, чем вариант с фильтрацией массива. Причем не важно — с JOIN или без, главное — без LIMIT. Попробовал другую пару айдишников, для которых в базе больше данных — там наоборот вариант с фильтрацией массива получается в 2 раза быстрее.


План для вашего варианта
HashAggregate  (cost=21103.43..21160.44 rows=5701 width=4) (actual time=3.280..3.447 rows=1786 loops=1)
  Group Key: content_id
  ->  Bitmap Heap Scan on content_keyword_ref r  (cost=118.16..21088.82 rows=5845 width=4) (actual time=0.785..2.754 rows=2007 loops=1)
        Recheck Cond: (keyword_id = ANY ('{4713,5951}'::integer[]))
        Heap Blocks: exact=1781
        ->  Bitmap Index Scan on content_keyword_ref_keyword_content_idx  (cost=0.00..116.70 rows=5845 width=0) (actual time=0.403..0.403 rows=2007 loops=1)
              Index Cond: (keyword_id = ANY ('{4713,5951}'::integer[]))
Planning time: 0.138 ms
Execution time: 3.542 ms
Айдишники должны быть в одном индексе. Тогда запрос работает только по индексу, не считывая данные из таблиц, что гораздо быстрее.

Понял вашу идею. Так и есть. Жаль, что не всегда можно обойтись чтением только айдишников.

Сколь я помню у PosgresPro есть возможность при создании индекса помещать в его листья дополнительные поля, чтобы не обращаться к таблице. Параметр including, если память не изменяет. А вообще используйте вариант genew с поиском по индексу без обращения к таблице (кстати, вам на самом деле нужен только один составной индекс, а не целых три!). Этот вариант лучше массива залитого gin — в массиве вы теряете согласованность данных на внешних ключах. Кстати, во внешних ключах правильнее использовать on update cascade, чем no action — понятно, что первичные ключи не обновляют, но концептуально… А в запросе вместо distinct можно попробовать group by, он на прежних версиях меньше ошибался.

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


Кстати, обнаружил интересный пункт в todo.


Change foreign key constraint for array -> element to mean element in array?

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

А если использовать method hash?
CREATE INDEX i_content_keyword_ref ON content_keyword_ref(keyword_id, content_id) USING HASH

PostgreSQL 9.6:


CREATE INDEX i_content_keyword_ref ON content_keyword_ref USING HASH(keyword_id, content_id)


ОШИБКА:  метод доступа "hash" не поддерживает индексы по многим столбцам

CREATE INDEX i_content_keyword_ref ON content_keyword_ref USING HASH(keyword_id)


а так
если оставить только этот индекс

Explain покажите с вариантом побольше документов. Желательно сразу explain (analyze,buffers)

Индекс добавил, но похоже он не используется.


Запрос с EXPLAIN
EXPLAIN (ANALYZE, BUFFERS)
SELECT DISTINCT c.*
FROM content_keyword_ref r 
JOIN content c on c.id = r.content_id
AND r.keyword_id IN (65, 95)
LIMIT 1000

Limit  (cost=1136536.10..1136568.60 rows=1000 width=651) (actual time=8013.431..8014.788 rows=1000 loops=1)
  Buffers: shared hit=3 read=397765, temp read=190014 written=204678
  ->  Unique  (cost=1136536.10..1142497.90 rows=183440 width=651) (actual time=8013.430..8014.724 rows=1000 loops=1)
        Buffers: shared hit=3 read=397765, temp read=190014 written=204678
        ->  Sort  (cost=1136536.10..1136994.70 rows=183440 width=651) (actual time=8013.429..8013.873 rows=1799 loops=1)
              Sort Key: c.id, c.content_url_hash, c.content_url, c.source_id, c.published_date, c.title, c.rubric_name, c.lead, c.is_visible, c.is_deleted, c.created_at, c.updated_at
              Sort Method: external merge  Disk: 128552kB
              Buffers: shared hit=3 read=397765, temp read=190014 written=204678
              ->  Hash Join  (cost=532835.09..1013909.91 rows=183440 width=651) (actual time=2178.317..7575.413 rows=189753 loops=1)
                    Hash Cond: (r.content_id = c.id)
                    Buffers: shared hit=3 read=397765, temp read=172732 written=171710
                    ->  Bitmap Heap Scan on content_keyword_ref r  (cost=3430.53..299134.75 rows=183440 width=4) (actual time=40.914..606.283 rows=189753 loops=1)
                          Recheck Cond: (keyword_id = ANY ('{65,95}'::integer[]))
                          Rows Removed by Index Recheck: 2416411
                          Heap Blocks: exact=44226 lossy=53909
                          Buffers: shared hit=1 read=98659
                          ->  Bitmap Index Scan on content_keyword_ref_keyword_content_idx  (cost=0.00..3384.67 rows=183440 width=0) (actual time=32.682..32.682 rows=189753 loops=1)
                                Index Cond: (keyword_id = ANY ('{65,95}'::integer[]))
                                Buffers: shared hit=1 read=524
                    ->  Hash  (cost=320935.36..320935.36 rows=2182736 width=651) (actual time=2129.508..2129.508 rows=2185453 loops=1)
                          Buckets: 8192  Batches: 512  Memory Usage: 2844kB
                          Buffers: shared hit=2 read=299106, temp written=170274
                          ->  Seq Scan on content c  (cost=0.00..320935.36 rows=2182736 width=651) (actual time=0.008..774.666 rows=2185453 loops=1)
                                Buffers: shared hit=2 read=299106
Planning time: 0.542 ms
Execution time: 8035.036 ms
EXPLAIN ANALYSE
SELECT c.id 
FROM content c
JOIN content_keyword_ref r ON r.content_id = c.id
    AND r.keyword_id IN (4713, 5951)
GROUP BY c.id
LIMIT 1000

Ммм. Ещё раз.
Вы селектите id из content, к которому по content.id джойните другую табличку? Вопрос: нафига вам вообще сдался джойн? Выкинуть нафиг как бесполезную операцию.

EXPLAIN ANALYSE
select distinct r.content_id 
from content_keyword_ref r
    AND r.keyword_id IN (4713, 5951)

Уникальный индекс keyword_id & content_id.

Вытащить ещё пару полей из content:
EXPLAIN ANALYSE
select c.foo, c.bar, c.id from content c
where c.id in (
select distinct r.content_id 
from content_keyword_ref r
    AND r.keyword_id IN (4713, 5951)
)

Опять же все зависит от данных. Попробовал ваш вариант — на одних данных он дает такую же скорость как и вариант с массивами, на других данных (где под выдачу попадает больше документов) скорость ниже примерно в 50 раз.

Explain покажите с вариантом побольше документов. Желательно сразу explain (analyze,buffers)
Запрос с EXPLAIN
EXPLAIN (ANALYZE, BUFFERS)
select c.* from content c
where c.id in (
select distinct r.content_id 
from keywords_content_ref r
    WHERE r.keyword_id IN (65, 95)
)
LIMIT 1000

Limit  (cost=317681.87..322062.63 rows=1000 width=651) (actual time=707.809..711.862 rows=1000 loops=1)
  Buffers: shared hit=3215 read=99449, temp read=65 written=326
  ->  Nested Loop  (cost=317681.87..722280.01 rows=92358 width=651) (actual time=707.808..711.765 rows=1000 loops=1)
        Buffers: shared hit=3215 read=99449, temp read=65 written=326
        ->  Unique  (cost=317681.44..318598.64 rows=92358 width=4) (actual time=707.779..708.136 rows=1000 loops=1)
              Buffers: shared hit=1 read=98659, temp read=65 written=326
              ->  Sort  (cost=317681.44..318140.04 rows=183440 width=4) (actual time=707.778..707.940 rows=1799 loops=1)
                    Sort Key: r.content_id
                    Sort Method: external merge  Disk: 2600kB
                    Buffers: shared hit=1 read=98659, temp read=65 written=326
                    ->  Bitmap Heap Scan on content_keyword_ref r  (cost=3430.53..299134.75 rows=183440 width=4) (actual time=36.964..597.049 rows=189753 loops=1)
                          Recheck Cond: (keyword_id = ANY ('{65,95}'::integer[]))
                          Rows Removed by Index Recheck: 2416411
                          Heap Blocks: exact=44226 lossy=53909
                          Buffers: shared hit=1 read=98659
                          ->  Bitmap Index Scan on content_keyword_ref_keyword_content_idx  (cost=0.00..3384.67 rows=183440 width=0) (actual time=28.715..28.715 rows=189753 loops=1)
                                Index Cond: (keyword_id = ANY ('{65,95}'::integer[]))
                                Buffers: shared hit=1 read=524
        ->  Index Scan using content_pkey on content c  (cost=0.43..4.35 rows=1 width=651) (actual time=0.003..0.003 rows=1 loops=1000)
              Index Cond: (id = r.content_id)
              Buffers: shared hit=3214 read=790
Planning time: 0.185 ms
Execution time: 712.438 ms
Всё читали с диска. Да ещё workmem ни на bitmap heap ни на сортировку не хватило. Разумеется диск — это медленно. Для массива в таких условиях тоже будет медленно.

Да, база у меня на диске, причем на hdd. С массивами все-же быстрее получается, причем сейчас postgres даже почему-то решил не использовать индекс по массиву:


Запрос с EXPLAIN
EXPLAIN (ANALYZE, BUFFERS)
SELECT c.*
FROM documents r 
JOIN content c on c.id = r.content_id
    AND r.keyword_ids && ARRAY[65, 95]
LIMIT 1000

Limit  (cost=0.43..4648.40 rows=1000 width=651) (actual time=0.020..11.901 rows=1000 loops=1)
  Buffers: shared hit=3169 read=1038
  ->  Nested Loop  (cost=0.43..518206.89 rows=111491 width=651) (actual time=0.019..11.806 rows=1000 loops=1)
        Buffers: shared hit=3169 read=1038
        ->  Seq Scan on documents r  (cost=0.00..50352.16 rows=111630 width=4) (actual time=0.011..8.005 rows=1000 loops=1)
              Filter: (keyword_ids && '{65,95}'::integer[])
              Rows Removed by Filter: 17983
              Buffers: shared hit=3 read=197
        ->  Index Scan using content_pkey on content c  (cost=0.43..4.18 rows=1 width=651) (actual time=0.003..0.003 rows=1 loops=1000)
              Index Cond: (id = r.content_id)
              Buffers: shared hit=3166 read=841
Planning time: 0.201 ms
Execution time: 11.969 ms
Планировщик решил, что какие-то несчастные полтора мегабайта быстрее прочитать последовательно и озадачить CPU обработкой, чем поднимать индекс и потом ходить случайным i/o. В общем-то, планировщик прав и в условии «дай мне любые 1000 документов» работает внятно заканчивая перебор сильно раньше срока.
Поясните, если у нас тэги в массиве, зачем для них отдельная таблица, можно же сделать в той же самой.

Можно. Но это не всегда лучше. Например, если у вас в основной таблице записей много, но далеко не для каждой есть записи в таблице связей. Тогда чтобы все это занимало меньше места на диске, лучше разнести. Или более надуманный пример — если таблицы большие, но на партиции бить не хотите. Мне хотелось более общий случай рассмотреть, поэтому я их не объединял.

Побольше бы таких публикаций, очень понравилось, спасибо.
Подскажите, в данном случае INTEGER[] будет просто массивом целых чисел или включится расширение intarray ( https://www.postgresql.org/docs/9.6/static/intarray.html )?
Чтобы включилось расширение нужно использовать CREATE EXTENSION (выполняется один раз для базы данных).

После включения расширения можно использовать его функции с этим типом данных.
Это я прекрасно знаю. Вопрос, здесь использовалось расширение или использовался общий тип «массив чего-то-там». Будет ли разница (в плане или скорости), если включить расширение?
Здесь использовался «общий тип». Не будет разницы в скорости.

Вот это используется: https://www.postgresql.org/docs/9.6/static/functions-array.html

Посмотрел список установленных расширений у себя:


# \dx
                                   Список установленных расширений
   Имя    | Версия |   Схема    |                              Описание                              
----------+--------+------------+--------------------------------------------------------------------
 intarray | 1.2    | public     | functions, operators, and index support for 1-D arrays of integers
 pg_trgm  | 1.3    | public     | text similarity measurement and index searching based on trigrams
 plpgsql  | 1.0    | pg_catalog | PL/pgSQL procedural language
(3 строки)
Не вижу варианта с exists, который сам собой напрашивается:
select c.id from content c
where exists(
    select 1 from content_keyword_ref r
    where r.content_id = c.id and r.keyword_id IN (4713, 5951)
)
limit 1000

Да. вполне уместный вариант. По скорости он ведет себя так — на маленьких выборках работает быстрее в 2-3 раза, чем с фильтрацией массива, а на больших — в 3-4 раза медленнее.

А какой у вас размер базы?
что такое большая и маленькая выборка?

Всего документов примерно 2 млн, а связей с ключевыми словами примерно 15 млн. Под большой выборкой имею ввиду случаи когда в нее попадает порядка 100 тыс документов. Под маленькой — 1 тыс.

На последнем дотнексте обсуждалось схожая задача (в контексте работы тэгов на Stackoverflow), в итоге оптимизация вылилась в «храним все тэги в памяти, а анализ принадлежности тэгов выполняем на GPU». Так что оптмизировать можно еще долго. Спасибо за статью.

идея лежит на поверхности.
Пока не видел баз в GPU?

Sign up to leave a comment.

Articles