Le viste sono dannose per le prestazioni in PostgreSQL?


45

Quello che segue è un estratto da un libro sulla progettazione di database (ISBN di progettazione del database iniziale: 0-7645-7490-6):

Il pericolo legato all'utilizzo delle viste è il filtraggio di una query in base a una vista, prevedendo di leggere una porzione molto piccola di una tabella molto grande. Qualsiasi filtro deve essere eseguito all'interno della vista perché qualsiasi filtro sulla vista stessa viene applicato dopo che la query nella vista ha completato l'esecuzione. Le viste sono in genere utili per accelerare il processo di sviluppo, ma a lungo termine possono uccidere completamente le prestazioni del database.

Quello che segue è un estratto della documentazione PostgreSQL 9.5:

Fare un uso liberale delle viste è un aspetto chiave della buona progettazione del database SQL. Le viste consentono di incapsulare i dettagli della struttura delle tabelle, che potrebbero cambiare con l'evoluzione dell'applicazione, dietro interfacce coerenti.

Le due fonti sembrano contraddirsi a vicenda ("non progettare con le viste" vs. "progettare con le viste").

Tuttavia, nelle viste PG vengono implementate utilizzando il sistema di regole. Quindi, possibilmente (e questa è la mia domanda) qualsiasi filtro sulla vista viene riscritto come filtro all'interno della vista, risultante in una singola esecuzione della query rispetto alle tabelle sottostanti.

La mia interpretazione è corretta e PG combina clausole WHERE dentro e fuori dalla vista? O li esegue separatamente, uno dopo l'altro? Qualche esempio breve, autonomo, corretto (compilabile)?


Penso che la domanda non sia giusta perché entrambe le fonti non parlano della stessa cosa. Il primo è correlato alla query da una vista e DOPO applicare un filtro: SELECT * FROM my_view WHERE my_column = 'blablabla';mentre il secondo riguarda l'uso delle viste per rendere trasparente il modello di dati all'applicazione che lo utilizza. Le prime fonti indicano di includere il filtro WHERE my_column = 'blablabla'all'interno della definizione della vista, in quanto ciò risulta in un piano di esecuzione migliore.
EAmez,

Risposte:


51

Il libro è sbagliato

La selezione da una vista è esattamente veloce o lenta come l'esecuzione dell'istruzione SQL sottostante: puoi facilmente verificarla utilizzando explain analyze.

L'ottimizzatore Postgres (e l'ottimizzatore per molti altri DBMS moderni) sarà in grado di trasferire i predicati sulla vista nell'istruzione di visualizzazione effettiva, a condizione che questa sia un'istruzione semplice (di nuovo, può essere verificata utilizzando explain analyze).

La "cattiva reputazione" per quanto riguarda le prestazioni deriva - penso - da quando si abusano delle viste e si inizia a creare viste che utilizzano viste che usano viste. Molto spesso ciò si traduce in affermazioni che fanno troppo rispetto a un'istruzione che è stata fatta su misura a mano senza le viste, ad esempio perché alcune tabelle intermedie non sarebbero necessarie. In quasi tutti i casi, l'ottimizzatore non è abbastanza intelligente da rimuovere quelle tabelle / join non necessari o per spingere verso il basso i predicati su più livelli di viste (questo vale anche per altri DBMS).


3
Date alcune delle contro-risposte proposte, potresti voler spiegare un po 'ciò che è una semplice affermazione .
RDFozz,

Puoi spiegare come usare la explain analyzedichiarazione?
Dustin Michels,

@DustinMichels: dai un'occhiata al manuale: postgresql.org/docs/current/using-explain.html
a_horse_with_no_name

19

Per darti un esempio di ciò che ha spiegato @a_horse :

Postgres implementa lo schema informativo, che consiste in viste (a volte complesse) che forniscono informazioni sugli oggetti DB in forma standardizzata. Ciò è conveniente e affidabile e può essere sostanzialmente più costoso dell'accesso diretto alle tabelle del catalogo Postgres.

Esempio molto semplice, per ottenere tutte le colonne visibili di una tabella
... dallo schema informativo:

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

... dal catalogo di sistema:

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

Confronta i piani di query e i tempi di esecuzione per entrambi con EXPLAIN ANALYZE.

  • La prima query si basa sulla vista information_schema.columns, che si unisce a più tabelle di cui non abbiamo assolutamente bisogno.

  • La seconda query analizza solo una tabella pg_catalog.pg_attribute, quindi molto più velocemente. (Ma la prima query richiede ancora solo pochi ms nei DB comuni.)

Dettagli:


7

MODIFICARE:

Con scuse, ho bisogno di ritirare la mia affermazione che la risposta accettata non è sempre corretta - afferma che la vista è sempre identica alla stessa cosa scritta come una sottoquery. Penso che sia indiscutibile, e ora penso di sapere cosa sta succedendo nel mio caso.

Ora penso anche che ci sia una risposta migliore alla domanda originale.

La domanda originale è se dovrebbe essere la guida pratica ad usare le viste (al contrario, ad esempio, di ripetere SQL nelle routine che potrebbero dover essere mantenute due o più volte).

