쿼리가 논리적으로 비슷한 경우 계획이 다른 이유는 무엇입니까?


19

나는 7 주 동안 Seven Databases 의 Day 3의 첫 숙제 질문에 답하는 두 가지 기능을 썼습니다 .

원하는 영화 제목이나 배우 이름을 입력 할 수있는 저장 프로 시저를 만들면 배우가 출연 한 영화 나 비슷한 장르의 영화를 기반으로 상위 5 개 제안을 반환합니다.

첫 번째 시도는 정확하지만 느립니다. 결과를 반환하는 데 최대 2000ms가 걸릴 수 있습니다.

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

두 번째 함수는 동일한 결과를 반환하는 데 약 10ms가 걸립니다.

함수 정의 외에는 데이터베이스에서 다른 점이 없습니다.

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에 대한 술어 푸시 다운 을 수행하지 않습니다 .

술어 푸시 다운을 수행하는 최적화 프로그램은 where 절을 내부 쿼리로 이동할 수 있습니다. 목표는 관련없는 데이터를 가능한 빨리 필터링하는 것입니다. 새 쿼리가 논리적으로 동등한 한 엔진은 여전히 ​​모든 관련 데이터를 가져 오므로 정확한 결과를 더 빨리 생성합니다.

핵심 개발자 인 톰 레인 (Tom Lane)은 pgsql-performance 메일 링리스트 에서 논리적 동등성을 결정하기가 어렵다고 주장합니다 .

CTE는 또한 최적화 펜스로 취급됩니다. 이것은 CTE에 쓰기 가능한 쿼리가 포함되어있을 때 의미를 제고하는 데있어 최적화의 한계가 아닙니다.

옵티마이 저는 읽기 전용 CTE와 쓰기 가능한 CTE를 구분하지 않으므로 계획을 고려할 때 지나치게 보수적입니다. '울타리'처리는 옵티마이 저가 CTE 내부의 where 절을 이동하지 못하게하지만 안전하다는 것을 알 수 있습니다.

PostgreSQL 팀이 CTE 최적화를 개선 할 때까지 기다릴 수 있지만 지금은 좋은 성능을 얻으려면 작문 스타일을 변경해야합니다.

성능을 위해 다시 작성

질문은 이미 더 나은 계획을 얻는 한 가지 방법을 보여줍니다. 필터 조건을 복제하면 술어 푸시 다운의 영향을 본질적으로 하드 코딩합니다.

두 계획에서 엔진은 결과 행을 작업 테이블에 복사하여 정렬 할 수 있습니다. 작업 테이블이 클수록 쿼리 속도가 느려집니다.

첫 번째 계획은 기본 테이블의 모든 행을 작업 테이블에 복사하고 결과를 찾기 위해 스캔합니다. 작업 속도를 줄이려면 엔진에 인덱스가 없으므로 전체 작업 테이블을 스캔해야합니다.

그것은 엄청나게 많은 양의 불필요한 일입니다. 기본 테이블의 추정 된 19350 개 행 중 5 개의 일치하는 행이있을 때 답을 찾기 위해 기본 테이블의 모든 데이터를 두 번 읽습니다.

두 번째 계획은 인덱스를 사용하여 일치하는 행을 찾고 해당 행만 작업 테이블에 복사합니다. 인덱스는 우리를 위해 효과적으로 데이터를 필터링했습니다.

85 페이지 SQL의 예술, 스테판 Faroult는 사용자의 기대의 우리를 생각 나게한다.

대부분의 경우 최종 사용자는 예상되는 행 수에 따라 인내심을 조정합니다. 바늘 하나를 요청하면 건초 더미의 크기에 거의주의를 기울이지 않습니다.

두 번째 계획은 바늘에 따라 확장되므로 사용자의 만족도를 높일 수 있습니다.

유지 보수성을 위해 다시 작성

하나의 필터 epxression을 변경하여 결함을 유발할 수 있지만 다른 필터는 변경하지 않기 때문에 새 쿼리를 유지하기가 더 어렵습니다.

모든 것을 한 번만 작성해도 여전히 좋은 성능을 얻을 수 있다면 좋지 않을까요?

우리는 할 수 있습니다. 옵티마이 저는 서브 쿼리에 대한 술어 푸시 다운을 수행합니다.

더 간단한 예는 설명하기가 더 쉽습니다.

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;

이렇게하면 각각 인덱스 열이있는 두 개의 테이블이 만들어집니다. 그들은 함께 백만 1, 백만 2및 하나를 포함 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 계획이 느립니다. 엔진은 3 개의 테이블을 스캔하고 약 4 백만 개의 행을 읽습니다. 거의 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에 대한 질문입니다. 5 년 후, 그 버전은 더 이상 사용되지 않지만 무엇이 변경 되었습니까?

PostgreSQL 12는 이제 이와 같은 CTE를 인라인합니다.

인라인 된 WITH 쿼리 (공통 테이블 표현식)

공통 테이블 표현식 (일명 WITH쿼리)은 a) 재귀 적이 지 않고 b) 부작용이없고 c) 쿼리의 후반부에서 한 번만 참조되는 경우 쿼리에서 자동으로 인라인 될 수 있습니다. 이것은 도입 후 존재했던 "최적화 울타리"를 제거합니다WITH PostgreSQL 8.4에 절이

필요한 경우 MATERIALIZED 절을 사용하여 WITH 쿼리를 강제로 구현할 수 있습니다.

WITH c AS MATERIALIZED ( SELECT * FROM a WHERE a.x % 4 = 0 ) SELECT * FROM c JOIN d ON d.y = a.x;
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.