Мой запрос PostgreSQL 10 очень медленный, нужен способ сделать это быстрее - PullRequest
2 голосов
/ 14 мая 2019

Я использую PostgreSQL 10

Вот моя модель:
https://imgur.com/bibWSq8

Каждый review принадлежит только одному product. Каждый product может принадлежать многим category с. Каждый category может иметь только одного родителя category. Я использую Prisma для запроса базы данных. Это своего рода ОРМ. Я хочу выбрать первые 10 review s из всех product s, которые принадлежат category с id = 27.

Вот запрос, сгенерированный Prisma:

select
"Alias"."id"
from "database"."review" as "Alias"
where ("Alias"."id"
       in (select "database"."review"."id"
           from "database"."review"
           where "database"."review"."product"
                 in (select "database"."category_to_product"."product"
                     from "database"."category_to_product"
                     join "database"."category" as "category_product_Alias"
                        on "category_product_Alias"."id" = "database"."category_to_product"."category"
                     where ("category_product_Alias"."id" = 27
                            or "category_product_Alias"."id"
                               in (select "database"."category"."id"
                                   from "database"."category"
                                   join "database"."category" as "category_category_product_Alias"
                                      on "category_category_product_Alias"."id" = "database"."category"."parent"
                                   where "category_category_product_Alias"."id" = 27
                                  )
                           )
                    )
          )
      )
order by "Alias"."id" desc
limit 11
offset 0;

Есть 1.500.000 review с, 12.000 product с и 130 category с. Этот запрос занимает почти 3 секунды.

Я пытался создать индексы, но это не сработало:

CREATE UNIQUE INDEX category_pkey ON "database".category USING btree (id)
CREATE INDEX idx_category_parent ON "database".category USING btree (parent)
CREATE UNIQUE INDEX "category_to_product_AB_unique" ON "database".category_to_product USING btree (category, product)
CREATE INDEX "category_to_product_B" ON "database".category_to_product USING btree (product))
CREATE UNIQUE INDEX product_pkey ON "database".product USING btree (id)
CREATE INDEX idx_review_product ON "database".review USING btree (product)
CREATE UNIQUE INDEX review_pkey ON "database".review USING btree (id)

А вот результат при запуске explain analyze:

Limit  (cost=9.00..101.89 rows=11 width=4) (actual time=3428.508..3431.048 rows=11 loops=1)
  ->  Merge Semi Join  (cost=9.00..12584725.82 rows=1490226 width=4) (actual time=3428.507..3431.043 rows=11 loops=1)
        Merge Cond: ("Alias".id = review.id)
        ->  Index Only Scan Backward using review_pkey on review "Alias"  (cost=0.43..84869.82 rows=1490226 width=4) (actual time=0.008..152.954 rows=1054436 loops=1)
              Heap Fetches: 0
        ->  Nested Loop Semi Join  (cost=8.57..12477502.61 rows=1490226 width=4) (actual time=3188.974..3191.303 rows=11 loops=1)
              ->  Index Scan Backward using review_pkey on review  (cost=0.43..266561.32 rows=1490226 width=8) (actual time=0.004..415.244 rows=1054436 loops=1)
              ->  Nested Loop  (cost=8.14..8.18 rows=1 width=4) (actual time=0.002..0.002 rows=0 loops=1054436)
                    ->  Index Scan using "category_to_product_B" on category_to_product  (cost=0.29..0.30 rows=1 width=8) (actual time=0.001..0.001 rows=1 loops=1054436)
                          Index Cond: (product = review.product)
                    ->  Index Only Scan using category_pkey on category "category_product_Alias"  (cost=7.86..7.88 rows=1 width=4) (actual time=0.001..0.001 rows=0 loops=1084175)
                          Index Cond: (id = category_to_product.category)
                          Filter: ((id = 27) OR (hashed SubPlan 1))
                          Rows Removed by Filter: 1
                          Heap Fetches: 0
                          SubPlan 1
                            ->  Nested Loop  (cost=0.00..7.71 rows=1 width=4) (actual time=0.016..0.016 rows=0 loops=1)
                                  ->  Seq Scan on category  (cost=0.00..3.85 rows=1 width=8) (actual time=0.015..0.016 rows=0 loops=1)
                                        Filter: (parent = 27)
                                        Rows Removed by Filter: 148
                                  ->  Seq Scan on category "category_category_product_Alias"  (cost=0.00..3.85 rows=1 width=4) (never executed)
                                        Filter: (id = 27)
