Les vues sont-elles nuisibles aux performances dans PostgreSQL?


45

Ce qui suit est un extrait d’un livre sur la conception de la base de données (ISBN: 0-7645-7490-6):

Le danger lié à l'utilisation de vues est le filtrage d'une requête par rapport à une vue, en s'attendant à lire une très petite partie d'un très grand tableau. Tout filtrage doit être effectué dans la vue, car il est appliqué après la fin de l'exécution de la requête dans la vue. Les vues sont généralement utiles pour accélérer le processus de développement, mais peuvent à long terme détruire complètement les performances de la base de données.

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

L'utilisation généralisée des vues est un aspect essentiel 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 en fonction de l'évolution de votre application, derrière des interfaces cohérentes.

Les deux sources semblent se contredire ("ne concevez pas avec des vues" ou "ne concevez avec des vues").

Cependant, dans les vues PG, elles sont implémentées à l'aide du système de règles. Donc, éventuellement (et c'est ma question), tout filtrage de la vue est réécrit en tant que filtre dans la vue, ce qui entraîne l'exécution d'une 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 est-ce qu'il les exécute séparément, l'un après l'autre? Des exemples courts, autonomes, corrects (compilables)?


Je pense que la question n'est pas correcte car les deux sources ne parlent pas de la même chose. La première concerne la requête dans une vue et APRÈS appliquer un filtre: SELECT * FROM my_view WHERE my_column = 'blablabla';.La seconde concerne l'utilisation des vues pour rendre votre modèle de données transparent pour l'application qui l'utilise. Les premières sources vous invitent à inclure le filtre WHERE my_column = 'blablabla'dans la définition de la vue, afin d’améliorer le plan d’exécution.
EAmez

Réponses:


51

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 cela à l'aide de explain analyze.

