Чому плани відрізняються, якщо запити логічно схожі?


19

Я написав дві функції, щоб відповісти на перше запитання домашнього завдання Дня 3 із семи баз даних за сім тижнів .

Створіть збережену процедуру, де ви можете ввести назву фільму чи ім’я актора, яке вам подобається, і воно поверне п’ятірку пропозицій на основі будь-яких фільмів, в яких знявся актор, або фільмів з подібними жанрами.

Моя перша спроба правильна, але повільна. Щоб повернути результат, це може зайняти до 2000 мс.

CREATE OR REPLACE FUNCTION suggest_movies(IN query text, IN result_limit integer DEFAULT 5)
  RETURNS TABLE(movie_id integer, title text) AS
$BODY$
WITH suggestions AS (

  SELECT
    actors.name AS entity_term,
    movies.movie_id AS suggestion_id,
    movies.title AS suggestion_title,
    1 AS rank
  FROM actors
  INNER JOIN movies_actors ON (actors.actor_id = movies_actors.actor_id)
  INNER JOIN movies ON (movies.movie_id = movies_actors.movie_id)

  UNION ALL

  SELECT
    searches.title AS entity_term,
    suggestions.movie_id AS suggestion_id,
    suggestions.title AS suggestion_title,
    RANK() OVER (PARTITION BY searches.movie_id ORDER BY cube_distance(searches.genre, suggestions.genre)) AS rank
  FROM movies AS searches
  INNER JOIN movies AS suggestions ON
    (searches.movie_id <> suggestions.movie_id) AND
    (cube_enlarge(searches.genre, 2, 18) @> suggestions.genre)
)
SELECT suggestion_id, suggestion_title
FROM suggestions
WHERE entity_term = query
ORDER BY rank, suggestion_id
LIMIT result_limit;
$BODY$
LANGUAGE sql;

Моя друга спроба правильна і швидка. Я оптимізував це, натиснувши фільтр із CTE в кожну частину об'єднання.

Я видалив цей рядок із зовнішнього запиту:

WHERE entity_term = query

Я додав цей рядок до першого внутрішнього запиту:

WHERE actors.name = query

Я додав цей рядок до другого внутрішнього запиту:

WHERE movies.title = query

Друга функція займає приблизно 10 мс, щоб повернути той же результат.

Ніщо не відрізняється в базі даних, крім визначень функцій.

Чому PostgreSQL створює такі різні плани для цих двох логічно еквівалентних запитів?

