web-dev-qa-db-fra.com

Les vues nuisent-elles aux performances dans PostgreSQL?

Ce qui suit est un extrait d'un livre sur db design (Beginning Database Design ISBN: 0-7645-7490-6):

Le danger avec l'utilisation des vues est de filtrer une requête par rapport à une vue, en s'attendant à lire une très petite partie d'une très grande table. Tout filtrage doit être effectué dans la vue car tout filtrage par rapport à la vue elle-même est appliqué une fois l'exécution de la requête dans la vue terminée. Les vues sont généralement utiles pour accélérer le processus de développement, mais à long terme, elles peuvent tuer complètement les performances de la base de données.

Ce qui suit est un extrait de la documentation PostgreSQL 9.5:

L'utilisation libérale des vues est un aspect clé d'une bonne conception de base de données SQL. Les vues vous permettent d'encapsuler les détails de la structure de vos tables, qui peuvent changer à mesure que votre application évolue, derrière des interfaces cohérentes.

Les deux sources semblent se contredire ("ne pas concevoir avec des vues" vs "faire la conception avec des vues").

Cependant, dans PG, les vues sont implémentées à l'aide du système de règles. Ainsi, il est possible (et c'est ma question) que tout filtrage par rapport à la vue soit réécrit en tant que filtre dans la vue, ce qui entraîne une exécution de requête unique sur les tables sous-jacentes.

Mon interprétation est-elle correcte et PG combine les clauses WHERE dans et hors de la vue? Ou les exécute-t-il séparément, l'un après l'autre? Des exemples courts, autonomes, corrects (compilables)?

56
ARX

Le livre est faux.

La sélection dans une vue est exactement aussi rapide ou lente que l'exécution de l'instruction SQL sous-jacente - vous pouvez facilement vérifier qu'en utilisant explain analyze.

L'optimiseur Postgres (et l'optimiseur pour de nombreux autres SGBD modernes) pourra pousser les prédicats de la vue dans la déclaration de vue réelle - à condition qu'il s'agisse d'une simple déclaration (là encore, cela peut être vérifié à l'aide de explain analyze).

La "mauvaise réputation" en matière de performances découle - je pense - de la surutilisation des vues et de la création de vues qui utilisent des vues qui utilisent des vues. Très souvent, cela se traduit par des déclarations qui en font trop par rapport à une déclaration qui a été personnalisée à la main sans les vues, par exemple car certaines tables intermédiaires ne seraient pas nécessaires. Dans presque tous les cas, l'optimiseur n'est pas assez intelligent pour supprimer ces tables/jointures inutiles ou pour pousser les prédicats sur plusieurs niveaux de vues (cela est également vrai pour les autres SGBD).

64

Pour vous donner un exemple de ce que @ a_horse expliqué :

Postgres implémente le schéma d'information, qui consiste en (parfois complexe) vues fournissant des informations sur les objets DB sous une forme standardisée. C'est pratique et fiable - et peut être beaucoup plus cher que d'accéder directement aux tables du catalogue Postgres.

Exemple très simple, pour obtenir toutes les colonnes visibles d'une table
... du schéma d'information:

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

... du catalogue système:

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

Comparez les plans de requête et le temps d'exécution pour les deux avec EXPLAIN ANALYZE.

  • La première requête est basée sur la vue information_schema.columns, qui se joint à plusieurs tables dont nous n'avons pas du tout besoin.

  • La deuxième requête analyse uniquement la table unepg_catalog.pg_attribute, donc beaucoup plus rapide. (Mais la première requête n'a encore besoin que de quelques ms dans les bases de données courantes.)

Détails:

21

ÉDITER:

Excusez-moi, je dois retirer mon affirmation selon laquelle la réponse acceptée n'est pas toujours correcte - elle indique que la vue est toujours identique à la même chose écrite comme une sous-requête. Je pense que c'est incontestable, et je pense que je sais maintenant ce qui se passe dans mon cas.

Je pense aussi maintenant qu'il y a une meilleure réponse à la question d'origine.

La question initiale est de savoir s'il devrait être une pratique directrice d'utiliser des vues (par opposition à, par exemple, répéter SQL dans des routines qui peuvent devoir être maintenues deux fois ou plus).

Ma réponse serait "pas si votre requête utilise des fonctions de fenêtre ou toute autre chose qui amène l'optimiseur à traiter la requête différemment lorsqu'elle devient une sous-requête, car le fait même de créer la sous-requête (qu'elle soit représentée comme une vue ou non) peut dégrader les performances si vous filtrez avec des paramètres lors de l'exécution.

La complexité de ma fonction de fenêtre n'est pas nécessaire. Le plan d'explication pour cela:

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

est beaucoup moins coûteux que pour cela:

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

J'espère que c'est un peu plus précis et utile.

Dans mon expérience récente (m'amenant à trouver cette question), la réponse acceptée ci-dessus n'est pas correcte dans tous les cas. J'ai une requête relativement simple qui inclut une fonction de fenêtre:

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

Si j'ajoute ce filtre:

where Assembly_key = '185132'

Le plan d'explication que je reçois est le suivant:

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

Cela utilise l'index de clé primaire sur la table de service de train et un index non unique sur la table portion_consist. Il s'exécute en 90ms.

J'ai créé une vue (en la collant ici pour être absolument claire, mais c'est littéralement la requête dans une vue):

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

Lorsque je recherche cette vue avec le filtre identique:

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

Voici le plan d'explication:

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)

