Các khung nhìn có hại cho hiệu suất trong PostgreSQL không?


45

Sau đây là đoạn trích từ một cuốn sách về thiết kế db (Bắt đầu thiết kế cơ sở dữ liệu ISBN: 0-7645-7490-6):

Nguy hiểm với việc sử dụng các khung nhìn là lọc một truy vấn đối với một khung nhìn, hy vọng sẽ đọc được một phần rất nhỏ của một bảng rất lớn. Bất kỳ quá trình lọc nào cũng phải được thực hiện trong chế độ xem vì bất kỳ chức năng lọc nào đối với chế độ xem được áp dụng sau khi truy vấn trong chế độ xem đã thực hiện xong. Các khung nhìn thường hữu ích để tăng tốc quá trình phát triển nhưng về lâu dài có thể giết chết hoàn toàn hiệu suất cơ sở dữ liệu.

Sau đây là đoạn trích từ Tài liệu PostgreQuery 9.5:

Sử dụng các khung nhìn tự do là một khía cạnh quan trọng của thiết kế cơ sở dữ liệu SQL tốt. Chế độ xem cho phép bạn gói gọn các chi tiết về cấu trúc của các bảng, có thể thay đổi khi ứng dụng của bạn phát triển, đằng sau các giao diện nhất quán.

Hai nguồn dường như mâu thuẫn với nhau ("không thiết kế với các khung nhìn" so với "làm thiết kế với các khung nhìn").

Tuy nhiên, trong chế độ xem PG được thực hiện bằng hệ thống quy tắc. Vì vậy, có thể (và đây là câu hỏi của tôi) bất kỳ bộ lọc nào đối với chế độ xem được viết lại dưới dạng bộ lọc trong chế độ xem, dẫn đến thực hiện truy vấn duy nhất đối với các bảng bên dưới.

Giải thích của tôi có đúng không và PG kết hợp các mệnh đề WHERE vào và ra khỏi tầm nhìn? Hay nó chạy chúng riêng rẽ, cái này đến cái khác? Bất kỳ ngắn, khép kín, chính xác (biên dịch), ví dụ?


Tôi nghĩ rằng câu hỏi không đúng vì cả hai nguồn đều không nói về cùng một thứ. Cái đầu tiên có liên quan đến truy vấn từ một khung nhìn và SAU áp dụng bộ lọc : SELECT * FROM my_view WHERE my_column = 'blablabla';. Trong khi thứ hai là về việc sử dụng các khung nhìn để làm cho mô hình dữ liệu của bạn trong suốt đối với ứng dụng sử dụng nó. Các nguồn đầu tiên chỉ cho bạn bao gồm bộ lọc WHERE my_column = 'blablabla'bên trong định nghĩa khung nhìn, vì điều này dẫn đến một kế hoạch thực hiện tốt hơn.
EAmez

Câu trả lời:


49

Cuốn sách sai.

Chọn từ một khung nhìn chính xác là nhanh hay chậm như chạy câu lệnh SQL cơ bản - bạn có thể dễ dàng kiểm tra xem bằng cách sử dụng explain analyze.

Trình tối ưu hóa Postgres (và trình tối ưu hóa cho nhiều DBMS hiện đại khác) sẽ có thể đẩy các vị từ trên khung nhìn vào câu lệnh xem thực tế - với điều kiện đây là một tuyên bố đơn giản (một lần nữa, điều này có thể được xác minh bằng cách sử dụng explain analyze).

"Danh tiếng xấu" liên quan đến hiệu suất bắt nguồn - tôi nghĩ - từ khi bạn lạm dụng các lượt xem và bắt đầu xây dựng các lượt xem sử dụng các lượt xem sử dụng các lượt xem. Rất thường dẫn đến các câu lệnh làm quá nhiều so với một câu lệnh được điều chỉnh bằng tay mà không có các khung nhìn, ví dụ như vì một số bảng trung gian sẽ không cần thiết. Trong hầu hết các trường hợp, trình tối ưu hóa không đủ thông minh để loại bỏ các bảng / phép nối không cần thiết đó hoặc đẩy các vị từ xuống qua nhiều cấp độ xem (điều này cũng đúng với các DBMS khác).


