web-dev-qa-db-ja.com

ビューはPostgreSQLのパフォーマンスに有害ですか?

以下は、db設計に関する本(データベース設計ISBNの開始:0-7645-7490-6)からの抜粋です。

ビューを使用することの危険性は、非常に大きなテーブルの非常に小さな部分を読み取ることを期待して、ビューに対してクエリをフィルタリングすることです。ビュー内のクエリの実行が完了すると、ビュー自体に対するフィルタリングが適用されるため、ビュー内でフィルタリングを行う必要があります。ビューは通常、開発プロセスをスピードアップするのに役立ちますが、長期的にはデータベースのパフォーマンスを完全に停止させる可能性があります。

以下は、PostgreSQL 9.5ドキュメントからの抜粋です。

ビューを自由に使用することは、優れたSQLデータベース設計の重要な側面です。ビューを使用すると、テーブルの構造の詳細をカプセル化できます。これは、一貫したインターフェイスの背後で、アプリケーションの進化に伴って変化する可能性があります。

2つのソースは互いに矛盾しているようです(「ビューを使用して設計しない」と「ビューを使用して設計する」)。

ただし、PGではビューはルールシステムを使用して実装されます。したがって、おそらく(これが私の質問です)、ビューに対するフィルタリングはビュー内のフィルターとして書き換えられ、基になるテーブルに対して単一のクエリが実行されます。

私の解釈は正しいですか、PGはWHERE句をビューの内外に組み合わせていますか?それとも、それらを次々に別々に実行しますか?短い、自己完結した、正しい(コンパイル可能な)例はありますか?

56
ARX

本は間違っています。

ビューからの選択はexactly基になるSQLステートメントを実行するのと同じくらい高速または低速– explain analyzeを使用して簡単に確認できます。

Postgresオプティマイザー(および他の多くの最新のDBMSのオプティマイザー)は、ビューの述語を実際のビューステートメントにプッシュダウンできます–これが単純なステートメントである場合(これもexplain analyzeを使用して確認できます)。

パフォーマンスに関する「悪い評判」は、ビューを使いすぎて、ビューを使用するビューを作成し始めたときからだと思います。その結果、非常に多くの場合、ビューなしで手動で調整されたステートメントと比較して、あまりにも多くのステートメントが実行されます。一部の中間テーブルは必要ないためです。ほとんどすべての場合、オプティマイザは、不要なテーブルや結合を削除したり、複数レベルのビューで述語をプッシュダウンしたりするほど賢くありません(これは他のDBMSにも当てはまります)。

64

あなたにあなたのを与えるために @ a_horseが説明した

Postgresは情報スキーマを実装します。これは(時々複雑)viewsで構成され、DBオブジェクトに関する情報を提供します標準化された形式。これは便利で信頼性が高く、Postgresカタログテーブルに直接アクセスするよりも大幅にコストがかかる可能性があります。

テーブルのすべての表示列を取得するための非常に単純な例
...情報スキーマから:

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

...システムカタログから:

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

両方のクエリプランと実行時間をEXPLAIN ANALYZEで比較します。

  • 最初のクエリはビューinformation_schema.columnsに基づいています。これは、このためにまったく必要のない複数のテーブルに結合します。

  • 2番目のクエリはoneテーブルpg_catalog.pg_attributeのみをスキャンするため、はるかに高速です。 (しかし、最初のクエリはまだ一般的なDBで数ミリ秒しか必要としません。)

詳細:

21

編集:

謝罪して、私は受け入れられた答えが常に正しいとは限らないという私の主張を撤回する必要があります-それはビューが常にサブクエリとして書かれたものと常に同一であると述べています。それは議論の余地のないことだと思います、そして私は今私の事件で何が起こっているかを知っていると思います。

私はまた、元の質問に対するより良い答えがあると思います。

元の質問は、ビューを使用することを実践の指針とすべきかどうかについてです(たとえば、2回以上維持する必要があるルーチンでSQLを繰り返すのではなく)。