EXPLAIN ANALYZEПлан першої функції виглядає наступним чином :

                                                                                       Limit  (cost=7774.18..7774.19 rows=5 width=44) (actual time=1738.566..1738.567 rows=5 loops=1)
   CTE suggestions
     ->  Append  (cost=332.56..7337.19 rows=19350 width=285) (actual time=7.113..1577.823 rows=383024 loops=1)
           ->  Subquery Scan on "*SELECT* 1"  (cost=332.56..996.80 rows=11168 width=33) (actual time=7.113..22.258 rows=11168 loops=1)
                 ->  Hash Join  (cost=332.56..885.12 rows=11168 width=33) (actual time=7.110..19.850 rows=11168 loops=1)
                       Hash Cond: (movies_actors.movie_id = movies.movie_id)
                       ->  Hash Join  (cost=143.19..514.27 rows=11168 width=18) (actual time=4.326..11.938 rows=11168 loops=1)
                             Hash Cond: (movies_actors.actor_id = actors.actor_id)
                             ->  Seq Scan on movies_actors  (cost=0.00..161.68 rows=11168 width=8) (actual time=0.013..1.648 rows=11168 loops=1)
                             ->  Hash  (cost=80.86..80.86 rows=4986 width=18) (actual time=4.296..4.296 rows=4986 loops=1)
                                   Buckets: 1024  Batches: 1  Memory Usage: 252kB
                                   ->  Seq Scan on actors  (cost=0.00..80.86 rows=4986 width=18) (actual time=0.009..1.681 rows=4986 loops=1)
                       ->  Hash  (cost=153.61..153.61 rows=2861 width=19) (actual time=2.768..2.768 rows=2861 loops=1)
                             Buckets: 1024  Batches: 1  Memory Usage: 146kB
                             ->  Seq Scan on movies  (cost=0.00..153.61 rows=2861 width=19) (actual time=0.003..1.197 rows=2861 loops=1)
           ->  Subquery Scan on "*SELECT* 2"  (cost=6074.48..6340.40 rows=8182 width=630) (actual time=1231.324..1528.188 rows=371856 loops=1)
                 ->  WindowAgg  (cost=6074.48..6258.58 rows=8182 width=630) (actual time=1231.324..1492.106 rows=371856 loops=1)
                       ->  Sort  (cost=6074.48..6094.94 rows=8182 width=630) (actual time=1231.307..1282.550 rows=371856 loops=1)
                             Sort Key: searches.movie_id, (cube_distance(searches.genre, suggestions_1.genre))
                             Sort Method: external sort  Disk: 21584kB
                             ->  Nested Loop  (cost=0.27..3246.72 rows=8182 width=630) (actual time=0.047..909.096 rows=371856 loops=1)
                                   ->  Seq Scan on movies searches  (cost=0.00..153.61 rows=2861 width=315) (actual time=0.003..0.676 rows=2861 loops=1)
                                   ->  Index Scan using movies_genres_cube on movies suggestions_1  (cost=0.27..1.05 rows=3 width=315) (actual time=0.016..0.277 rows=130 loops=2861)
                                         Index Cond: (cube_enlarge(searches.genre, 2::double precision, 18) @> genre)
                                         Filter: (searches.movie_id <> movie_id)
                                         Rows Removed by Filter: 1
   ->  Sort  (cost=436.99..437.23 rows=97 width=44) (actual time=1738.565..1738.566 rows=5 loops=1)
         Sort Key: suggestions.rank, suggestions.suggestion_id
         Sort Method: top-N heapsort  Memory: 25kB
         ->  CTE Scan on suggestions  (cost=0.00..435.38 rows=97 width=44) (actual time=1281.905..1738.531 rows=43 loops=1)
               Filter: (entity_term = 'Die Hard'::text)
               Rows Removed by Filter: 382981
 Total runtime: 1746.623 ms