Planning time: 0.649 ms
Execution time: 3431.098 ms

Я думаю, что мои данные не слишком велики, но запрос слишком медленный. Есть ли способ сделать это быстрее?

Обновление 1 Я просто делаю путь @Laurenz Albe, это быстрее. Вот результат

Limit  (cost=217773.56..217773.59 rows=11 width=8) (actual time=735.033..735.041 rows=11 loops=1)
  ->  Sort  (cost=217773.56..221499.13 rows=1490226 width=8) (actual time=735.031..735.033 rows=11 loops=1)
        Sort Key: (("Alias".id + 0)) DESC
        Sort Method: top-N heapsort  Memory: 25kB
        ->  Hash Semi Join  (cost=99929.33..184545.76 rows=1490226 width=8) (actual time=354.030..733.405 rows=13589 loops=1)
              Hash Cond: ("Alias".id = review.id)
              ->  Seq Scan on review "Alias"  (cost=0.00..60400.26 rows=1490226 width=4) (actual time=0.005..157.747 rows=1482065 loops=1)
              ->  Hash  (cost=81301.50..81301.50 rows=1490226 width=4) (actual time=350.842..350.842 rows=13589 loops=1)
                    Buckets: 2097152  Batches: 1  Memory Usage: 16862kB
                    ->  Hash Join  (cost=410.63..81301.50 rows=1490226 width=4) (actual time=3.363..347.392 rows=13589 loops=1)
                          Hash Cond: (review.product = category_to_product.product)
                          ->  Seq Scan on review  (cost=0.00..60400.26 rows=1490226 width=8) (actual time=0.011..144.852 rows=1482065 loops=1)
                          ->  Hash  (cost=326.86..326.86 rows=6702 width=4) (actual time=2.121..2.121 rows=100 loops=1)
                                Buckets: 8192  Batches: 1  Memory Usage: 68kB
                                ->  HashAggregate  (cost=259.84..326.86 rows=6702 width=4) (actual time=2.064..2.103 rows=100 loops=1)
                                      Group Key: category_to_product.product
                                      ->  Hash Join  (cost=12.86..243.08 rows=6702 width=4) (actual time=0.336..2.026 rows=100 loops=1)
                                            Hash Cond: (category_to_product.category = "category_product_Alias".id)
                                            ->  Seq Scan on category_to_product  (cost=0.00..194.03 rows=13403 width=8) (actual time=0.004..0.873 rows=12063 loops=1)
                                            ->  Hash  (cost=11.93..11.93 rows=74 width=4) (actual time=0.037..0.037 rows=1 loops=1)
                                                  Buckets: 1024  Batches: 1  Memory Usage: 9kB
                                                  ->  Seq Scan on category "category_product_Alias"  (cost=7.71..11.93 rows=74 width=4) (actual time=0.025..0.035 rows=1 loops=1)
                                                        Filter: ((id = 27) OR (hashed SubPlan 1))
                                                        Rows Removed by Filter: 147
                                                        SubPlan 1
                                                          ->  Nested Loop  (cost=0.00..7.71 rows=1 width=4) (actual time=0.015..0.015 rows=0 loops=1)
                                                                ->  Seq Scan on category  (cost=0.00..3.85 rows=1 width=8) (actual time=0.015..0.015 rows=0 loops=1)
                                                                      Filter: (parent = 27)
                                                                      Rows Removed by Filter: 148
                                                                ->  Seq Scan on category "category_category_product_Alias"  (cost=0.00..3.85 rows=1 width=4) (never executed)
                                                                      Filter: (id = 27)
Planning time: 0.591 ms
Execution time: 735.127 ms

Обновление 2 Я попытался упростить запрос:

explain analyze select
"review"."id"
from "review"
where "review"."product" in
(
select "category_to_product"."product"
from "category_to_product"
join "category"
on "category"."id" = "category_to_product"."category"
where "category"."id" = 27 or "category"."parent" = 27
)
order by "reviewty$dev"."review"."id" desc
limit 11
offset 0;

Но результат не сильно меняется

Limit  (cost=0.86..456.52 rows=11 width=4) (actual time=3354.756..3357.181 rows=11 loops=1)
  ->  Nested Loop Semi Join  (cost=0.86..1019733.07 rows=24617 width=4) (actual time=3354.754..3357.176 rows=11 loops=1)
        ->  Index Scan Backward using review_pkey on review  (cost=0.43..266561.32 rows=1490226 width=8) (actual time=0.007..391.076 rows=1054436 loops=1)
        ->  Nested Loop  (cost=0.43..0.50 rows=1 width=4) (actual time=0.002..0.002 rows=0 loops=1054436)
              ->  Index Scan using "category_to_product_B" on category_to_product  (cost=0.29..0.30 rows=1 width=8) (actual time=0.001..0.001 rows=1 loops=1054436)
                    Index Cond: (product = review.product)
              ->  Index Scan using category_pkey on category  (cost=0.14..0.17 rows=1 width=4) (actual time=0.001..0.001 rows=0 loops=1084175)
                    Index Cond: (id = category_to_product.category)
                    Filter: ((id = 27) OR (parent = 27))
                    Rows Removed by Filter: 1
Planning time: 0.434 ms
Execution time: 3357.210 ms

Единственный способ сделать это сейчас - добавить + 0 после order by "Alias"."id". Как ни печально, как я уже сказал, этот запрос генерируется Prisma (prisma.io), а не мной, я хочу написать собственный sql.

Обновление 3 @ Ancoron прав, set enable_nestloop = off перед запуском мой запрос сделает это быстрее. Это заставляет PostgreSQL использовать hash join вместо nested loop.

Limit  (cost=10000238022.63..10000238023.45 rows=11 width=4) (actual time=629.606..629.804 rows=11 loops=1)
  ->  Merge Semi Join  (cost=10000238022.63..10000348970.97 rows=1490226 width=4) (actual time=629.605..629.797 rows=11 loops=1)
        Merge Cond: ("Alias".id = review.id)
        ->  Index Only Scan Backward using review_pkey on review "Alias"  (cost=0.43..84869.82 rows=1490226 width=4) (actual time=0.006..152.252 rows=1054436 loops=1)
              Heap Fetches: 0
        ->  Sort  (cost=10000238022.20..10000241747.77 rows=1490226 width=4) (actual time=390.996..391.000 rows=11 loops=1)
              Sort Key: review.id DESC
              Sort Method: quicksort  Memory: 1021kB
              ->  Hash Semi Join  (cost=10000000604.70..10000085221.14 rows=1490226 width=4) (actual time=4.306..388.164 rows=13589 loops=1)
                    Hash Cond: (review.product = category_to_product.product)
                    ->  Seq Scan on review  (cost=0.00..60400.26 rows=1490226 width=8) (actual time=0.004..157.976 rows=1482065 loops=1)
                    ->  Hash  (cost=10000000529.30..10000000529.30 rows=6032 width=4) (actual time=0.617..0.617 rows=100 loops=1)
                          Buckets: 8192  Batches: 1  Memory Usage: 68kB
                          ->  Merge Join  (cost=10000000008.29..10000000529.30 rows=6032 width=4) (actual time=0.555..0.603 rows=100 loops=1)
                                Merge Cond: (category_to_product.category = "category_product_Alias".id)
                                ->  Index Only Scan using "category_to_product_AB_unique" on category_to_product  (cost=0.29..419.82 rows=12063 width=8) (actual time=0.007..0.374 rows=2272 loops=1)
                                      Heap Fetches: 1123
                                ->  Index Only Scan using category_pkey on category "category_product_Alias"  (cost=10000000007.86..10000000018.82 rows=74 width=4) (actual time=0.024..0.035 rows=1 loops=1)
                                      Filter: ((id = 27) OR (hashed SubPlan 1))
                                      Rows Removed by Filter: 147
                                      Heap Fetches: 0
                                      SubPlan 1
                                        ->  Nested Loop  (cost=10000000000.00..10000000007.71 rows=1 width=4) (actual time=0.015..0.015 rows=0 loops=1)
                                              ->  Seq Scan on category  (cost=0.00..3.85 rows=1 width=8) (actual time=0.015..0.015 rows=0 loops=1)
                                                    Filter: (parent = 27)
                                                    Rows Removed by Filter: 148
                                              ->  Seq Scan on category "category_category_product_Alias"  (cost=0.00..3.85 rows=1 width=4) (never executed)
                                                    Filter: (id = 27)
