Beeinträchtigen Views die Performance in PostgreSQL?

45

Das Folgende ist ein Auszug aus einem Buch über Datenbankdesign (Beginning Database Design ISBN: 0-7645-7490-6):

Die Gefahr bei der Verwendung von Ansichten besteht darin, dass eine Abfrage nach einer Ansicht gefiltert wird und erwartet wird, dass ein sehr kleiner Teil einer sehr großen Tabelle gelesen wird. Jede Filterung sollte in der Ansicht durchgeführt werden, da jede Filterung für die Ansicht selbst angewendet wird, nachdem die Abfrage in der Ansicht die Ausführung abgeschlossen hat. Ansichten sind in der Regel nützlich, um den Entwicklungsprozess zu beschleunigen, können jedoch auf lange Sicht die Datenbankleistung vollständig beeinträchtigen.

Das Folgende ist ein Auszug aus der PostgreSQL 9.5-Dokumentation:

Die freie Verwendung von Ansichten ist ein Schlüsselaspekt für ein gutes SQL-Datenbankdesign. Mithilfe von Ansichten können Sie die Details der Struktur Ihrer Tabellen, die sich bei der Entwicklung Ihrer Anwendung ändern können, hinter konsistenten Schnittstellen zusammenfassen.

Die beiden Quellen scheinen sich zu widersprechen ("Nicht mit Ansichten gestalten" vs. "Mit Ansichten gestalten").

In PG werden Views jedoch über das Regelsystem implementiert. Daher wird möglicherweise (und das ist meine Frage) jede Filterung für die Ansicht als Filter in der Ansicht umgeschrieben, was zu einer einzelnen Abfrageausführung für die zugrunde liegenden Tabellen führt.

Ist meine Interpretation korrekt und kombiniert PG WHERE-Klauseln in und aus der Ansicht? Oder werden sie einzeln nacheinander ausgeführt? Irgendwelche kurzen, eigenständigen, korrekten (kompilierbaren) Beispiele?

ARX
quelle
Ich denke, die Frage ist nicht richtig, weil beide Quellen nicht über dasselbe reden. Die erste bezieht sich auf die Abfrage aus einer Ansicht und SELECT * FROM my_view WHERE my_column = 'blablabla';danach auf das Anwenden eines Filters: In der zweiten werden Ansichten verwendet, um das Datenmodell für die Anwendung, die es verwendet, transparent zu machen. Die erste Quelle weist Sie darauf hin, den Filter WHERE my_column = 'blablabla'in die Ansichtsdefinition aufzunehmen, da dies zu einem besseren Ausführungsplan führt.
EAmez,

Antworten:

49

Das Buch ist falsch.

Das Auswählen aus einer Ansicht ist genauso schnell oder langsam wie das Ausführen der zugrunde liegenden SQL-Anweisung - Sie können dies ganz einfach mit überprüfen explain analyze.

Der Postgres-Optimierer (und der Optimierer für viele andere moderne DBMS) kann Prädikate für die Ansicht in die tatsächliche Ansichtsanweisung verschieben - vorausgesetzt, es handelt sich um eine einfache Anweisung (dies kann wiederum mit überprüft werden explain analyze).

Der "schlechte Ruf" in Bezug auf die Leistung entsteht - glaube ich -, wenn Sie Ansichten überbeanspruchen und damit beginnen, Ansichten zu erstellen, die Ansichten verwenden, die Ansichten verwenden. Sehr oft führt dies zu Anweisungen, die im Vergleich zu einer Anweisung, die ohne die Ansichten von Hand erstellt wurde, zu viel bewirken, z. B. weil einige Zwischentabellen nicht benötigt würden. In fast allen Fällen ist der Optimierer nicht intelligent genug, um diese nicht benötigten Tabellen / Joins zu entfernen oder Prädikate über mehrere Ansichtsebenen hinweg nach unten zu verschieben (dies gilt auch für andere DBMS).

ein Pferd ohne Name
quelle
3
In Anbetracht einiger der vorgeschlagenen Gegenantworten möchten Sie möglicherweise ein wenig auf eine einfache Aussage eingehen .
RDFozz
Können Sie erklären, wie die explain analyzeAnweisung verwendet wird?
Dustin Michels
@DustinMichels: Schauen Sie sich das Handbuch an: postgresql.org/docs/current/using-explain.html
a_horse_with_no_name
19