3
Đưa ra một số câu trả lời phản hồi được đề xuất, bạn có thể muốn giải thích một chút về câu lệnh đơn giản là gì .
RDFozz

Bạn có thể giải thích làm thế nào để sử dụng các explain analyzetuyên bố?
Dustin Michels

@DustinMichels: hãy xem hướng dẫn sử dụng: postgresql.org/docs/civerse/USE-explain.html
a_horse_with_no_name

19

Để cho bạn một ví dụ về những gì @a_horse đã giải thích :

Postgres thực hiện lược đồ thông tin, bao gồm các khung nhìn (đôi khi phức tạp) cung cấp thông tin về các đối tượng DB ở dạng chuẩn. Điều này thuận tiện và đáng tin cậy - và có thể đắt hơn đáng kể so với việc truy cập trực tiếp vào bảng danh mục của Postgres.

Ví dụ rất đơn giản, để lấy tất cả các cột hiển thị của bảng
... từ lược đồ thông tin:

SELECT column_name
FROM   information_schema.columns
WHERE  table_name = 'big'
AND    table_schema = 'public';

... từ danh mục hệ thống:

SELECT attname
FROM   pg_catalog.pg_attribute
WHERE  attrelid = 'public.big'::regclass
AND    attnum > 0
AND    NOT attisdropped;

So sánh kế hoạch truy vấn và thời gian thực hiện cho cả hai với EXPLAIN ANALYZE.

  • Truy vấn đầu tiên dựa trên chế độ xem information_schema.columns, tham gia vào nhiều bảng mà chúng ta không cần cho điều này.

  • Truy vấn thứ hai chỉ quét một bảng pg_catalog.pg_attribute, do đó nhanh hơn nhiều. (Nhưng truy vấn đầu tiên vẫn chỉ cần một vài ms trong các DB chung.)

Chi tiết:


7

BIÊN TẬP:

Với lời xin lỗi, tôi cần rút lại khẳng định của mình rằng câu trả lời được chấp nhận không phải lúc nào cũng đúng - nó nói rằng quan điểm luôn giống hệt với điều tương tự được viết như một câu hỏi phụ. Tôi nghĩ đó là điều không thể chối cãi, và tôi nghĩ bây giờ tôi biết những gì đang xảy ra trong trường hợp của tôi.

Bây giờ tôi cũng nghĩ rằng có một câu trả lời tốt hơn cho câu hỏi ban đầu.

Câu hỏi ban đầu là về việc có nên hướng dẫn thực hành sử dụng các khung nhìn hay không (ví dụ, lặp lại SQL trong các thói quen có thể cần được duy trì hai lần trở lên).

Câu trả lời của tôi sẽ là "không phải nếu truy vấn của bạn sử dụng các chức năng của cửa sổ hay bất cứ điều gì khác khiến trình tối ưu hóa xử lý truy vấn khác nhau khi nó trở thành truy vấn phụ, bởi vì chính hành động tạo truy vấn con (dù được biểu thị dưới dạng xem hay không) có thể làm giảm hiệu suất nếu bạn đang lọc với các tham số trong thời gian chạy.

Sự phức tạp của chức năng cửa sổ của tôi là không cần thiết. Kế hoạch giải thích cho việc này:

SELECT DISTINCT ts.train_service_key,
            pc.assembly_key,
            count(*) OVER 
              (PARTITION BY ts.train_service_key) AS train_records
FROM staging.train_service ts
   JOIN staging.portion_consist pc 
     USING (ds_code, train_service_key)
WHERE assembly_key = '185132';

ít tốn kém hơn nhiều so với việc này:

SELECT *
FROM (SELECT DISTINCT ts.train_service_key,
            pc.assembly_key,
            count(*) OVER
              (PARTITION BY ts.train_service_key) AS train_records
FROM staging.train_service ts
   JOIN staging.portion_consist pc
     USING (ds_code, train_service_key)) AS query
WHERE assembly_key = '185132';

Hy vọng đó là một chút cụ thể và hữu ích.

Theo kinh nghiệm gần đây của tôi (khiến tôi tìm thấy câu hỏi này), câu trả lời được chấp nhận ở trên không chính xác trong tất cả các chu kỳ. Tôi có một truy vấn tương đối đơn giản bao gồm chức năng cửa sổ:

SELECT DISTINCT ts.train_service_key,
                pc.assembly_key,
                dense_rank() OVER (PARTITION BY ts.train_service_key
                ORDER BY pc.through_idx DESC, pc.first_portion ASC,
               ((CASE WHEN (NOT ts.primary_direction)
                 THEN '-1' :: INTEGER
                 ELSE 1
                 END) * pc.first_seq)) AS coach_block_idx
FROM (staging.train_service ts
JOIN staging.portion_consist pc USING (ds_code, train_service_key))

Nếu tôi thêm bộ lọc này:

where assembly_key = '185132'

Kế hoạch giải thích tôi nhận được như sau:

QUERY PLAN
Unique  (cost=11562.66..11568.77 rows=814 width=43)
  ->  Sort  (cost=11562.66..11564.70 rows=814 width=43)
    Sort Key: ts.train_service_key, (dense_rank() OVER (?))
    ->  WindowAgg  (cost=11500.92..11523.31 rows=814 width=43)
          ->  Sort  (cost=11500.92..11502.96 rows=814 width=35)
                Sort Key: ts.train_service_key, pc.through_idx DESC, pc.first_portion, ((CASE WHEN (NOT ts.primary_direction) THEN '-1'::integer ELSE 1 END * pc.first_seq))
                ->  Nested Loop  (cost=20.39..11461.57 rows=814 width=35)
                      ->  Bitmap Heap Scan on portion_consist pc  (cost=19.97..3370.39 rows=973 width=38)
                            Recheck Cond: (assembly_key = '185132'::text)
                            ->  Bitmap Index Scan on portion_consist_assembly_key_index  (cost=0.00..19.72 rows=973 width=0)
                                  Index Cond: (assembly_key = '185132'::text)
                      ->  Index Scan using train_service_pk on train_service ts  (cost=0.43..8.30 rows=1 width=21)
                            Index Cond: ((ds_code = pc.ds_code) AND (train_service_key = pc.train_service_key))

Điều này đang sử dụng chỉ mục khóa chính trên bảng dịch vụ xe lửa và chỉ mục không duy nhất trên bảng part_consist. Nó thực thi trong 90ms.

Tôi đã tạo một chế độ xem (dán nó ở đây để hoàn toàn rõ ràng nhưng đó thực sự là truy vấn trong một chế độ xem):

CREATE OR REPLACE VIEW staging.v_unit_coach_block AS
SELECT DISTINCT ts.train_service_key,
            pc.assembly_key,
            dense_rank() OVER (PARTITION BY ts.train_service_key
              ORDER BY pc.through_idx DESC, pc.first_portion ASC, (
                (CASE
              WHEN (NOT ts.primary_direction)
                THEN '-1' :: INTEGER
              ELSE 1
              END) * pc.first_seq)) AS coach_block_idx
 FROM (staging.train_service ts
  JOIN staging.portion_consist pc USING (ds_code, train_service_key))

Khi tôi truy vấn chế độ xem này với bộ lọc giống hệt nhau:

select * from staging.v_unit_coach_block
where assembly_key = '185132';

Đây là kế hoạch giải thích:

QUERY PLAN
Subquery Scan on v_unit_coach_block  (cost=494217.13..508955.10     rows=3275 width=31)
Filter: (v_unit_coach_block.assembly_key = '185132'::text)
 ->  Unique  (cost=494217.13..500767.34 rows=655021 width=43)
    ->  Sort  (cost=494217.13..495854.68 rows=655021 width=43)
          Sort Key: ts.train_service_key, pc.assembly_key, (dense_rank() OVER (?))
          ->  WindowAgg  (cost=392772.16..410785.23 rows=655021 width=43)
                ->  Sort  (cost=392772.16..394409.71 rows=655021 width=35)
                      Sort Key: ts.train_service_key, pc.through_idx DESC, pc.first_portion, ((CASE WHEN (NOT ts.primary_direction) THEN '-1'::integer ELSE 1 END * pc.first_seq))
                      ->  Hash Join  (cost=89947.40..311580.26 rows=655021 width=35)
                            Hash Cond: ((pc.ds_code = ts.ds_code) AND (pc.train_service_key = ts.train_service_key))
                            ->  Seq Scan on portion_consist pc  (cost=0.00..39867.86 rows=782786 width=38)
                            ->  Hash  (cost=65935.36..65935.36 rows=1151136 width=21)
                                  ->  Seq Scan on train_service ts  (cost=0.00..65935.36 rows=1151136 width=21)

Điều này đang thực hiện quét toàn bộ trên cả hai bảng và mất 17 giây.

Cho đến khi tôi bắt gặp điều này, tôi đã tự do sử dụng các khung nhìn với PostgreSQL (đã hiểu các quan điểm được tổ chức rộng rãi thể hiện trong câu trả lời được chấp nhận). Tôi đặc biệt tránh sử dụng các chế độ xem nếu tôi cần lọc tổng hợp trước, trong đó tôi sử dụng các hàm trả về cài đặt.

Tôi cũng biết rằng các CTE trong PostgreSQL được đánh giá riêng biệt, theo thiết kế, vì vậy tôi không sử dụng chúng theo cách tương tự như với SQL Server, ví dụ, nơi chúng dường như được tối ưu hóa như các truy vấn con.

Do đó, câu trả lời của tôi là, có những trường hợp trong đó các khung nhìn không thực hiện chính xác như truy vấn mà chúng dựa trên, vì vậy nên thận trọng. Tôi đang sử dụng Amazon Aurora dựa trên PostgreQuery 9.6.6.


2
Lưu ý cảnh báo trong câu trả lời khác - " miễn là đây là một tuyên bố đơn giản ".
RDFozz

Là một lưu ý phụ, CASE WHEN (NOT ts.primary_direction) THEN '-1' :: INTEGER ELSE 1 ENDsẽ không cần thiết làm cho truy vấn chậm hơn mức cần thiết, bạn nên viết thêm hai điều kiện theo thứ tự.
Evan Carroll

@EvanCarroll Tôi vật lộn với điều này một thời gian. Chỉ cần tìm thấy nó nhanh hơn một chút để kéo CASE ra một cấp:CASE WHEN (NOT ts.primary_direction) THEN dense_rank() OVER (PARTITION BY ts.train_service_key ORDER BY pc.through_idx DESC, pc.first_portion ASC, pc.first_seq DESC) ELSE dense_rank() OVER (PARTITION BY ts.train_service_key ORDER BY pc.through_idx DESC, pc.first_portion ASC, pc.first_seq ASC) END AS coach_block_idx
enjayaitch

Đó cũng không phải là một ý tưởng tốt .. bạn đã có một vài vấn đề ở đây. Ý tôi là vấn đề lớn là quan điểm của bạn không thực sự có ý nghĩa và nó làm những điều khác biệt vì việc bạn sử dụng dense_rank()vì vậy nó không thực sự là một vấn đề hiệu suất.
Evan Carroll

1
@EvanCarroll bình luận của bạn đã nhắc tôi tự mình đến đó (do đó câu trả lời đã được chỉnh sửa của tôi). Cảm ơn bạn.
enjayaitch

