Tại sao các kế hoạch khác nhau nếu các truy vấn giống nhau về mặt logic?


19

Tôi đã viết hai chức năng để trả lời câu hỏi bài tập về nhà đầu tiên của Ngày 3 từ Bảy Cơ sở dữ liệu trong Bảy tuần .

Tạo một quy trình được lưu trữ trong đó bạn có thể nhập tên phim hoặc tên diễn viên mà bạn thích và nó sẽ trả về năm đề xuất hàng đầu dựa trên các phim mà diễn viên đã đóng hoặc các phim có thể loại tương tự.

Nỗ lực đầu tiên của tôi là đúng nhưng chậm. Có thể mất tới 2000ms để trả về kết quả.

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;

Nỗ lực thứ hai của tôi là chính xác và nhanh chóng. Tôi đã tối ưu hóa nó bằng cách đẩy bộ lọc xuống từ CTE vào từng phần của liên minh.

Tôi đã xóa dòng này khỏi truy vấn bên ngoài:

WHERE entity_term = query

Tôi đã thêm dòng này vào truy vấn bên trong đầu tiên:

WHERE actors.name = query

Tôi đã thêm dòng này vào truy vấn bên trong thứ hai:

WHERE movies.title = query

Hàm thứ hai mất khoảng 10ms để trả về kết quả tương tự.

Không có gì khác nhau trong cơ sở dữ liệu ngoài các định nghĩa hàm.

Tại sao PostgreSQL tạo ra các kế hoạch khác nhau như vậy cho hai truy vấn tương đương logic này?

Các EXPLAIN ANALYZEkế hoạch của hàm đầu tiên trông như thế này:

                                                                                       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

Các EXPLAIN ANALYZEkế hoạch của truy vấn thứ hai trông như thế này:

 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

Câu trả lời:


21

Không đẩy xuống vị ngữ tự động cho CTE

PostgreQuery 9.3 không thực hiện đẩy lùi vị ngữ cho CTE.

Trình tối ưu hóa thực hiện việc đẩy xuống vị ngữ có thể di chuyển các mệnh đề vào các truy vấn bên trong. Mục tiêu là lọc ra những dữ liệu không liên quan càng sớm càng tốt. Miễn là truy vấn mới tương đương về mặt logic, công cụ vẫn tìm nạp tất cả dữ liệu có liên quan, do đó tạo ra kết quả chính xác, chỉ nhanh hơn.

Nhà phát triển cốt lõi Tom Lane ám chỉ đến khó khăn trong việc xác định tính tương đương logic trên danh sách gửi thư hiệu năng pssql .

CTE cũng được coi là hàng rào tối ưu hóa; đây không phải là một giới hạn tối ưu hóa để giữ cho ngữ nghĩa lành mạnh khi CTE chứa một truy vấn có thể ghi.

Trình tối ưu hóa không phân biệt các CTE chỉ đọc với các CTE có thể ghi, do đó quá bảo thủ khi xem xét các kế hoạch. Điều trị 'hàng rào' ngăn chặn trình tối ưu hóa di chuyển mệnh đề where bên trong CTE, mặc dù chúng ta có thể thấy nó an toàn khi làm như vậy.

Chúng tôi có thể chờ đợi nhóm PostgreSQL cải thiện tối ưu hóa CTE, nhưng bây giờ để có hiệu suất tốt, bạn phải thay đổi phong cách viết của mình.

Viết lại cho hiệu suất

Câu hỏi đã chỉ ra một cách để có được một kế hoạch tốt hơn. Sao chép điều kiện bộ lọc về cơ bản là mã cứng hiệu ứng đẩy xuống vị ngữ.

Trong cả hai kế hoạch, công cụ sao chép các hàng kết quả thành một bàn làm việc để nó có thể sắp xếp chúng. Bàn làm việc càng lớn, truy vấn càng chậm.

Kế hoạch đầu tiên sao chép tất cả các hàng trong các bảng cơ sở vào bàn làm việc và quét để tìm kết quả. Để làm cho mọi thứ thậm chí chậm hơn, động cơ phải quét toàn bộ bàn làm việc vì nó không có chỉ mục.

Đó là một số lượng lớn công việc không cần thiết. Nó đọc tất cả dữ liệu trong các bảng cơ sở hai lần để tìm câu trả lời, khi chỉ có 5 hàng khớp ước tính trong số 19350 hàng ước tính trong các bảng cơ sở.

Kế hoạch thứ hai sử dụng các chỉ mục để tìm các hàng phù hợp và chỉ sao chép các hàng vào bàn làm việc. Các chỉ số có hiệu quả lọc dữ liệu cho chúng tôi.

Trên trang 85 của The Art of SQL, Stéphane Faroult nhắc nhở chúng ta về những kỳ vọng của người dùng.

Ở một mức độ rất lớn, người dùng cuối điều chỉnh sự kiên nhẫn của họ theo số lượng hàng họ mong đợi: khi họ yêu cầu một cây kim, họ ít chú ý đến kích thước của đống cỏ khô.

Kế hoạch thứ hai quy mô với kim, vì vậy có nhiều khả năng giữ cho người dùng của bạn hài lòng.

Viết lại cho khả năng bảo trì

Truy vấn mới khó bảo trì hơn vì bạn có thể đưa ra một khiếm khuyết bằng cách thay đổi một epxression của bộ lọc nhưng không phải là bộ lọc khác.

Sẽ không tuyệt sao nếu chúng ta có thể viết mọi thứ chỉ một lần mà vẫn đạt được hiệu suất tốt?

Chúng ta có thể. Trình tối ưu hóa thực hiện đẩy xuống vị ngữ cho các subqeries.

Một ví dụ đơn giản là dễ giải thích hơn.

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;

Điều này tạo ra hai bảng với mỗi cột được lập chỉ mục. Cùng nhau, chúng chứa một triệu 1s, một triệu 2s và một 3.

Bạn có thể tìm kim 3bằng một trong hai truy vấn này.

-- 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;

Kế hoạch cho CTE là chậm. Động cơ quét ba bảng và đọc khoảng bốn triệu hàng. Phải mất gần 1000 mili giây.

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

Kế hoạch cho truy vấn con là nhanh chóng. Động cơ chỉ tìm kiếm từng chỉ số. Phải mất ít hơn một phần nghìn giây.

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

Xem SQLFiddle cho một phiên bản tương tác.


0

Các kế hoạch là giống nhau trong Postgres 12

Câu hỏi hỏi về Postgres 9.3. Năm năm sau, phiên bản đó đã lỗi thời, nhưng điều gì đã thay đổi?

PostgreQuery 12 hiện đang giới thiệu các CTE như thế này.

Nội tuyến VỚI truy vấn (Biểu thức bảng chung)

Các biểu thức bảng thông thường (còn gọi là WITHtruy vấn) hiện có thể được tự động nội tuyến trong một truy vấn nếu chúng a) không được đệ quy, b) không có bất kỳ tác dụng phụ nào và c) chỉ được tham chiếu một lần trong phần sau của truy vấn. Điều này loại bỏ một "hàng rào tối ưu hóa" đã tồn tại kể từ khi giới thiệu WITHmệnh đề trong PostgreQuery 8.4

Nếu cần, bạn có thể buộc truy vấn CÓ để thực hiện bằng cách sử dụng mệnh đề MATERIALIZED, vd

WITH c AS MATERIALIZED ( SELECT * FROM a WHERE a.x % 4 = 0 ) SELECT * FROM c JOIN d ON d.y = a.x;
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.