Как мне сделать postgres, чтобы избежать двойного последовательного сканирования для этого запроса разбивки на страницы? - PullRequest
0 голосов
/ 20 июня 2020

Схема

  • У меня есть куча сообщений, хранящихся в таблице (feed_items)
  • У меня есть таблица, содержащая, какой идентификатор пользователя понравился / не понравился which feed_item_id (feed_item_likes_dislikes)
  • У меня есть другая таблица, содержащая, какой идентификатор пользователя любил / возмущал, какой feed_item_id (feed_item_love_anger)
  • У меня есть четвертая таблица, содержащая, какая feed_item_id имеет какие теги, где теги являются ARRAY из varchar (feed_item_tags)
  • Общее количество лайков / антипатий для каждой публикации сохраняется в материализованном представлении (feed_item_likes_dislikes_aggregate)
  • Общее количество любви / гнева сохраняется в другом материализованном представлении (feed_item_love_anger_agregate)
  • Мне нравится, не нравится, любовь и гнев хранятся отдельно, потому что сообщение может понравиться / не понравиться и полюбиться / разозлить одновременно (к сожалению, бизнес-требования)
  • У меня есть 2 столбца с названиями title_vector и summary_vector of введите TSVECTOR в feed_items, который помогает найти позицию ts по ключевому слову поиска (полнотекстовый поиск в postgres)

Проблема

  • Я хочу найти все сообщения в порядке убывания их pubdate и feed_item_id
  • Некоторые сообщения публикуются одновременно, и я хочу разбить их на страницы, используя (pubdate, feed_item_id) <(value1, value2) описанный метод разбивки на страницы <a href="https://use-the-index-luke.com/sql/partial-results/fetch-next-page" rel="nofollow noreferrer"> HERE

Моя страница 1 Запрос

Найти сообщения с лайками> 0, в заголовке или сводке которых есть слово мошенничество, помеченное как

SELECT
  fi.feed_item_id,
  pubdate,
  link,
  title,
  summary,
  author,
  feed_id,
  likes,
  dislikes,
  love,
  anger,
  tags 
FROM
  feed_items fi 
  LEFT JOIN
    feed_item_tags t 
    ON fi.feed_item_id = t.feed_item_id 
  LEFT JOIN
    feed_item_love_anger_aggregate bba 
    ON fi.feed_item_id = bba.feed_item_id 
  LEFT JOIN
    feed_item_likes_dislikes_aggregate lda 
    ON fi.feed_item_id = lda.feed_item_id 
WHERE
  (
    title_vector @@ to_tsquery('scam') 
    OR summary_vector @@ to_tsquery('scam')
  )
  AND 'for' = ANY(tags) 
  AND likes > 0 
ORDER BY
  pubdate DESC,
  feed_item_id DESC LIMIT 3;

EXPLAIN ANALYZE Page 1

 Limit  (cost=2.83..16.88 rows=3 width=233) (actual time=0.075..0.158 rows=3 loops=1)
   ->  Nested Loop Left Join  (cost=2.83..124.53 rows=26 width=233) (actual time=0.074..0.157 rows=3 loops=1)
         ->  Nested Loop  (cost=2.69..116.00 rows=26 width=217) (actual time=0.067..0.146 rows=3 loops=1)
               Join Filter: (t.feed_item_id = fi.feed_item_id)
               Rows Removed by Join Filter: 73
               ->  Index Scan using idx_feed_items_pubdate_feed_item_id_desc on feed_items fi  (cost=0.14..68.77 rows=76 width=62) (actual time=0.016..0.023 rows=3 loops=1)
                     Filter: ((title_vector @@ to_tsquery('scam'::text)) OR (summary_vector @@ to_tsquery('scam'::text)))
                     Rows Removed by Filter: 1
               ->  Materialize  (cost=2.55..8.56 rows=34 width=187) (actual time=0.016..0.037 rows=25 loops=3)
                     ->  Hash Join  (cost=2.55..8.39 rows=34 width=187) (actual time=0.044..0.091 rows=36 loops=1)
                           Hash Cond: (t.feed_item_id = lda.feed_item_id)
                           ->  Seq Scan on feed_item_tags t  (cost=0.00..5.25 rows=67 width=155) (actual time=0.009..0.043 rows=67 loops=1)
                                 Filter: ('for'::text = ANY ((tags)::text[]))
                                 Rows Removed by Filter: 33
                           ->  Hash  (cost=1.93..1.93 rows=50 width=32) (actual time=0.029..0.029 rows=50 loops=1)
                                 Buckets: 1024  Batches: 1  Memory Usage: 12kB
                                 ->  Seq Scan on feed_item_likes_dislikes_aggregate lda  (cost=0.00..1.93 rows=50 width=32) (actual time=0.004..0.013 rows=50 loops=1)
                                       Filter: (likes > 0)
                                       Rows Removed by Filter: 24
         ->  Index Scan using idx_feed_item_love_anger_aggregate on feed_item_love_anger_aggregate bba  (cost=0.14..0.32 rows=1 width=32) (actual time=0.002..0.003 rows=0 loops=3)
               Index Cond: (feed_item_id = fi.feed_item_id)
 Planning Time: 0.601 ms
 Execution Time: 0.195 ms
(23 rows)

Он выполняет последовательное сканирование 2 раза, несмотря на наличие соответствующих индексов во всех таблицах

My Page N Query

Возьмите дату публикации и feed_item_id 3-го результата из вышеуказанного запроса и загрузите следующие 3 результата