Cela effectue des analyses complètes sur les deux tables et prend 17 secondes.

Jusqu'à ce que je tombe sur ce point, j'ai utilisé généreusement les vues avec PostgreSQL (après avoir compris les vues largement exprimées dans la réponse acceptée). J'éviterais spécifiquement d'utiliser des vues si j'ai besoin d'un filtrage pré-agrégé, pour lequel j'utiliserais des fonctions de retour d'ensemble.

Je suis également conscient que les CTE dans PostgreSQL sont strictement évalués séparément, par conception, donc je ne les utilise pas de la même manière que je le ferais avec SQL Server, par exemple, où ils semblent être optimisés en tant que sous-requêtes.

Par conséquent, ma réponse est qu'il existe des cas dans lesquels les vues ne fonctionnent pas exactement comme la requête sur laquelle elles sont basées, il est donc conseillé d'être prudent. J'utilise Amazon Aurora basé sur PostgreSQL 9.6.6.

8
enjayaitch

(Je suis un grand fan de vues, mais vous devez être très prudent avec PG ici et Je voudrais encourager tout le monde à utiliser des vues généralement aussi dans PG pour une meilleure compréhension et maintenabilité des requêtes/code)

En fait et malheureusement (AVERTISSEMENT :) l'utilisation des vues dans Postgres nous a causé de vrais problèmes et a considérablement diminué nos performances en fonction des fonctionnalités que nous utilisions à l'intérieur: (au moins avec la v10.1). (Ce ne serait pas le cas avec d'autres systèmes DB modernes comme Oracle.)

Donc, éventuellement (et c'est ma question) tout filtrage par rapport à la vue ... résultant en une seule exécution de requête sur les tables sous-jacentes.

(Selon ce que vous voulez dire exactement - non - des tables de températures intermédiaires peuvent se matérialiser que vous ne voulez peut-être pas ou où les prédicats ne sont pas poussés vers le bas ...)

Je connais au moins deux "fonctionnalités" majeures, qui nous ont laissé tomber au milieu de migrations d'Oracle vers Postgres nous avons donc dû abandonner PG dans un projet:

  • Les CTE (with- sous-requêtes de clause/--- (expressions de table communes ) sont (généralement) utiles pour la structuration plus complexe les requêtes (même dans les applications plus petites), mais dans PG sont par conception implémentées comme optimiseurs "cachés" indices (générant par exemple des tables temporaires non indexées) et donc violer le concept (pour moi et beaucoup d'autres important) de SQL déclaratif ( Oracle doc ): par exemple.

    • requête simple:

      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)
        ...
      */
      
    • réécrit en utilisant du 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)  
      ...
      */
      
    • d'autres sources avec discussions, etc.: https://blog.2ndquadrant.com/postgresql-ctes-are-optimization-fences/

  • les fonctions de la fenêtre avec les instructions over- sont potentiellement inutilisables (généralement utilisé dans les vues, par exemple comme source de rapports basés sur des requêtes plus complexes)


notre solution de contournement pour les clauses with-

Nous transformerons toutes les "vues en ligne" en vues réelles avec un préfixe spécial afin qu'elles ne gâchent pas la liste/l'espace de noms des vues et puissent facilement être liées à la "vue extérieure" d'origine: - /


notre solution pour les fonctions de fenêtre

Nous l'avons implémenté avec succès en utilisant la base de données Oracle.

1
Andreas Dietrich