¿Las vistas son perjudiciales para el rendimiento en PostgreSQL?

45

Lo siguiente es un extracto de un libro sobre db design (Beginning Database Design ISBN: 0-7645-7490-6):

El peligro de usar vistas es filtrar una consulta contra una vista, esperando leer una porción muy pequeña de una tabla muy grande. Cualquier filtrado debe realizarse dentro de la vista porque cualquier filtrado contra la vista en sí se aplica después de que la consulta en la vista haya completado la ejecución. Las vistas suelen ser útiles para acelerar el proceso de desarrollo, pero a la larga pueden matar por completo el rendimiento de la base de datos.

Lo siguiente es un extracto de la documentación de PostgreSQL 9.5:

Hacer un uso liberal de las vistas es un aspecto clave del buen diseño de la base de datos SQL. Las vistas le permiten encapsular los detalles de la estructura de sus tablas, que pueden cambiar a medida que su aplicación evoluciona, detrás de interfaces consistentes.

Las dos fuentes parecen contradecirse ("no diseñar con vistas" versus "hacer diseño con vistas").

Sin embargo, en PG las vistas se implementan utilizando el sistema de reglas. Por lo tanto, posiblemente (y esta es mi pregunta) cualquier filtrado contra la vista se reescribe como un filtro dentro de la vista, lo que resulta en una ejecución de consulta única en las tablas subyacentes.

¿Es correcta mi interpretación y PG combina las cláusulas WHERE dentro y fuera de la vista? ¿O los ejecuta por separado, uno tras otro? ¿Algún ejemplo breve, autocontenido, correcto (compilable)?

ARX
fuente
Creo que la pregunta no es correcta porque ambas fuentes no están hablando de lo mismo. El primero está relacionado con la consulta desde una vista y DESPUÉS de aplicar un filtro:. SELECT * FROM my_view WHERE my_column = 'blablabla';Mientras que el segundo se trata de usar vistas para hacer que su modelo de datos sea transparente para la aplicación que lo utiliza. Las primeras fuentes le indican que incluya el filtro WHERE my_column = 'blablabla'dentro de la definición de la vista, ya que esto da como resultado un mejor plan de ejecución.
EAmez

Respuestas:

51

El libro esta equivocado.

Seleccionar desde una vista es exactamente tan rápido o lento como ejecutar la instrucción SQL subyacente; puede verificarlo fácilmente usando explain analyze.

El optimizador de Postgres (y el optimizador para muchos otros DBMS modernos) podrá insertar predicados en la vista en la declaración de vista real, siempre que se trate de una declaración simple (de nuevo, esto se puede verificar usando explain analyze).

La "mala reputación" con respecto al rendimiento se deriva, creo, de cuando usas en exceso las vistas y comienzas a construir vistas que usan vistas que usan vistas. Muy a menudo, eso da como resultado declaraciones que hacen demasiado en comparación con una declaración hecha a mano sin las vistas, por ejemplo, porque algunas tablas intermedias no serían necesarias. En casi todos los casos, el optimizador no es lo suficientemente inteligente como para eliminar esas tablas / combinaciones innecesarias o para empujar hacia abajo los predicados en múltiples niveles de vistas (esto también es cierto para otros DBMS).

un caballo sin nombre
fuente
3
Dadas algunas de las contra-respuestas propuestas, es posible que desee exponer un poco lo que es una declaración simple .
RDFozz
¿Puedes explicar cómo usar la explain analyzedeclaración?
Dustin Michels
@DustinMichels: eche un vistazo al manual: postgresql.org/docs/current/using-explain.html
a_horse_with_no_name
19

Para darle un ejemplo de lo que explicó @a_horse :

Postgres implementa el esquema de información, que consiste en vistas (a veces complejas) que proporcionan información sobre objetos DB en forma estandarizada. Esto es conveniente y confiable, y puede ser sustancialmente más costoso que acceder directamente a las tablas del catálogo de Postgres.

Ejemplo muy simple, para obtener todas las columnas visibles de una tabla
... del esquema de información:

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

... del catálogo del sistema:

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

Compare los planes de consulta y el tiempo de ejecución de ambos con EXPLAIN ANALYZE.

  • La primera consulta se basa en la vista information_schema.columns, que se une a varias tablas que no necesitamos para esto.

  • La segunda consulta solo escanea la única tabla pg_catalog.pg_attribute, por lo tanto, mucho más rápido. (Pero la primera consulta solo necesita unos pocos ms en las bases de datos comunes).

Detalles:

Erwin Brandstetter
fuente
7

EDITAR:

Con disculpas, necesito retractarme de mi afirmación de que la respuesta aceptada no siempre es correcta: establece que la vista siempre es idéntica a la misma cosa escrita como una subconsulta. Creo que eso es indiscutible, y creo que ahora sé lo que está pasando en mi caso.

Ahora también creo que hay una mejor respuesta a la pregunta original.

La pregunta original es si debería ser una práctica orientadora el uso de vistas (en lugar de, por ejemplo, repetir SQL en rutinas que pueden necesitar mantenerse dos veces o más).