私の答えは、「クエリがウィンドウ関数などを使用していて、オプティマイザがクエリをサブクエリになるときに別の方法で処理する場合は、ビューとして表されているかどうかに関係なく、サブクエリを作成する行為自体がパフォーマンスを低下させる可能性があるためです。実行時にパラメータでフィルタリングする場合。

ウィンドウ関数の複雑さは不要です。このための説明計画:

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

これよりもはるかに安価です:

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

それがもう少し具体的で役立つことを願っています。

私の最近の経験(私にこの質問を見つけさせた)で、上記の受け入れられた答えはすべての状況下では正しくありません。ウィンドウ関数を含む比較的単純なクエリがあります。

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

このフィルターを追加すると:

where Assembly_key = '185132'

私が得る説明計画は次のとおりです:

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

これは、train serviceテーブルの主キーインデックスと、partition_consistテーブルの一意でないインデックスを使用しています。 90msで実行されます。

私はビューを作成しました(完全に明確になるようにここに貼り付けますが、それは文字通りビュー内のクエリです):

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

同じビューでこのビューをクエリすると:

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

これは説明プランです:

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)

これは両方のテーブルでフルスキャンを実行しており、17秒かかります。

私がこれに遭遇するまで、私はPostgreSQLで自由にビューを使用していました(広く受け入れられているビューが受け入れられた回答で表現されていることを理解していました)。集合前のフィルタリングが必要な場合は、特にビューの使用を避けます。そのために、集合を返す関数を使用します。

また、PostgreSQLのCTEは設計により厳密に個別に評価されるため、SQL Serverのように、たとえばサブクエリとして最適化されているように見えるように使用することはありません。

したがって、私の答えは、ビューが基になっているクエリとまったく同じようにビューを実行しない場合があるため、注意が必要です。 PostgreSQL 9.6.6ベースのAmazon Auroraを使用しています。

8
enjayaitch

(私はビューの大ファンですが、ここではPGに非常に注意する必要があります一般的にPGでもビューを使用するようにみんなに勧めたいクエリ/コードの理解と保守性を向上させるために)

実際にそして悲しいことに(警告:) Postgresでビューを使用すると、実際に問題が発生し、その内部で使用していた機能によってはパフォーマンスが大幅に低下しました: (少なくともv10.1では)(これは、Oracleなどの他の最新のDBシステムではそうではありません。

したがって、おそらく(これは私の質問です)ビューに対する任意のフィルタリング...基になるテーブルに対して単一のクエリが実行されます。

(あなたが正確に何を意味するかによって-いいえ-中間の一時テーブルが望ましくないかもしれない、または述語が押し下げられない場所で具体化されるかもしれません...)

私は少なくとも2つの主要な「機能」を知っています。それはお知らせくださいOracleからPostgresへの移行の真ん中にあるため、プロジェクトでPGを放棄する必要がありました。

  • CTEswith- clause subqueries/---(common table expression )は(通常)より複雑なクエリ(小さいアプリケーションでも)を構造化するのに便利ですが、PGでも"hidden"オプティマイザヒント(たとえば、インデックス付けされていない一時テーブルを生成する)として実装されているため、violate(私や他の多くの人にとって重要)宣言SQLの概念Oracle doc ):例.

    • 簡単なクエリ:

      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)
        ...
      */
      
    • いくつかの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)  
      ...
      */
      
    • 議論などのさらなる情報源: https://blog.2ndquadrant.com/postgresql-ctes-are-optimization-fences/

  • ウィンドウ関数 with over- statements are potently unusable (通常、ビューで使用されます。たとえば、より多くに基づくレポートのソースとして複雑なクエリ)


with節の回避策

すべての「インラインビュー」を実際のビューに特別なプレフィックスを付けて変換します。これにより、ビューのリスト/名前空間が台無しにならず、元の「外部ビュー」に簡単に関連付けることができます。


ウィンドウ関数のソリューション

Oracleデータベースを使用して正常に実装しました。

1