0

(Tôi là một người hâm mộ lớn về lượt xem, nhưng bạn phải rất cẩn thận với PG ở đây và tôi muốn khuyến khích mọi người sử dụng lượt xem thường trong PG để dễ hiểu hơn và khả năng duy trì truy vấn / mã)

Thật ra và thật đáng buồn (CẢNH BÁO :) sử dụng lượt xem trong Postgres đã gây ra cho chúng tôi các vấn đề thực sự và làm giảm hiệu suất của chúng tôi tùy thuộc vào các tính năng chúng tôi đang sử dụng bên trong nó :-( (ít nhất là với v10.1). (Điều này sẽ không như vậy với các hệ thống DB hiện đại như Oracle.)

Vì vậy, có thể (và đây là câu hỏi của tôi) bất kỳ bộ lọc nào đối với chế độ xem ... dẫn đến một thực thi truy vấn duy nhất đối với các bảng bên dưới.

(Tùy thuộc vào ý nghĩa chính xác của bạn - không - các bảng tạm thời trung gian có thể được cụ thể hóa mà bạn có thể không muốn hoặc nơi các vị từ không được đẩy xuống ...)

Tôi biết ít nhất hai "tính năng" chính, khiến chúng tôi thất vọng trong quá trình di chuyển từ Oracle sang Postgres nên chúng tôi phải từ bỏ PG trong một dự án:

  • Cácwith CTE ( truy vấn phụ / biểu thức bảng chung ) (thường) hữu ích cho việc cấu trúc các truy vấn phức tạp hơn (ngay cả trong các ứng dụng nhỏ hơn), nhưng trong PG được thiết kế thực hiện dưới dạng gợi ý tối ưu hóa "ẩn" (tạo ra các bảng tạm thời không được lập chỉ mục) và do đó vi phạm khái niệm (đối với tôi và rất nhiều người quan trọng khác) về SQL khai báo ( Oracle docu ): vd

    • truy vấn đơn giản:

      explain
      
        select * from pg_indexes where indexname='pg_am_name_index'
      
      /* result: 
      
      Nested Loop Left Join  (cost=12.38..26.67 rows=1 width=260)
        ...
        ->  Bitmap Index Scan on pg_class_relname_nsp_index  (cost=0.00..4.29 rows=2 width=0)
                                               Index Cond: (relname = 'pg_am_name_index'::name)
        ...
      */
    • viết lại bằng cách sử dụng một số CTE:

      explain
      
        with 
      
        unfiltered as (
          select * from pg_indexes
        ) 
      
        select * from unfiltered where indexname='pg_am_name_index'
      
      /* result:
      
      CTE Scan on unfiltered  (cost=584.45..587.60 rows=1 width=288)
         Filter: (indexname = 'pg_am_name_index'::name)
         CTE unfiltered
           ->  Hash Left Join  (cost=230.08..584.45 rows=140 width=260)  
      ...
      */
    • các nguồn khác với các cuộc thảo luận, v.v.: https://blog.2ndquadrant.com/postgresql-ctes-are-optimization-fences/

  • các chức năng cửa sổ với over-statements có khả năng không sử dụng được (thường được sử dụng trong các chế độ xem, ví dụ như là một nguồn cho các báo cáo dựa trên các truy vấn phức tạp hơn)


cách giải quyết của chúng tôi cho các điều withkhoản

Chúng tôi sẽ chuyển đổi tất cả "chế độ xem nội tuyến" thành chế độ xem thực với tiền tố đặc biệt để chúng không làm rối danh sách / không gian tên của chế độ xem và có thể dễ dàng liên quan đến "chế độ xem bên ngoài" ban đầu: - /


giải pháp của chúng tôi cho các chức năng cửa sổ

Chúng tôi đã thực hiện thành công khi sử dụng cơ sở dữ liệu Oracle.


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.