EXPLAIN ANALYZEПлан другого запиту виглядає наступним чином :

 Limit  (cost=43.74..43.76 rows=5 width=44) (actual time=1.231..1.234 rows=5 loops=1)
   CTE suggestions
     ->  Append  (cost=4.86..43.58 rows=5 width=391) (actual time=1.029..1.141 rows=43 loops=1)
           ->  Subquery Scan on "*SELECT* 1"  (cost=4.86..20.18 rows=2 width=33) (actual time=0.047..0.047 rows=0 loops=1)
                 ->  Nested Loop  (cost=4.86..20.16 rows=2 width=33) (actual time=0.047..0.047 rows=0 loops=1)
                       ->  Nested Loop  (cost=4.58..19.45 rows=2 width=18) (actual time=0.045..0.045 rows=0 loops=1)
                             ->  Index Scan using actors_name on actors  (cost=0.28..8.30 rows=1 width=18) (actual time=0.045..0.045 rows=0 loops=1)
                                   Index Cond: (name = 'Die Hard'::text)
                             ->  Bitmap Heap Scan on movies_actors  (cost=4.30..11.13 rows=2 width=8) (never executed)
                                   Recheck Cond: (actor_id = actors.actor_id)
                                   ->  Bitmap Index Scan on movies_actors_actor_id  (cost=0.00..4.30 rows=2 width=0) (never executed)
                                         Index Cond: (actor_id = actors.actor_id)
                       ->  Index Scan using movies_pkey on movies  (cost=0.28..0.35 rows=1 width=19) (never executed)
                             Index Cond: (movie_id = movies_actors.movie_id)
           ->  Subquery Scan on "*SELECT* 2"  (cost=23.31..23.40 rows=3 width=630) (actual time=0.982..1.081 rows=43 loops=1)
                 ->  WindowAgg  (cost=23.31..23.37 rows=3 width=630) (actual time=0.982..1.064 rows=43 loops=1)
                       ->  Sort  (cost=23.31..23.31 rows=3 width=630) (actual time=0.963..0.971 rows=43 loops=1)
                             Sort Key: searches.movie_id, (cube_distance(searches.genre, suggestions_1.genre))
                             Sort Method: quicksort  Memory: 28kB
                             ->  Nested Loop  (cost=4.58..23.28 rows=3 width=630) (actual time=0.808..0.916 rows=43 loops=1)
                                   ->  Index Scan using movies_title on movies searches  (cost=0.28..8.30 rows=1 width=315) (actual time=0.025..0.027 rows=1 loops=1)
                                         Index Cond: (title = 'Die Hard'::text)
                                   ->  Bitmap Heap Scan on movies suggestions_1  (cost=4.30..14.95 rows=3 width=315) (actual time=0.775..0.844 rows=43 loops=1)
                                         Recheck Cond: (cube_enlarge(searches.genre, 2::double precision, 18) @> genre)
                                         Filter: (searches.movie_id <> movie_id)
                                         Rows Removed by Filter: 1
                                         ->  Bitmap Index Scan on movies_genres_cube  (cost=0.00..4.29 rows=3 width=0) (actual time=0.750..0.750 rows=44 loops=1)
                                               Index Cond: (cube_enlarge(searches.genre, 2::double precision, 18) @> genre)
   ->  Sort  (cost=0.16..0.17 rows=5 width=44) (actual time=1.230..1.231 rows=5 loops=1)
         Sort Key: suggestions.rank, suggestions.suggestion_id
         Sort Method: top-N heapsort  Memory: 25kB
         ->  CTE Scan on suggestions  (cost=0.00..0.10 rows=5 width=44) (actual time=1.034..1.187 rows=43 loops=1)
 Total runtime: 1.410 ms

Відповіді:


21

Немає автоматичного натискання на предикати для CTE

PostgreSQL 9.3 не робить предикатного витиску для CTE.

Оптимізатор, який робить передбачувальний висув, може переміщуватись там, де клаузує внутрішні запити. Мета - відфільтрувати невідповідні дані якомога раніше. Поки новий запит є логічно еквівалентним, двигун все ще отримує всі відповідні дані, тому дає правильний результат лише швидше.

Основний розробник Том Лейн натякає на складність визначення логічної еквівалентності у списку розсилки pgsql-performance .

ЦТЕ також розглядаються як огорожі для оптимізації; це не стільки обмеження оптимізатора, скільки збереження здорової семантики, коли CTE містить запит, що можна записати.

Оптимізатор не відрізняє CTE лише для читання від записуваних, тому надто консервативний при розгляді планів. Обробка «забором» перешкоджає оптимізатору переміщати пункт «там» всередині CTE, хоча ми можемо бачити, що це безпечно.

Ми можемо чекати, коли команда PostgreSQL поліпшить оптимізацію CTE, але поки для отримання хорошої продуктивності вам доведеться змінити стиль написання.

Перепишіть для виконання

Питання вже показує один із способів отримати кращий план. Дублювання стану фільтра по суті жорстко кодує ефект натискання предиката.

В обох планах двигун копіює рядки результатів на робочий стіл, щоб він міг їх сортувати. Чим більше робочий стіл, тим повільніше запит.

Перший план копіює всі рядки в базових таблицях до робочого столу та сканує, щоб знайти результат. Щоб зробити все ще повільніше, двигун повинен сканувати весь робочий стіл, оскільки він не має індексів.

Це смішна кількість непотрібної роботи. Він зчитує всі дані в базових таблицях двічі, щоб знайти відповідь, коли в оціночних 19350 рядках у базових таблицях є лише 5 відповідних рядків із оцінених 19350 рядків.

Другий план використовує індекси для пошуку відповідних рядків та копіювання лише тих, хто працює на робочому столі. Індекс ефективно фільтрував дані для нас.

На сторінці 85 The Art of SQL, Stéphane Faroult нагадує нам про очікування користувачів.