L'optimiseur Postgres (et l'optimiseur de nombreux autres SGBD modernes) sera capable d'insérer des prédicats de la vue dans la déclaration réelle, à condition qu'il s'agisse d'une simple déclaration (à nouveau, elle peut être vérifiée à l'aide de explain analyze).

Je pense que la "mauvaise réputation" en matière de performances découle du fait que vous abusez des vues et commencez à créer des vues qui utilisent des vues qui utilisent des vues. Très souvent, cela donne des déclarations qui en font trop par rapport à une déclaration qui a été personnalisée à la main sans les vues, par exemple parce que 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 abaisser les prédicats sur plusieurs niveaux de vues (cela s'applique également aux autres SGBD).


3
Compte tenu de certaines des réponses proposées, vous voudrez peut-être expliquer un peu ce qu’est une déclaration simple .
RDFozz

Pouvez-vous expliquer comment utiliser cette explain analyzedéclaration?
Dustin Michels


19

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

Postgres implémente le schéma d'information, constitué de vues (parfois complexes) fournissant des informations sur les objets de base de données sous une forme normalisée. C'est pratique et fiable - et peut coûter 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
... à partir du schéma d'information:

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

... depuis le 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 rejoint plusieurs tables dont nous n’avons pas besoin pour cela.

  • La deuxième requête scanne seulement un tableau pg_catalog.pg_attribute, donc beaucoup plus rapide. (Mais la première requête n'a toujours besoin que de quelques ms dans les bases de données communes.)

Détails:


7

MODIFIER:

Avec des excuses, 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 en tant que sous-requête. Je pense que c'est indiscutable et que je sais maintenant ce qui se passe dans mon cas.

Je pense aussi qu'il existe une meilleure réponse à la question initiale.

La question initiale était de savoir s'il fallait guider l'utilisation de vues (par exemple, la répétition de SQL dans des routines pouvant nécessiter une maintenance deux fois ou plus).

Ma réponse serait "pas si votre requête utilise des fonctions de fenêtre ou quoi que ce soit qui amène l'optimiseur à traiter la requête différemment lorsqu'elle devient une sous-requête, car le simple fait de créer la sous-requête (représentée ou non sous forme de vue) risque de dégrader les performances. si vous filtrez avec des paramètres au moment de l'exécution.

La complexité de ma fonction de fenêtre est inutile. Le plan d'explication pour ceci:

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 (qui m'a amené à trouver cette question), la réponse acceptée ci-dessus n'est pas correcte dans toutes les circonscriptions. 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 j’obtiens 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 (la coller 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 j'interroge cette vue avec le filtre identique:

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

C'est le plan d'expliquer:

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 trouve cela, j'utilisais généreusement des vues avec PostgreSQL (ayant compris les vues largement partagées exprimées dans la réponse acceptée). J'éviterais spécifiquement d'utiliser des vues si j'avais besoin d'un filtrage pré-agrégé, pour lequel j'utiliserais des fonctions de renvoi de jeu.

Je suis également conscient du fait que les CTE de PostgreSQL sont évalués séparément, de par leur conception. Je ne les utilise donc pas comme je le ferais avec SQL Server, par exemple, où ils semblent être optimisés en tant que sous-requêtes.

Ma réponse est donc la suivante: dans certains cas, les vues ne fonctionnent pas exactement comme la requête sur laquelle elles sont basées. Il est donc conseillé de faire preuve de prudence. J'utilise Amazon Aurora basé sur PostgreSQL 9.6.6.


2
Notez la mise en garde dans l'autre réponse - "à condition qu'il s'agisse d'une simple déclaration ".
RDFozz

En guise de remarque, il CASE WHEN (NOT ts.primary_direction) THEN '-1' :: INTEGER ELSE 1 ENDsera inutile de ralentir la requête plus que nécessaire, il est préférable d'écrire deux autres conditions dans la commande.
Evan Carroll

@EvanCarroll J'ai eu du mal avec cela pendant un moment. Je viens de trouver qu'il est légèrement plus rapide de tirer le CASE d'un niveau: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

Ce n'est pas une bonne idée non plus… vous avez quelques problèmes ici. Le plus important, c’est que votre point de vue n’a pas vraiment de sens et qu’il fait différentes choses en raison de votre utilisation dense_rank(), ce n’est donc pas vraiment un problème de performances.
Evan Carroll

1
@ EvanCarroll, votre commentaire m'a incité à y aller moi-même (d'où ma réponse modifiée). Merci.
enjayaitch

0

(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 les vues de manière générale également dans PG pour une meilleure compréhension et maintenance des requêtes / du code.)

En réalité et malheureusement (AVERTISSEMENT :), l’ utilisation de vues dans Postgres nous posait de véritables problèmes et réduisait considérablement nos performances en fonction des fonctionnalités que nous utilisions à l’intérieur :-( (au moins avec la version 10.1). systèmes de base de données modernes comme Oracle.)

Donc, éventuellement (et ceci est ma question) tout filtrage sur la vue ... entraînant une exécution de requête unique sur les tables sous-jacentes.

(En fonction de ce que vous voulez dire exactement - non - des tables de températures intermédiaires peuvent être matérialisées que vous ne voulez pas être ou des prédicats ne sont pas enfoncés ...)

Je connais au moins deux fonctionnalités principales, qui nous ont laissés au milieu des migrations d’Oracle à Postgres , nous avons donc dû abandonner PG dans un projet:

  • CTEs ( withsous - requêtes -clause / expressions de table communes ) sont (généralement) utiles pour structurer des requêtes plus complexes (même dans des applications plus petites), mais dans PG sont par la conception mises en œuvre comme « cachées » optimiseur conseils (générant par exemple des tables temporaires non indexées) et viole donc le concept (pour moi et beaucoup d’autres importants) de SQL déclaratif ( docu Oracle ): 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 un 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)  
      ...
      */
    • Autres sources avec discussions, etc.: https://blog.2ndquadrant.com/postgresql-ctes-are-optimization-fences/

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


notre solution de contournement pour les withclauses

Nous allons transformer toutes les "vues en ligne" en vues réelles avec un préfixe spécial afin qu'elles ne perturbent pas la liste / l'espace de noms des vues et qu'elles puissent être facilement associé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.


En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.