As visualizações são prejudiciais para o desempenho no PostgreSQL?

45

A seguir, um trecho de um livro sobre design de banco de dados (ISBN: 0-7645-7490-6):

O perigo do uso de visualizações é filtrar uma consulta em uma visualização, esperando ler uma parte muito pequena de uma tabela muito grande. Qualquer filtragem deve ser feita dentro da visualização porque qualquer filtragem na própria visualização é aplicada após a consulta na visualização concluir a execução. As visualizações geralmente são úteis para acelerar o processo de desenvolvimento, mas a longo prazo podem prejudicar completamente o desempenho do banco de dados.

A seguir, um trecho da documentação do PostgreSQL 9.5:

Fazer uso liberal de visualizações é um aspecto essencial do bom design de banco de dados SQL. As visualizações permitem encapsular os detalhes da estrutura de suas tabelas, que podem mudar à medida que seu aplicativo evolui, por trás de interfaces consistentes.

As duas fontes parecem se contradizer ("não projetar com vistas" vs. "projetar com vistas").

No entanto, nas visualizações PG, são implementadas usando o sistema de regras. Portanto, possivelmente (e essa é minha pergunta) qualquer filtragem na exibição é reescrita como um filtro na exibição, resultando em uma única execução de consulta nas tabelas subjacentes.

Minha interpretação está correta e o PG combina as cláusulas WHERE dentro e fora da visualização? Ou os executa separadamente, um após o outro? Algum exemplo curto, independente, correto (compilável)?

ARX
fonte
Eu acho que a pergunta não está certa, porque ambas as fontes não estão falando sobre a mesma coisa. O primeiro está relacionado à consulta em uma visualização e APÓS a aplicação de um filtro: SELECT * FROM my_view WHERE my_column = 'blablabla';.Enquanto o segundo é sobre o uso de visualizações para tornar seu modelo de dados transparente para o aplicativo que o utiliza. As primeiras fontes apontam para você incluir o filtro WHERE my_column = 'blablabla'dentro da definição de exibição, pois isso resulta em um melhor plano de execução.
EAmez 13/08

Respostas:

49

O livro está errado.

A seleção de uma visualização é exatamente tão rápida ou lenta quanto a execução da instrução SQL subjacente - você pode verificar isso facilmente explain analyze.

O otimizador do Postgres (e o otimizador de muitos outros DBMSes modernos) poderá enviar os predicados da exibição para a declaração de exibição real - desde que seja uma declaração simples (novamente, isso pode ser verificado usando explain analyze).

A "má reputação" em relação ao desempenho decorre - eu acho - de quando você usa demais as visualizações e começa a criar visualizações que usam visualizações que usam visualizações. Muitas vezes, isso resulta em declarações que fazem muito em comparação com uma declaração feita à mão sem as visualizações, por exemplo, porque algumas tabelas intermediárias não seriam necessárias. Em quase todos os casos, o otimizador não é inteligente o suficiente para remover as tabelas / associações desnecessárias ou empurrar os predicados para vários níveis de visualizações (isso também é válido para outros DBMSes).

um cavalo sem nome
fonte
3
Dadas algumas das respostas contrárias propostas, você pode explicar um pouco o que é uma afirmação simples .
RDFozz
Você pode explicar como usar a explain analyzedeclaração?
Dustin Michels
@DustinMichels: dê uma olhada no manual: postgresql.org/docs/current/using-explain.html
a_horse_with_no_name
19

Para dar um exemplo do que o @a_horse explicou :

O Postgres implementa o esquema de informações, que consiste em visualizações (às vezes complexas) que fornecem informações sobre objetos de banco de dados de forma padronizada. Isso é conveniente e confiável - e pode ser substancialmente mais caro do que acessar diretamente as tabelas de catálogo do Postgres.

Exemplo muito simples, para obter todas as colunas visíveis de uma tabela
... a partir do esquema de informações:

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

... do catálogo do sistema:

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

Compare planos de consulta e tempo de execução para ambos com EXPLAIN ANALYZE.

  • A primeira consulta é baseada na visualização information_schema.columns, que se une a várias tabelas que não precisamos para isso.

  • A segunda consulta verifica apenas uma tabela pg_catalog.pg_attribute, portanto, muito mais rápido. (Mas a primeira consulta ainda precisa de apenas alguns ms em bancos de dados comuns.)

Detalhes:

Erwin Brandstetter
fonte
7

EDITAR:

Com desculpas, preciso retirar minha afirmação de que a resposta aceita nem sempre é correta - ela afirma que a exibição é sempre idêntica à mesma coisa escrita como uma subconsulta. Eu acho que é indiscutível, e acho que agora sei o que está acontecendo no meu caso.

Agora também acho que há uma resposta melhor para a pergunta original.

A questão original é se deveria ser prática orientadora o uso de visualizações (em vez de, por exemplo, repetir o SQL em rotinas que talvez precisem ser mantidas duas ou mais vezes).