Planning time: 0.594 ms
Execution time: 629.857 ms

Но я спрашиваю себя, почему я должен это делать, PostgreSQL выбирает неправильный план, он использует вложенный цикл вместо хеш-соединения, это замедляет мой запрос. Это зрелая база данных, поэтому я полагал, что это была моя ошибка, когда запрос медленный, я пытался создать индексы, переписать запрос в надежде, что PostgreSQL изменит свой план, но это не так. Это приемлемо? Другое дело, я уверен, что мой запрос будет выполняться быстрее в каждом случае. Вот мой запрос Prisma:

# Write your query or mutation here
query {
  reviews (where: {
    product:{
      categories_some: {
        OR:[
          {
            id: 27
          },
          {
            parent: {
              id: 27
            }
          }
        ]
      }
    }
  }, orderBy:id_DESC, first:11, skip:0){
    id
  }
}

Я не нашел другого способа изменить мой запрос Prisma.

Ответы [ 2 ]

0 голосов
/ 15 мая 2019

Вы также можете отключить объединение вложенных циклов, используя SET enable_nestloop = off перед выполнением запроса (IDK, если это возможно включить в prisma).

Это дает мне даже немного лучшее время выполнения, как с ORDER BY ... + 0 DESCбез изменения запроса вообще: https://explain.depesz.com/s/Mi4W

Я создал полный онлайн-пример с немного сокращенным набором данных (но все еще занимает некоторое время для загрузки) для проверки различных запросов: https://dbfiddle.uk/?rdbms=postgres_10&fiddle=2c4d104804f57e1a59f7ed31bd57e2f5

Если вы работаете с prisma, возможно, было бы также неплохо поделиться вашим запросом prisma от клиента, который приводит к этому запросу SQL.Может быть, что-то можно сделать и на этой стороне.

С точки зрения запроса, возможно, оптимизация с использованием CTE дает наилучший возможный результат:

WITH
    cte_reviews (id) AS (
        SELECT r.id
        FROM review AS r
            INNER JOIN category_to_product AS cp ON (r.product = cp.product)
        WHERE
            cp.category IN (
                SELECT 27
                UNION ALL
                SELECT id FROM category WHERE parent = 27
            )
        ORDER BY 1 ASC
    )
SELECT id
FROM cte_reviews
ORDER BY id DESC
LIMIT 11 OFFSET 0;

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

До ~ 22 миллисекунд:

Planning time: 0.577 ms
Execution time: 22.021 ms
0 голосов
/ 14 мая 2019

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

Старайтесь избегать сканирования индекса и используйте явную сортировку, изменив предложение ORDER BY на

ORDER BY "Alias".id + 0 DESC

Что я подразумеваю под & ldquo; распределением противника & rdquo ;? Основываясь на своих оценках, PostgreSQL считает, что существует довольно много строк, удовлетворяющих условию, поэтому он считает, что дешевле всего обрабатывать строки в порядке убывания Alias.id и продолжать работу, пока не найдет 11 строк, удовлетворяющих состояние. Даже если догадка верна, может случиться так, что (многие) строки, которые удовлетворяют условию, имеют низкий Alias.id, поэтому он должен вычислить намного больше строк, чем рассчитывал.

Видя ваш второй план выполнения, я подозреваю, что, по крайней мере, частично проблема в том, что PostgreSQL переоценивает количество строк, удовлетворяющих условию: 1490226 вместо 13589 строк. Упрощение запроса может помочь.

...