Czy widoki są szkodliwe dla wydajności w PostgreSQL?

45

Poniżej znajduje się fragment książki o projekcie db (Początkowy projekt bazy danych ISBN: 0-7645-7490-6):

Niebezpieczeństwo związane z używaniem widoków polega na filtrowaniu zapytania względem widoku, spodziewając się odczytania bardzo małej części bardzo dużej tabeli. Wszelkie filtrowanie powinno odbywać się w widoku, ponieważ wszelkie filtrowanie względem samego widoku jest stosowane po zakończeniu wykonywania zapytania w widoku. Widoki są zwykle przydatne do przyspieszenia procesu programowania, ale na dłuższą metę mogą całkowicie zabić wydajność bazy danych.

Oto fragment dokumentacji PostgreSQL 9.5:

Liberalne wykorzystanie widoków jest kluczowym aspektem dobrego projektu bazy danych SQL. Widoki pozwalają na zawarcie w szczegółach struktury tabel, które mogą się zmieniać w miarę ewolucji aplikacji, za spójnymi interfejsami.

Te dwa źródła wydają się ze sobą sprzeczne („nie projektuj z widokami” vs. „projektuj z widokami”).

Jednak w PG widoki są implementowane przy użyciu systemu reguł. Być może (i to jest moje pytanie) każde filtrowanie w stosunku do widoku jest przepisywane jako filtr w widoku, co powoduje wykonanie pojedynczego zapytania względem bazowych tabel.

Czy moja interpretacja jest poprawna, a PG łączy klauzule GDZIE w widoku i poza nim? Czy też uruchamia je osobno, jeden po drugim? Jakieś krótkie, samodzielne, poprawne (kompilowalne) przykłady?

ARX
źródło
Myślę, że pytanie jest niewłaściwe, ponieważ oba źródła nie mówią o tym samym. Pierwszy dotyczy zapytania z widoku i PO zastosowaniu filtra: SELECT * FROM my_view WHERE my_column = 'blablabla';Podczas gdy drugi dotyczy korzystania z widoków, aby model danych był przezroczysty dla aplikacji, która go używa. Pierwsze źródła wskazują, że należy uwzględnić filtr WHERE my_column = 'blablabla'w definicji widoku, ponieważ zapewnia to lepszy plan wykonania.
EAmez

Odpowiedzi:

49

Książka jest zła.

Wybieranie z widoku jest dokładnie tak szybkie lub wolne, jak uruchomienie bazowej instrukcji SQL - możesz to łatwo sprawdzić za pomocą explain analyze.

Optymalizator Postgres (i optymalizator dla wielu innych nowoczesnych DBMS) będzie w stanie przesuwać predykaty widoku do rzeczywistej instrukcji widoku - pod warunkiem, że jest to prosta instrukcja (ponownie, można to zweryfikować za pomocą explain analyze).

„Zła reputacja” związana z wydajnością wynika - jak sądzę - z momentu, gdy nadużywasz widoków i zaczynasz budować widoki, które wykorzystują widoki, które korzystają z widoków. Bardzo często skutkuje to stwierdzeniami, które robią za dużo w porównaniu do instrukcji, które zostały ręcznie dopasowane bez widoków, np. Ponieważ niektóre tabele pośrednie nie byłyby potrzebne. W prawie wszystkich przypadkach optymalizator nie jest wystarczająco inteligentny, aby usunąć niepotrzebne tabele / sprzężenia lub przesuwać predykaty na wielu poziomach widoków (dotyczy to również innych DBMS).

koń bez imienia
źródło
3
Biorąc pod uwagę niektóre z proponowanych odpowiedzi, możesz nieco wyjaśnić to, co jest prostym stwierdzeniem .
RDFozz
Czy możesz wyjaśnić, jak korzystać z tego explain analyzeoświadczenia?
Dustin Michels
@DustinMichels: zapoznaj się z instrukcją: postgresql.org/docs/current/using-explain.html
a_horse_with_no_name
19