Mi respuesta sería "no si su consulta usa funciones de ventana o cualquier otra cosa que haga que el optimizador trate la consulta de manera diferente cuando se convierte en una subconsulta, porque el acto mismo de crear la subconsulta (ya sea representada como una vista o no) puede degradar el rendimiento si está filtrando con parámetros en tiempo de ejecución.

La complejidad de mi función de ventana es innecesaria. El plan de explicación para esto:

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

es mucho menos costoso que para esto:

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

Espero que sea un poco más específico y útil.

En mi experiencia reciente (haciéndome encontrar esta pregunta), la respuesta aceptada arriba no es correcta en todas las circunstancias. Tengo una consulta relativamente simple que incluye una función de ventana:

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 agrego este filtro:

where assembly_key = '185132'

El plan de explicación que obtengo es el siguiente:

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

Esto está utilizando el índice de clave primaria en la tabla de servicio del tren y un índice no único en la tabla porción_consistir. Se ejecuta en 90ms.

Creé una vista (pegándola aquí para que sea absolutamente clara, pero es literalmente la consulta en 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))

Cuando consulto esta vista con el filtro idéntico:

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

Este es el plan de explicación:

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)

Esto está haciendo escaneos completos en ambas tablas y toma 17 segundos.

Hasta que me encontré con esto, he estado usando generosamente vistas con PostgreSQL (habiendo entendido las opiniones ampliamente expresadas en la respuesta aceptada). Evitaría específicamente el uso de vistas si necesito un filtrado agregado previo, para lo cual usaría funciones de devolución de conjuntos.

También soy consciente de que los CTE en PostgreSQL se evalúan estrictamente por separado, por diseño, por lo que no los uso de la misma manera que lo haría con SQL Server, por ejemplo, donde parecen estar optimizados como subconsultas.

Mi respuesta, por lo tanto, es que hay casos en los que las vistas no funcionan exactamente como la consulta en la que se basan, por lo que se recomienda precaución. Estoy usando Amazon Aurora basado en PostgreSQL 9.6.6.

enjayaitch
fuente
2
Tenga en cuenta la advertencia en la otra respuesta : " siempre que esta sea una declaración simple ".
RDFozz
Como nota al margen, no CASE WHEN (NOT ts.primary_direction) THEN '-1' :: INTEGER ELSE 1 ENDserá necesario que la consulta sea más lenta de lo necesario, ya que es mejor escribir dos condicionales más en el orden.
Evan Carroll
@EvanCarroll Luché con esto por un tiempo. Acabo de descubrir que es marginalmente más rápido sacar el CASO un nivel: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
Esa tampoco es una buena idea ... tienes algunos problemas aquí. Quiero decir, lo importante es que su punto de vista realmente no tiene sentido y hace cosas diferentes debido a su uso, por dense_rank()lo que no es realmente un problema de rendimiento.
Evan Carroll
1
@EvanCarroll tu comentario me llevó a llegar allí (de ahí mi respuesta editada). Gracias.
enjayaitch
0

(Soy un gran admirador de las vistas, pero debe tener mucho cuidado con PG aquí y me gustaría animar a todos a usar vistas en general también en PG para una mejor comprensión y facilidad de mantenimiento de consultas / código)

En realidad y tristemente (ADVERTENCIA :) el uso de vistas en Postgres nos causó problemas reales y disminuyó gravemente nuestro rendimiento dependiendo de las características que estábamos usando dentro de él :-( (al menos con v10.1). (Esto no sería así con otros sistemas de bases de datos modernos como Oracle).

Entonces, posiblemente (y esta es mi pregunta) cualquier filtrado contra la vista ... resultando en una ejecución de consulta única contra las tablas subyacentes.

(Dependiendo de lo que quiera decir exactamente, no, las tablas temporales intermedias pueden materializarse como tal vez no desee o donde no se presionen los predicados ...)

Conozco al menos dos "características" principales, que nos decepcionaron en medio de las migraciones de Oracle a Postgres, por lo que tuvimos que abandonar PG en un proyecto:

  • Los CTE ( withsubconsultas de cláusula / expresiones de tabla comunes ) son (generalmente) útiles para estructurar consultas más complejas (incluso en aplicaciones más pequeñas), pero en PG se implementan por diseño como sugerencias optimizadoras "ocultas" (que generan, por ejemplo, tablas temporales no indexadas) y violar así el concepto (para mí y para muchos otros importantes) de SQL declarativo ( documento de Oracle ): por ejemplo

    • consulta 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)
        ...
      */
    • reescrito usando algo de 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)  
      ...
      */
    • Más fuentes con discusiones, etc .: https://blog.2ndquadrant.com/postgresql-ctes-are-optimization-fences/

  • Las funciones de ventana con over-declaraciones son potencialmente inutilizables (normalmente se utilizan en vistas, por ejemplo, como fuente de informes basados ​​en consultas más complejas)


nuestra solución para las withcláusulas

Transformaremos todas las "vistas en línea" en vistas reales con un prefijo especial para que no alteren la lista / espacio de nombres de las vistas y puedan relacionarse fácilmente con la "vista externa" original: - /


nuestra solución para las funciones de ventana

Lo implementamos con éxito utilizando la base de datos Oracle.

Andreas Dietrich
fuente
1
La discusión se movió al chat .
ypercubeᵀᴹ