Minha resposta seria "se a sua consulta usar funções de janela ou qualquer outra coisa que faça com que o otimizador trate a consulta de maneira diferente quando ela se tornar uma subconsulta, porque o próprio ato de criar a subconsulta (representada como uma exibição ou não) pode prejudicar o desempenho" se você estiver filtrando com parâmetros em tempo de execução.

A complexidade da minha função de janela é desnecessária. O plano de explicação para isso:

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

é muito menos oneroso do que para isso:

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 seja um pouco mais específico e útil.

Na minha experiência recente (levando-me a encontrar essa pergunta), a resposta aceita acima não está correta em todas as circunstâncias. Eu tenho uma consulta relativamente simples que inclui uma função de janela:

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 eu adicionar este filtro:

where assembly_key = '185132'

O plano de explicação que recebo é o seguinte:

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

Isso está usando o índice de chave primária na tabela de serviços de trem e um índice não exclusivo na tabela porção_consist. Ele é executado em 90ms.

Criei uma view (colá-la aqui para ficar absolutamente clara, mas é literalmente a consulta em uma view):

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 eu consulto essa exibição com o filtro idêntico:

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

Este é o plano de explicação:

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)

Isso faz varreduras completas nas duas tabelas e leva 17s.

Até me deparar com isso, tenho usado liberalmente visualizações com o PostgreSQL (tendo compreendido as visões amplamente expressas expressas na resposta aceita). Eu evitaria especificamente o uso de visualizações se precisar de filtragem pré-agregada, para a qual usaria funções de retorno de conjunto.

Também estou ciente de que as CTEs no PostgreSQL são rigorosamente avaliadas separadamente, por design, portanto não as uso da mesma maneira que usaria no SQL Server, por exemplo, onde elas parecem otimizadas como subconsultas.

Minha resposta, portanto, é que há casos em que os modos de exibição não funcionam exatamente como a consulta na qual eles se baseiam, portanto, recomenda-se cautela. Estou usando o Amazon Aurora com base no PostgreSQL 9.6.6.

enjayaitch
fonte
2
Observe a ressalva na outra resposta - " desde que seja uma afirmação simples ".
RDFozz
Como observação lateral, CASE WHEN (NOT ts.primary_direction) THEN '-1' :: INTEGER ELSE 1 ENDserá desnecessário tornar a consulta mais lenta do que o necessário, é melhor escrever mais dois condicionais no pedido.
Evan Carroll
@EvanCarroll Eu lutei com isso por um tempo. Acabei de descobrir que é um pouco mais rápido extrair o CASE de um nível: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 11/18
Também não é uma boa ideia ... você tem alguns problemas aqui. Quero dizer, o grande problema é que sua visão não faz muito sentido e faz coisas diferentes por causa do seu uso, por dense_rank()isso não é realmente um problema de desempenho.
Evan Carroll
1
@EvanCarroll, seu comentário me levou a chegar lá (daí a minha resposta editada). Obrigado.
precisa saber é o seguinte
0

(Eu sou um grande fã de visualizações, mas você deve ter muito cuidado com o PG aqui e gostaria de incentivar todos a usar as visualizações geralmente também no PG para melhor compreensão e manutenção de consultas / códigos)

Na verdade, infelizmente (AVISO :) o uso de visualizações no Postgres nos causou problemas reais e diminuiu bastante nosso desempenho, dependendo dos recursos que estávamos usando dentro dele :-( (pelo menos na v10.1). (Isso não seria o caso com outras sistemas de banco de dados modernos, como o Oracle.)

Portanto, possivelmente (e essa é a minha pergunta) qualquer filtragem contra a exibição ... resultando em uma única execução de consulta nas tabelas subjacentes.

(Dependendo do que você quer dizer exatamente - as tabelas temporárias intermediárias podem ser materializadas que você pode não querer estar ou onde os predicados não são pressionados para baixo ...)

Conheço pelo menos dois "recursos" principais que nos decepcionaram no meio das migrações do Oracle para o Postgres, então tivemos que abandonar o PG em um projeto:

  • CTEs ( withsubconsultas -clause / expressões de tabela comuns ) são (geralmente) útil para a estruturação de consultas mais complexas (mesmo em aplicações menores), mas em PG são por design implementado como "escondido" otimizador dicas (gerando por exemplo, tabelas temporárias não indexados) e viole, assim, o conceito (para mim e para muitos outros importantes) de SQL declarativo ( Oracle docu ):

    • consulta simples:

      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 algum 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)  
      ...
      */
      
    • outras fontes com discussões etc .: https://blog.2ndquadrant.com/postgresql-ctes-are-optimization-fences/

  • as funções de janela com over-statements são potencialmente inutilizáveis (normalmente usadas em visualizações, por exemplo, como fonte de relatórios com base em consultas mais complexas)


nossa solução alternativa para as withcláusulas

Transformaremos todas as "visualizações inline" em visualizações reais com um prefixo especial para que elas não atrapalhem a lista / namespace de visualizações e possam ser facilmente relacionadas à "visualização externa" original: - /


nossa solução para as funções da janela

Nós o implementamos com sucesso usando o banco de dados Oracle.

Andreas Dietrich
fonte