SELECT
  fi.feed_item_id,
  pubdate,
  link,
  title,
  summary,
  author,
  feed_id,
  likes,
  dislikes,
  love,
  anger,
  tags 
FROM
  feed_items fi 
  LEFT JOIN
    feed_item_tags t 
    ON fi.feed_item_id = t.feed_item_id 
  LEFT JOIN
    feed_item_love_anger_aggregate bba 
    ON fi.feed_item_id = bba.feed_item_id 
  LEFT JOIN
    feed_item_likes_dislikes_aggregate lda 
    ON fi.feed_item_id = lda.feed_item_id 
WHERE
  (
    pubdate,
    fi.feed_item_id
  )
  < ('2020-06-19 19:50:00+05:30', 'bc5c8dfe-13a9-d97a-a328-0e5b8990c500') 
  AND 
  (
    title_vector @@ to_tsquery('scam') 
    OR summary_vector @@ to_tsquery('scam')
  )
  AND 'for' = ANY(tags) 
  AND likes > 0 
ORDER BY
  pubdate DESC,
  feed_item_id DESC LIMIT 3;

Объясните запрос страницы N Несмотря на фильтрацию, он выполняет 2 последовательных сканирование

 Limit  (cost=2.83..17.13 rows=3 width=233) (actual time=0.082..0.199 rows=3 loops=1)
   ->  Nested Loop Left Join  (cost=2.83..121.97 rows=25 width=233) (actual time=0.081..0.198 rows=3 loops=1)
         ->  Nested Loop  (cost=2.69..113.67 rows=25 width=217) (actual time=0.073..0.185 rows=3 loops=1)
               Join Filter: (t.feed_item_id = fi.feed_item_id)
               Rows Removed by Join Filter: 183
               ->  Index Scan using idx_feed_items_pubdate_feed_item_id_desc on feed_items fi  (cost=0.14..67.45 rows=74 width=62) (actual time=0.014..0.034 rows=6 loops=1)
                     Index Cond: (ROW(pubdate, feed_item_id) < ROW('2020-06-19 19:50:00+05:30'::timestamp with time zone, 'bc5c8dfe-13a9-d97a-a328-0e5b8990c500'::uuid))
                     Filter: ((title_vector @@ to_tsquery('scam'::text)) OR (summary_vector @@ to_tsquery('scam'::text)))
                     Rows Removed by Filter: 2
               ->  Materialize  (cost=2.55..8.56 rows=34 width=187) (actual time=0.009..0.022 rows=31 loops=6)
                     ->  Hash Join  (cost=2.55..8.39 rows=34 width=187) (actual time=0.050..0.098 rows=36 loops=1)
                           Hash Cond: (t.feed_item_id = lda.feed_item_id)
                           ->  Seq Scan on feed_item_tags t  (cost=0.00..5.25 rows=67 width=155) (actual time=0.009..0.044 rows=67 loops=1)
                                 Filter: ('for'::text = ANY ((tags)::text[]))
                                 Rows Removed by Filter: 33
                           ->  Hash  (cost=1.93..1.93 rows=50 width=32) (actual time=0.028..0.029 rows=50 loops=1)
                                 Buckets: 1024  Batches: 1  Memory Usage: 12kB
                                 ->  Seq Scan on feed_item_likes_dislikes_aggregate lda  (cost=0.00..1.93 rows=50 width=32) (actual time=0.005..0.014 rows=50 loops=1)
                                       Filter: (likes > 0)
                                       Rows Removed by Filter: 24
         ->  Index Scan using idx_feed_item_love_anger_aggregate on feed_item_love_anger_aggregate bba  (cost=0.14..0.32 rows=1 width=32) (actual time=0.003..0.003 rows=1 loops=3)
               Index Cond: (feed_item_id = fi.feed_item_id)
 Planning Time: 0.596 ms
 Execution Time: 0.236 ms
(24 rows)

ССЫЛКА НА СКРЕПКУ

Я установил скрипку с необходимыми таблицами и индексами, может ли кто-нибудь сказать мне, как исправить запрос, чтобы использовать сканирование индекса в лучшем случае или сократить количество последовательных сканирований до 1?

Ответы [ 2 ]

1 голос
/ 20 июня 2020

В настоящее время у вас нет другого индекса в таблице тегов, кроме индекса GIN. В вашей скрипке, если я create index on feed_item_tags (feed_item_id) и выполняю ANALYZE, то оба seq сканируют go. Вероятно, лучше сделать это таким образом, а затем переформулировать так, чтобы он мог использовать индекс GIN, как и мой другой ответ, потому что этот способ более эффективно использует перспективу ранней остановки с помощью LIMIT.

Но действительно, в чем смысл таблицы "feed_item_tags"? Если вы собираетесь иметь дочернюю таблицу для перечисления тегов, у вас обычно будет одна комбинация tag / parent_id для каждой строки. Если вам нужен массив тегов вместо их столбца, почему бы просто не вставить массив непосредственно в родительскую таблицу? Иногда есть причины иметь таблицы с соотношением 1: 1 между двумя таблицами, но не очень часто.

1 голос
/ 20 июня 2020

Конструкция 'for' = ANY(tags) не может использовать индекс GIN. Чтобы использовать это, вам нужно будет переформулировать его во что-то вроде '{for}' <@ tags.

Однако тогда он все равно решит не использовать индекс, потому что таблица настолько мала, а условие настолько неселективно . Если вы все равно хотите принудительно использовать индекс, чтобы доказать, что он способен на это, вы можете сначала set enable_seqscan=off.

...