La mia risposta sarebbe "no se la tua query utilizza funzioni di finestra o qualsiasi altra cosa che induca l'ottimizzatore a trattare la query in modo diverso quando diventa una sottoquery, perché l'atto stesso di creare la sottoquery (rappresentata come vista o meno) può ridurre le prestazioni se si sta filtrando con parametri in fase di esecuzione.

La complessità della mia funzione finestra non è necessaria. Il piano esplicativo per questo:

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

è molto meno costoso rispetto a questo:

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

Spero che sia un po 'più specifico e utile.

Nella mia recente esperienza (che mi ha fatto trovare questa domanda), la risposta accettata sopra non è corretta in tutte le circostanze. Ho una query relativamente semplice che include una funzione di finestra:

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

Se aggiungo questo filtro:

where assembly_key = '185132'

Il piano esplicativo che ottengo è il seguente:

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

Questo utilizza l'indice della chiave primaria nella tabella dei servizi del treno e un indice non univoco nella tabella porzione_consist. Si esegue in 90ms.

Ho creato una vista (incollandola qui per essere assolutamente chiara ma è letteralmente la query in una vista):

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

Quando eseguo una query su questa vista con lo stesso filtro:

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

Questo è il piano esplicativo:

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)

Questo esegue scansioni complete su entrambi i tavoli e richiede 17 secondi.

Fino a quando non mi sono imbattuto in questo, ho usato liberamente le opinioni con PostgreSQL (avendo compreso le opinioni ampiamente espresse nella risposta accettata). In particolare, eviterei di usare le viste se avessi bisogno di un filtro pre-aggregato, per il quale userei le funzioni di restituzione del set.

Sono anche consapevole che i CTE in PostgreSQL sono rigorosamente valutati separatamente, in base alla progettazione, quindi non li uso come farei con SQL Server, ad esempio, dove sembrano essere ottimizzati come sottoquery.

La mia risposta, quindi, è che ci sono casi in cui le viste non funzionano esattamente come la query su cui sono basate, quindi si consiglia cautela. Sto usando Amazon Aurora basato su PostgreSQL 9.6.6.


2
Nota l'avvertenza nell'altra risposta - " purché questa sia una semplice affermazione ".
RDFozz,

Come nota CASE WHEN (NOT ts.primary_direction) THEN '-1' :: INTEGER ELSE 1 ENDa margine , sarà inutile rendere la query più lenta del necessario, quindi è meglio scrivere altri due condizionali nell'ordine.
Evan Carroll,

@EvanCarroll Ho lottato con questo per un po '. Ho appena scoperto che è leggermente più veloce estrarre il CASO di un livello: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 l'

Neanche questa è una buona idea .. qui hai qualche problema. Voglio dire, quello grande è che la tua visione non ha davvero senso e fa cose diverse a causa del tuo uso, dense_rank()quindi non è davvero un problema di prestazioni.
Evan Carroll,

1
@EvanCarroll il tuo commento mi ha spinto ad arrivarci da solo (da qui la mia risposta modificata). Grazie.
enjayaitch,

0

(Sono un grande fan delle visualizzazioni, ma devi stare molto attento con PG qui e vorrei incoraggiare tutti a utilizzare le visualizzazioni in generale anche in PG per una migliore comprensibilità e manutenibilità di query / codice)

In realtà e purtroppo (ATTENZIONE :) l' uso delle viste in Postgres ci ha causato problemi reali e ha notevolmente ridotto le nostre prestazioni a seconda delle funzionalità che usavamo al suo interno :-( (almeno con v10.1). (Questo non sarebbe così con altri moderni sistemi DB come Oracle.)

Quindi, possibilmente (e questa è la mia domanda) qualsiasi filtro sulla vista ... risultante in una singola esecuzione della query sulle tabelle sottostanti.

(A seconda di cosa intendi esattamente - no - le tabelle temporanee intermedie potrebbero essere materializzate che potresti non voler essere o dove i predicati non vengono spinti verso il basso ...)

Conosco almeno due importanti "caratteristiche", che ci hanno deluso durante le migrazioni da Oracle a Postgres, quindi abbiamo dovuto abbandonare PG in un progetto:

  • I CTE ( withsottoquery -clause / espressioni di tabella comuni ) sono (di solito) utili per strutturare query più complesse (anche in applicazioni più piccole), ma in PG sono progettate implementate come suggerimenti di ottimizzazione "nascosti" (generando ad esempio tabelle temporanee non indicizzate) e quindi violare il concetto (per me e molti altri importanti) di SQL dichiarativo ( Oracle docu ): ad es

    • query semplice:

      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)
        ...
      */
      
    • riscritto usando alcuni 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)  
      ...
      */
      
    • ulteriori fonti con discussioni ecc .: https://blog.2ndquadrant.com/postgresql-ctes-are-optimization-fences/

  • le funzioni della finestra con over-statements sono potenzialmente inutilizzabili (generalmente utilizzate nelle viste, ad es. come fonte per report basati su query più complesse)


la nostra soluzione alternativa per le withclausole -clause

Trasformeremo tutte le "viste incorporate" in viste reali con un prefisso speciale in modo che non rovinino l'elenco / spazio dei nomi delle viste e possano essere facilmente correlate alla "vista esterna" originale: - /


la nostra soluzione per le funzioni della finestra

Lo abbiamo implementato con successo utilizzando il database Oracle.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.