Aby dać przykład , co @a_horse wyjaśnił :

Postgres implementuje schemat informacyjny, który składa się z (czasem skomplikowanych) widoków dostarczających informacji o obiektach DB w znormalizowanej formie. Jest to wygodne i niezawodne - i może być znacznie droższe niż bezpośredni dostęp do tabel katalogu Postgres.

Bardzo prosty przykład, aby uzyskać wszystkie widoczne kolumny tabeli
... ze schematu informacyjnego:

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

... z katalogu systemowego:

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

Porównaj plany zapytań i czas wykonania dla obu EXPLAIN ANALYZE.

  • Pierwsze zapytanie opiera się na widoku information_schema.columns, który łączy się z wieloma tabelami, których wcale nie potrzebujemy.

  • Drugie zapytanie skanuje tylko jedną tabelę pg_catalog.pg_attribute, a więc znacznie szybciej. (Ale pierwsze zapytanie nadal wymaga tylko kilku ms we wspólnych bazach danych).

Detale:

Erwin Brandstetter
źródło
7

EDYTOWAĆ:

Z przeprosinami muszę wycofać moje twierdzenie, że zaakceptowana odpowiedź nie zawsze jest poprawna - stwierdza, że ​​widok jest zawsze identyczny z tym samym, napisanym jako podzapytanie. Myślę, że to bezdyskusyjne i myślę, że teraz wiem, co się dzieje w moim przypadku.

Myślę też, że jest lepsza odpowiedź na pierwotne pytanie.

Pierwotne pytanie dotyczy tego, czy korzystanie z widoków powinno być przewodnią praktyką (w przeciwieństwie do na przykład powtarzania SQL w procedurach, które mogą wymagać utrzymania dwa lub więcej razy).

Moja odpowiedź brzmiałaby: „nie, jeśli zapytanie używa funkcji okna lub czegokolwiek innego, co powoduje, że optymalizator traktuje zapytanie inaczej, gdy staje się ono podzapytaniem, ponieważ sam akt tworzenia podzapytania (reprezentowanego jako widok lub nie) może obniżyć wydajność jeśli filtrujesz za pomocą parametrów w czasie wykonywania.

Złożoność mojej funkcji okna jest niepotrzebna. Wyjaśnij plan:

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

jest o wiele tańszy niż w tym przypadku:

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

Mam nadzieję, że jest to bardziej szczegółowe i pomocne.

Z mojego ostatniego doświadczenia (powodującego, że znalazłem to pytanie), powyższa zaakceptowana odpowiedź jest nieprawidłowa we wszystkich okolicznościach. Mam stosunkowo proste zapytanie, które obejmuje funkcję okna:

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

Jeśli dodam ten filtr:

where assembly_key = '185132'

Wyjaśniam plan, który otrzymuję:

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

Wykorzystuje się w tym indeks klucza podstawowego w tabeli obsługi pociągu i nieunikalny indeks w tabelipart_consist. Wykonuje się w 90ms.

Utworzyłem widok (wklejając go tutaj, aby był absolutnie jasny, ale dosłownie jest to zapytanie w widoku):

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

Kiedy pytam ten widok z identycznym filtrem:

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

Oto plan wyjaśniania:

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)

Robi to pełne skanowanie obu tabel i zajmuje 17 sekund.

Dopóki się z tym nie spotkałem, swobodnie korzystałem z widoków w PostgreSQL (po zrozumieniu szeroko rozpowszechnionych poglądów wyrażonych w zaakceptowanej odpowiedzi). W szczególności unikałbym korzystania z widoków, jeśli potrzebuję filtrowania wstępnego agregacji, dla którego używałbym funkcji zwracających zestaw.

Wiem również, że CTE w PostgreSQL są ściśle oceniane osobno, z założenia, więc nie używam ich w taki sam sposób, jak na przykład w SQL Server, gdzie wydają się być zoptymalizowane jako podkwerendy.