Um Ihnen ein Beispiel zu geben, was @a_horse erklärt hat :

Postgres implementiert das Informationsschema, das aus (manchmal komplexen) Ansichten besteht, die Informationen zu DB-Objekten in standardisierter Form bereitstellen. Dies ist bequem und zuverlässig - und kann erheblich teurer sein als der direkte Zugriff auf die Postgres-Katalogtabellen.

Sehr einfaches Beispiel, um alle sichtbaren Spalten einer Tabelle
... aus dem Informationsschema zu erhalten:

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

... aus dem Systemkatalog:

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

Vergleichen Sie die Abfragepläne und die Ausführungszeit für beide mit EXPLAIN ANALYZE.

  • Die erste Abfrage basiert auf der Ansicht information_schema.columns, die sich zu mehreren Tabellen zusammenfügt, die wir hierfür überhaupt nicht benötigen.

  • Die zweite Abfrage durchsucht nur die eine Tabelle pg_catalog.pg_attribute, daher viel schneller. (Die erste Abfrage benötigt jedoch nur wenige ms in gemeinsamen DBs.)

Einzelheiten:

Erwin Brandstetter
quelle
7

BEARBEITEN:

Mit Entschuldigung muss ich meine Behauptung zurückziehen, dass die akzeptierte Antwort nicht immer korrekt ist - es heißt, dass die Ansicht immer mit der gleichen Sache identisch ist, die als Unterabfrage geschrieben wurde. Ich denke, das ist unbestreitbar, und ich glaube jetzt zu wissen, was in meinem Fall vor sich geht.

Ich denke jetzt auch, dass es eine bessere Antwort auf die ursprüngliche Frage gibt.

Die ursprüngliche Frage ist, ob die Verwendung von Ansichten als Richtschnur dienen sollte (im Gegensatz zum Beispiel zum Wiederholen von SQL in Routinen, die möglicherweise zweimal oder öfter verwaltet werden müssen).

Meine Antwort wäre "nicht, wenn Ihre Abfrage Fensterfunktionen oder andere Funktionen verwendet, die das Optimierungsprogramm veranlassen, die Abfrage anders zu behandeln, wenn sie zu einer Unterabfrage wird, da das Erstellen der Unterabfrage (unabhängig davon, ob sie als Ansicht dargestellt wird oder nicht) die Leistung beeinträchtigen kann Wenn Sie zur Laufzeit mit Parametern filtern.

Die Komplexität meiner Fensterfunktion ist unnötig. Der Erklärungsplan dazu:

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

ist viel günstiger als hierfür:

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

Hoffe, das ist ein bisschen genauer und hilfreich.

Nach meinen jüngsten Erfahrungen (die mich veranlassen, diese Frage zu finden) ist die oben akzeptierte Antwort nicht unter allen Umständen korrekt. Ich habe eine relativ einfache Abfrage, die eine Fensterfunktion enthält:

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

Wenn ich diesen Filter hinzufüge:

where assembly_key = '185132'

Der Erklärungsplan, den ich erhalte, lautet wie folgt:

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

Hierbei wird der Primärschlüsselindex für die Train Service-Tabelle und ein nicht eindeutiger Index für die portion_consist-Tabelle verwendet. Es wird in 90 ms ausgeführt.

Ich habe eine Ansicht erstellt (hier einfügen, um die Übersichtlichkeit zu verdeutlichen, aber es ist buchstäblich die Abfrage in einer Ansicht):

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

Wenn ich diese Ansicht mit dem identischen Filter abfrage:

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

Dies ist der EXPLAIN-Plan:

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)

Dies führt vollständige Scans an beiden Tischen durch und dauert 17 Sekunden.

Bis ich darauf stieß, habe ich Ansichten mit PostgreSQL großzügig genutzt (nachdem ich die weit verbreiteten Ansichten verstanden hatte, die in der akzeptierten Antwort zum Ausdruck kamen). Ich würde speziell die Verwendung von Ansichten vermeiden, wenn ich eine Vor-Aggregat-Filterung benötige, für die ich Set-Return-Funktionen verwenden würde.