Дуже великою мірою кінцеві користувачі підлаштовують своє терпіння до кількості очікуваних рядів: коли вони запитують одну голку, вони мало звертають уваги на розмір стога сіна.

Другий план важить з голкою, тому більше шансів зберегти користувачів.

Перепишіть для ремонту

Новий запит важче підтримувати, оскільки ви можете ввести дефект, змінивши одну фільтрацію, але не іншу.

Хіба не було б чудово, якби ми могли все написати лише один раз і все-таки отримати хороші показники?

Ми можемо. Оптимізатор робить передбачувальний підключення для підзаходів.

Простіший приклад простіше пояснити.

CREATE TABLE a (c INT);

CREATE TABLE b (c INT);

CREATE INDEX a_c ON a(c);

CREATE INDEX b_c ON b(c);

INSERT INTO a SELECT 1 FROM generate_series(1, 1000000);

INSERT INTO b SELECT 2 FROM a;

INSERT INTO a SELECT 3;

Це створює дві таблиці з індексованим стовпцем. Разом вони містять мільйон 1s, мільйон 2s та один 3.

Ви можете знайти голку 3за допомогою будь-якого з цих запитів.

-- CTE
EXPLAIN ANALYZE
WITH cte AS (
  SELECT c FROM a
  UNION ALL
  SELECT c FROM b
)
SELECT c FROM cte WHERE c = 3;

-- Subquery
EXPLAIN ANALYZE
SELECT c
FROM (
  SELECT c FROM a
  UNION ALL
  SELECT c FROM b
) AS subquery
WHERE c = 3;

План для CTE повільний. Двигун сканує три таблиці і читає близько чотирьох мільйонів рядків. Це займає майже 1000 мілісекунд.

CTE Scan on cte  (cost=33275.00..78275.00 rows=10000 width=4) (actual time=471.412..943.225 rows=1 loops=1)
  Filter: (c = 3)
  Rows Removed by Filter: 2000000
  CTE cte
    ->  Append  (cost=0.00..33275.00 rows=2000000 width=4) (actual time=0.011..409.573 rows=2000001 loops=1)
          ->  Seq Scan on a  (cost=0.00..14425.00 rows=1000000 width=4) (actual time=0.010..114.869 rows=1000001 loops=1)
          ->  Seq Scan on b  (cost=0.00..18850.00 rows=1000000 width=4) (actual time=5.530..104.674 rows=1000000 loops=1)
Total runtime: 948.594 ms

План для запиту швидкий. Двигун просто шукає кожен індекс. Це займає менше мілісекунди.

Append  (cost=0.42..8.88 rows=2 width=4) (actual time=0.021..0.038 rows=1 loops=1)
  ->  Index Only Scan using a_c on a  (cost=0.42..4.44 rows=1 width=4) (actual time=0.020..0.021 rows=1 loops=1)
        Index Cond: (c = 3)
        Heap Fetches: 1
  ->  Index Only Scan using b_c on b  (cost=0.42..4.44 rows=1 width=4) (actual time=0.016..0.016 rows=0 loops=1)
        Index Cond: (c = 3)
        Heap Fetches: 0
Total runtime: 0.065 ms

Див. SQLFiddle для інтерактивної версії.


0

Плани однакові в Postgres 12

Питання про Postgres 9.3. Через п'ять років ця версія застаріла, але що змінилося?

PostgreSQL 12 тепер додає такі CTE.

Вбудовані З запитами (загальні вирази таблиці)

Загальні вирази таблиці (ака- WITHзапити) тепер можуть бути автоматично введені в запит, якщо вони а) не є рекурсивними, б) не мають побічних ефектів і в) посилаються лише один раз у пізнішій частині запиту. Це знімає "оптимізацію меж", яка існувала з моменту введення WITHпункту в PostgreSQL 8.4

Якщо це необхідно, ви можете змусити запит WITH здійснити за допомогою пункту MATERIALIZED, наприклад

WITH c AS MATERIALIZED ( SELECT * FROM a WHERE a.x % 4 = 0 ) SELECT * FROM c JOIN d ON d.y = a.x;
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.