Moja odpowiedź brzmi zatem: są przypadki, w których widoki nie działają dokładnie tak, jak zapytanie, na którym są oparte, dlatego zaleca się ostrożność. Korzystam z Amazon Aurora opartej na PostgreSQL 9.6.6.

enjayaitch
źródło
2
Zwróć uwagę na zastrzeżenie w drugiej odpowiedzi - „ pod warunkiem, że jest to proste stwierdzenie ”.
RDFozz
Na marginesie, CASE WHEN (NOT ts.primary_direction) THEN '-1' :: INTEGER ELSE 1 ENDniepotrzebnie sprawi, że zapytanie będzie wolniejsze niż potrzeba , ponieważ lepiej jest napisać dwa kolejne warunki warunkowe w kolejności.
Evan Carroll,
@EvanCarroll Przez jakiś czas walczyłem z tym. Właśnie odkryłem, że wyciągnięcie CASE z jednego poziomu jest nieznacznie szybsze: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,
To też nie jest dobry pomysł ... masz tutaj kilka problemów. Chodzi mi o to, że twój duży pogląd nie ma sensu i robi różne rzeczy z powodu twojego użycia, dense_rank()więc nie jest to tak naprawdę problem z wydajnością.
Evan Carroll,
1
@EvanCarroll Twój komentarz zachęcił mnie, żebym się tam dostał (stąd moja zredagowana odpowiedź). Dziękuję Ci.
enjayaitch,
0

(Jestem wielkim fanem widoków, ale musisz być bardzo ostrożny z PG tutaj i chciałbym zachęcić wszystkich do korzystania z widoków ogólnie również w PG dla lepszej zrozumiałości i łatwości obsługi zapytań / kodu)

Właściwie i niestety (OSTRZEŻENIE :) używanie widoków w Postgresie spowodowało nam poważne problemy i znacznie obniżyło naszą wydajność w zależności od funkcji, których używaliśmy wewnątrz :-( (przynajmniej z wersją 10.1). (Nie byłoby tak w przypadku innych nowoczesne systemy DB, takie jak Oracle.)

Możliwe więc (i to jest moje pytanie) jakiekolwiek filtrowanie w stosunku do widoku ... powodujące wykonanie pojedynczego zapytania względem bazowych tabel.

(W zależności od tego, co dokładnie masz na myśli - nie - można zmaterializować pośrednie tabele temperatur, których możesz nie chcieć lub gdzie predykaty nie są spychane w dół ...)

Znam co najmniej dwie główne „funkcje”, które zawiodły nas w trakcie migracji z Oracle do Postgres, więc musieliśmy porzucić PG w projekcie:

  • CTE ( with-klauzulazy / wspólne wyrażenia tabelowe ) są (zwykle) przydatne do konstruowania bardziej złożonych zapytań (nawet w mniejszych aplikacjach), ale w PG są z założenia zaimplementowane jako „ukryte” wskazówki optymalizatora (generujące np. Nieindeksowane tabele temp) i dlatego naruszają (dla mnie i wielu innych ważnych) koncepcję deklaratywnego SQL ( Oracle docu ): np

    • proste zapytanie:

      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)
        ...
      */
    • przepisane przy użyciu 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)  
      ...
      */
    • dalsze źródła z dyskusjami itp .: https://blog.2ndquadrant.com/postgresql-ctes-are-optimization-fences/

  • funkcje okna z over-statements są potencjalnie bezużyteczne (zwykle używane w widokach, np. jako źródło raportów opartych na bardziej złożonych zapytaniach)


nasze obejście withklauzul

Przekształcimy wszystkie „widoki wbudowane” w prawdziwe widoki ze specjalnym prefiksem, aby nie zakłócały listy / przestrzeni nazw widoków i mogą być łatwo powiązane z oryginalnym „widokiem zewnętrznym”: - /


nasze rozwiązanie dla funkcji okna

Z powodzeniem zaimplementowaliśmy go przy użyciu bazy danych Oracle.

Andreas Dietrich
źródło