Mir ist auch bewusst, dass CTEs in PostgreSQL strikt getrennt ausgewertet werden, sodass ich sie nicht so verwende, wie ich es zum Beispiel mit SQL Server tun würde, wo sie als Unterabfragen optimiert zu sein scheinen.

Meine Antwort lautet daher, dass es Fälle gibt, in denen Ansichten nicht genau so ausgeführt werden, wie die Abfrage, auf der sie basieren. Daher ist Vorsicht geboten. Ich benutze Amazon Aurora basierend auf PostgreSQL 9.6.6.

Enjayaitch
quelle
2
Beachten Sie die Einschränkung in der anderen Antwort - " vorausgesetzt, dies ist eine einfache Aussage ".
RDFozz
CASE WHEN (NOT ts.primary_direction) THEN '-1' :: INTEGER ELSE 1 ENDNebenbei bemerkt, wird die Abfrage unnötigerweise langsamer als nötig, und Sie sollten besser zwei weitere Bedingungen in der Reihenfolge schreiben.
Evan Carroll
@EvanCarroll Ich hatte eine Weile damit zu kämpfen. 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
Ich habe
Das ist auch keine gute Idee. Sie haben hier ein paar Probleme. Ich meine, das große dense_rank()Problem ist, dass Ihre Sichtweise keinen Sinn ergibt und aufgrund Ihrer Verwendung andere Dinge bewirkt, sodass es sich nicht wirklich um ein Leistungsproblem handelt.
Evan Carroll
1
@EvanCarroll Ihr Kommentar hat mich dazu veranlasst, selbst dorthin zu gelangen (daher meine bearbeitete Antwort). Danke.
Enjayaitch
0

(Ich bin ein großer Fan von Views, aber hier muss man sehr vorsichtig mit PG sein und ich möchte jeden ermutigen, Views generell auch in PG zu verwenden, um die Verständlichkeit und Wartbarkeit von Abfragen / Code zu verbessern.)

Tatsächlich und traurigerweise (WARNUNG :) verursachte die Verwendung von Ansichten in Postgres echte Probleme und verringerte unsere Leistung erheblich, abhängig von den Funktionen, die wir darin verwendeten :-( (zumindest mit v10.1). (Dies wäre bei anderen nicht der Fall.) moderne DB-Systeme wie Oracle.)

Also, möglicherweise (und das ist meine Frage) jede Filterung gegen die Ansicht ... was zu einer einzelnen Abfrage für die zugrunde liegenden Tabellen führt.

(Abhängig davon, was Sie genau meinen - no - intermediäre temporäre Tabellen können materialisiert werden, die Sie vielleicht nicht wollen oder auf die Prädikate nicht gedrückt werden ...)

Ich kenne mindestens zwei wichtige "Features", die uns mitten in der Migration von Oracle nach Postgres im Stich ließen, sodass wir PG in einem Projekt aufgeben mussten:

  • CTEs ( with-Klausel-Unterabfragen / allgemeine Tabellenausdrücke ) sind (normalerweise) nützlich für die Strukturierung komplexerer Abfragen (auch in kleineren Anwendungen), werden jedoch in PG standardmäßig als "versteckte" Optimierungshinweise implementiert (z. B. Generieren von nicht indizierten temporären Tabellen) und Verstoßen Sie damit gegen das (für mich und viele andere wichtige) Konzept des deklarativen SQL ( Oracle-Doku ): z

    • einfache Abfrage:

      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)
        ...
      */
    • umgeschrieben mit etwas 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)  
      ...
      */
    • Weitere Quellen mit Diskussionen etc .: https://blog.2ndquadrant.com/postgresql-ctes-are-optimization-fences/

  • Fensterfunktionen mit over-Anweisungen sind möglicherweise unbrauchbar (werden normalerweise in Ansichten verwendet, z. B. als Quelle für Berichte, die auf komplexeren Abfragen basieren).


unser Workaround für die with-Klauseln

Wir werden alle "Inline-Ansichten" in reale Ansichten mit einem speziellen Präfix umwandeln, damit sie die Liste / den Namespace der Ansichten nicht durcheinander bringen und leicht mit der ursprünglichen "Außenansicht" in Beziehung gesetzt werden können: - /


unsere lösung für die fensterfunktionen

Wir haben es erfolgreich mit der Oracle-Datenbank implementiert.

Andreas Dietrich
quelle