33.2. As visões e o sistema de regras

No PostgreSQL as visões são implementadas através do sistema de regras. De fato, essencialmente não há diferença entre

CREATE VIEW minha_visão AS SELECT * FROM minha_tabela;

quando se compara com os dois comandos

CREATE TABLE minha_visão (mesma lista de colunas de minha_tabela);
CREATE RULE "_RETURN" AS ON SELECT TO minha_visão DO INSTEAD
    SELECT * FROM minha_tabela;

porque é exatamente isto o que o comando CREATE VIEW faz internamente. Causa alguns efeitos colaterais. Um deles é que nos catálogos do sistema do PostgreSQL as informações sobre visão são exatamente as mesmas que para tabela. Portanto, para o analisador não existe absolutamente nenhuma diferença entre uma tabela e uma visão, são a mesma coisa: relações.

33.2.1. Como as regras do SELECT funcionam

As regras ON SELECT são aplicadas a todos os comandos como o último passo, mesmo que o comando seja um INSERT, UPDATE ou DELETE, e possuem semântica diferente das regras para os outros tipos de comando, porque modificam a árvore de comando diretamente em vez de criar uma nova. Portanto as regras do SELECT são descritas primeiro.

Atualmente só pode haver uma ação em uma regra ON SELECT, e deve ser uma ação SELECT incondicional que seja INSTEAD. Esta restrição é necessária para tornar as regras seguras o suficiente para serem abertas aos usuários comuns, restringindo as regras ON SELECT a agirem como visões.

Os exemplos deste capítulo são duas visões com junção que realizam alguns cálculos, e mais algumas visões que utilizam estas visões. Uma das duas primeiras visões é personalizada posteriormente adicionando regras para as operações INSERT, UPDATE e DELETE, para que o resultado final seja uma visão que se comporte como uma tabela real com alguma funcionalidade mágica. Não é um exemplo simples para se começar, torna o assunto mais difícil de ser entendido, mas é melhor ter um exemplo que cobre todos os pontos discutido passo a passo, do que ter muitos exemplos diferentes o que pode acabar confundindo a mente.

Para o exemplo é necessária uma pequena função, min, que retorna o menor entre dois valores inteiros. É criada como:

CREATE FUNCTION min(integer, integer) RETURNS integer AS $$
    SELECT CASE WHEN $1 < $2 THEN $1 ELSE $2 END
$$ LANGUAGE SQL STRICT;

As tabelas reais necessárias nas duas primeiras descrições do sistemas de regras são estas:

CREATE TABLE tbl_sapato (
    sap_nome              text,     -- nome do sapato - chave primária
    sap_num_par_disp      integer,  -- número de pares disponíveis
    sap_cor_cad_pref      text,     -- cor preferida do cadarço do sapato
    sap_comp_cad_min      real,     -- comprimento mínimo do cadarço do sapato
    sap_comp_cad_max      real,     -- comprimento máximo do cadarço do sapato
    sap_comp_cad_unid     text      -- unidade de comprimento
);

CREATE TABLE tbl_cadarço (
    cad_sap_nome          text,     -- nome do cadarço do sapato - chave primária
    cad_sap_num_par_disp  integer,  -- número de pares disponíveis
    cad_sap_cor           text,     -- cor do cadarço
    cad_sap_comp          real,     -- comprimento do cadarço
    cad_sap_unid          text      -- unidade de comprimento
);

CREATE TABLE tbl_unidade (
    uni_nome              text,     -- nome da unidade - chave primária
    uni_fator             real      -- fator para transformar em cm
);

Como pode ser visto, representa os dados de uma loja de sapatos.

As visões são criadas como

CREATE VIEW vis_sapato AS
    SELECT sap.sap_nome,
           sap.sap_num_par_disp,
           sap.sap_cor_cad_pref,
           sap.sap_comp_cad_min,
           sap.sap_comp_cad_min * u.uni_fator AS sap_comp_cad_min_cm,
           sap.sap_comp_cad_max,
           sap.sap_comp_cad_max * u.uni_fator AS sap_comp_cad_max_cm,
           sap.sap_comp_cad_unid
      FROM tbl_sapato sap, tbl_unidade u
     WHERE sap.sap_comp_cad_unid = u.uni_nome;

CREATE VIEW vis_cadarço AS
    SELECT cad.cad_sap_nome,
           cad.cad_sap_num_par_disp,
           cad.cad_sap_cor,
           cad.cad_sap_comp,
           cad.cad_sap_unid,
           cad.cad_sap_comp * uni.uni_fator AS cad_sap_comp_cm
      FROM tbl_cadarço cad, tbl_unidade uni
     WHERE cad.cad_sap_unid = uni.uni_nome;

CREATE VIEW vis_sapato_pronto AS
    SELECT vsap.sap_nome,
           vsap.sap_num_par_disp,
           vcad.cad_sap_nome,
           vcad.cad_sap_num_par_disp,
           min(vsap.sap_num_par_disp, vcad.cad_sap_num_par_disp) AS total_disp
      FROM vis_sapato vsap, vis_cadarço vcad
     WHERE vcad.cad_sap_cor = vsap.sap_cor_cad_pref
       AND vcad.cad_sap_comp_cm >= vsap.sap_comp_cad_min_cm
       AND vcad.cad_sap_comp_cm <= vsap.sap_comp_cad_max_cm;

O comando CREATE VIEW para a visão vis_cadarço (que é a mais simples que temos) cria a relação vis_cadarço, e uma entrada em pg_rewrite informando que existe uma regra de reescrita que deve ser aplicada sempre que a relação vis_cadarço for referenciada na tabela de abrangência do comando. A regra não possui nenhuma qualificação de regra (discutido mais tarde, nas regras não-SELECT, uma vez que atualmente as regras SELECT não podem tê-las), e é do tipo INSTEAD. Deve ser observado que qualificação de regra não é o mesmo que qualificação de comando. A ação da nossa regra possui uma qualificação de comando. A ação da regra é uma árvore de comando que é a cópia da instrução SELECT do comando de criação da visão.

Nota: As duas entradas adicionais na tabela de abrangência para NEW e OLD (chamadas na árvore de comando impressa de *NEW* e *OLD* por motivos históricos) que podem ser vistas em pg_rewrite não são de interesse para as regras de SELECT.

Agora são inseridas linhas nas tabelas tbl_unidade, tbl_sapato e tbl_cadarço, e executada uma consulta simples na visão:

INSERT INTO tbl_unidade VALUES ('cm', 1.0);
INSERT INTO tbl_unidade VALUES ('m', 100.0);
INSERT INTO tbl_unidade VALUES ('inch', 2.54);

INSERT INTO tbl_sapato VALUES ('sap1', 2, 'preto', 70.0, 90.0, 'cm');
INSERT INTO tbl_sapato VALUES ('sap2', 0, 'preto', 30.0, 40.0, 'inch');
INSERT INTO tbl_sapato VALUES ('sap3', 4, 'marrom', 50.0, 65.0, 'cm');
INSERT INTO tbl_sapato VALUES ('sap4', 3, 'marrom', 40.0, 50.0, 'inch');

INSERT INTO tbl_cadarço VALUES ('cad1', 5, 'preto', 80.0, 'cm');
INSERT INTO tbl_cadarço VALUES ('cad2', 6, 'preto', 100.0, 'cm');
INSERT INTO tbl_cadarço VALUES ('cad3', 0, 'preto', 35.0 , 'inch');
INSERT INTO tbl_cadarço VALUES ('cad4', 8, 'preto', 40.0 , 'inch');
INSERT INTO tbl_cadarço VALUES ('cad5', 4, 'marrom', 1.0 , 'm');
INSERT INTO tbl_cadarço VALUES ('cad6', 0, 'marrom', 0.9 , 'm');
INSERT INTO tbl_cadarço VALUES ('cad7', 7, 'marrom', 60 , 'cm');
INSERT INTO tbl_cadarço VALUES ('cad8', 1, 'marrom', 40 , 'inch');

SELECT * FROM vis_cadarço;

 cad_sap_nome | cad_sap_num_par_disp | cad_sap_cor | cad_sap_comp | cad_sap_unid | cad_sap_comp_cm
--------------+----------------------+-------------+--------------+--------------+-----------------
 cad1         |                    5 | preto       |           80 | cm           |              80
 cad2         |                    6 | preto       |          100 | cm           |             100
 cad7         |                    7 | marrom      |           60 | cm           |              60
 cad3         |                    0 | preto       |           35 | inch         |            88.9
 cad4         |                    8 | preto       |           40 | inch         |           101.6
 cad8         |                    1 | marrom      |           40 | inch         |           101.6
 cad5         |                    4 | marrom      |            1 | m            |             100
 cad6         |                    0 | marrom      |          0.9 | m            |              90
(8 linhas)

Este é o SELECT mais simples que pode ser feito nestas visões, portanto é utilizada esta oportunidade para explicar os princípios básicos das regras de visão. A instrução SELECT * FROM vis_cadarço após ser interpretada pelo analisador produz a árvore de comando

SELECT vis_cadarço.cad_sap_nome, vis_cadarço.cad_sap_num_par_disp,
       vis_cadarço.cad_sap_cor, vis_cadarço.cad_sap_comp,
       vis_cadarço.cad_sap_unid, vis_cadarço.cad_sap_comp_cm
  FROM vis_cadarço vis_cadarço;

entregue ao sistema de regras. O sistema de regras percorre a tabela de abrangência e verifica se há regras para alguma relação. Ao processar a entrada para vis_cadarço (a única até agora) na tabela de abrangência encontra a regra _RETURN com a árvore de comando

SELECT cad.cad_sap_nome, cad.cad_sap_num_par_disp,
       cad.cad_sap_cor, cad.cad_sap_comp, cad.cad_sap_unid,
       cad.cad_sap_comp * uni.uni_fator AS cad_sap_comp_cm
  FROM vis_cadarço *OLD*, vis_cadarço *NEW*,
       tbl_cadarço cad, tbl_unidade uni
 WHERE cad.cad_sap_unid = uni.uni_nome;

Para expandir a visão o reescritor simplesmente cria uma entrada na tabela de abrangência do subcomando, contendo a árvore de comando da ação da regra, e substitui esta entrada da tabela de abrangência pela original que fazia referência à visão. A árvore de comando reescrita resultante é praticamente a mesma que seria se tivesse sido digitado

SELECT vis_cadarço.cad_sap_nome, vis_cadarço.cad_sap_num_par_disp,
       vis_cadarço.cad_sap_cor, vis_cadarço.cad_sap_comp,
       vis_cadarço.cad_sap_unid, vis_cadarço.cad_sap_comp_cm
  FROM (SELECT cad.cad_sap_nome,
               cad.cad_sap_num_par_disp,
               cad.cad_sap_cor,
               cad.cad_sap_comp,
               cad.cad_sap_unid,
               cad.cad_sap_comp * uni.uni_fator AS cad_sap_comp_cm
          FROM tbl_cadarço cad, tbl_unidade uni
         WHERE cad.cad_sap_unid = uni.uni_nome) vis_cadarço;

Entretanto, há uma diferença: a tabela de abrangência do subcomando possui duas entradas adicionais, vis_cadarço *OLD* e vis_cadarço *NEW*. Estas entradas não participam diretamente no comando, uma vez que não são referenciadas pela árvore de junção ou pela lista de destino do subcomando. O reescritor utiliza as mesmas para armazenar informações de verificação de privilégio de acesso presentes originalmente na entrada da tabela de abrangência que fazia referência à visão. Desta maneira, o executor ainda vai verificar se o usuário possui os privilégios apropriados para acessar a visão, muito embora não exista uso direto da visão no comando reescrito.

Esta foi a aplicação da primeira regra. O sistema de regras continua verificando as entradas remanescentes na tabela de abrangência do comando no topo (neste exemplo não há mais nenhuma), e verifica recursivamente as entradas nas tabelas de abrangência dos subcomandos adicionados para verificar se alguma destes faz referência a visão (Mas não expande *OLD* ou *NEW* — senão haveria uma recursão infinita!) Neste exemplo não existem regras de reescrita para tbl_cadarço ou tbl_unidade. Portanto a reescrita está completa e a instrução acima é o resultado final entregue ao planejador.

Agora desejamos escrever uma consulta para descobrir para quais sapatos na loja existem cadarços correspondentes (cor e comprimento), e com número total de pares com correspondência exata maior ou igual a dois.

SELECT * FROM vis_sapato_pronto WHERE total_disp >= 2;

 sap_nome | sap_num_par_disp | cad_sap_nome | cad_sap_num_par_disp | total_disp
----------+------------------+--------------+----------------------+------------
 sap1     |                2 | cad1         |                    5 |          2
 sap3     |                4 | cad7         |                    7 |          4
(2 linhas)

Desta vez a saída do analisador é a árvore de comando

SELECT vis_sapato_pronto.sap_nome, vis_sapato_pronto.sap_num_par_disp,
       vis_sapato_pronto.cad_sap_nome, vis_sapato_pronto.cad_sap_num_par_disp,
       vis_sapato_pronto.total_disp
  FROM vis_sapato_pronto vis_sapato_pronto
 WHERE vis_sapato_pronto.total_disp >= 2;

A primeira regra aplicada será a da visão vis_sapato_pronto, resultando na árvore de comando

SELECT vis_sapato_pronto.sap_nome, vis_sapato_pronto.sap_num_par_disp,
       vis_sapato_pronto.cad_sap_nome, vis_sapato_pronto.cad_sap_num_par_disp,
       vis_sapato_pronto.total_disp
  FROM (SELECT vsap.sap_nome,
               vsap.sap_num_par_disp,
               vcad.cad_sap_nome,
               vcad.cad_sap_num_par_disp,
               min(vsap.sap_num_par_disp, vcad.cad_sap_num_par_disp) AS total_disp
          FROM vis_sapato vsap, vis_cadarço vcad
         WHERE vcad.cad_sap_cor = vsap.sap_cor_cad_pref
           AND vcad.cad_sap_comp_cm >= vsap.sap_comp_cad_min_cm
           AND vcad.cad_sap_comp_cm <= vsap.sap_comp_cad_max_cm) vis_sapato_pronto
 WHERE vis_sapato_pronto.total_disp >= 2;

De maneira semelhante, as regras para vis_sapato e vis_cadarço são substituídas na tabela de abrangência do subcomando, conduzindo a uma árvore de comando final com três níveis:

SELECT vis_sapato_pronto.sap_nome, vis_sapato_pronto.sap_num_par_disp,
       vis_sapato_pronto.cad_sap_nome, vis_sapato_pronto.cad_sap_num_par_disp,
       vis_sapato_pronto.total_disp
  FROM (SELECT vsap.sap_nome,
               vsap.sap_num_par_disp,
               vcad.cad_sap_nome,
               vcad.cad_sap_num_par_disp,
               min(vsap.sap_num_par_disp, vcad.cad_sap_num_par_disp) AS total_disp
          FROM (SELECT sap.sap_nome,
                       sap.sap_num_par_disp,
                       sap.sap_cor_cad_pref,
                       sap.sap_comp_cad_min,
                       sap.sap_comp_cad_min * u.uni_fator AS sap_comp_cad_min_cm,
                       sap.sap_comp_cad_max,
                       sap.sap_comp_cad_max * u.uni_fator AS sap_comp_cad_max_cm,
                       sap.sap_comp_cad_unid
                  FROM tbl_sapato sap, tbl_unidade u
                 WHERE sap.sap_comp_cad_unid = u.uni_nome) vsap,
               (SELECT cad.cad_sap_nome,
                       cad.cad_sap_num_par_disp,
                       cad.cad_sap_cor,
                       cad.cad_sap_comp,
                       cad.cad_sap_unid,
                       cad.cad_sap_comp * uni.uni_fator AS cad_sap_comp_cm
                  FROM tbl_cadarço cad, tbl_unidade uni
                 WHERE cad.cad_sap_unid = uni.uni_nome) vcad
         WHERE vcad.cad_sap_cor = vsap.sap_cor_cad_pref
           AND vcad.cad_sap_comp_cm >= vsap.sap_comp_cad_min_cm
           AND vcad.cad_sap_comp_cm <= vsap.sap_comp_cad_max_cm) vis_sapato_pronto
 WHERE vis_sapato_pronto.total_disp >= 2;

Acontece que o planejador colapsa esta árvore em uma árvore de comando de dois níveis: os comandos SELECT na parte inferior são "puxados" para o SELECT do meio, uma vez que não é necessário processá-los separadamente. Porém, o SELECT do meio permanece separado do de cima, porque contém funções de agregação. Se fosse puxado para cima mudaria do comportamento do SELECT do topo, o que não se deseja. Entretanto, colapsar a árvore de comando é uma otimização que o sistema de reescrita não tem que se preocupar a mesma.

Nota: Atualmente não existe no sistema de regras mecanismo para interromper a recursão das regras de visão (somente para os outros tipos de regra). Isto não é um problema sério, porque a única maneira de provocar um laço sem fim (inchando o processo servidor até que este chegue ao limite de memória), é criar as tabelas e depois definir as regras de visão manualmente utilizando CREATE RULE, de uma maneira que a primeira tabela selecione da segunda e a segunda selecione da primeira. Esta situação não pode acontecer quando se utiliza CREATE VIEW, porque no primeiro CREATE VIEW a segunda relação não existe e, portanto, a primeira relação não pode selecionar da segunda.

33.2.2. Regras de visão em instruções não-SELECT

Dois detalhes da árvore de comando não foram tocados na descrição das regras de visão acima. São estes o tipo do comando e a relação do resultado. De fato, as regras de visão não precisam desta informação.

Existem poucas diferenças entre uma árvore de comando para o SELECT e para qualquer outro comando. Obviamente possuem tipos de comando diferentes e, fora o o SELECT, a relação do resultado aponta para uma entrada na tabela de abrangência para onde o resultado deve ir. Tudo mais é exatamente o mesmo. Portanto, se existirem as tabelas t1 e t2 com as colunas a e b, as árvores de comando para as instruções

SELECT t2.b FROM t1, t2 WHERE t1.a = t2.a;

UPDATE t1 SET b = t2.b WHERE t1.a = t2.a;

são praticamente idênticas. Em particular:

A conseqüência é que as duas árvores de comando resultam em planos de execução semelhantes: ambos são junções de duas tabelas. Para o UPDATE as colunas de t1 que faltam são adicionadas à lista de destino pelo planejador, e a árvore de comando final fica sendo:

UPDATE t1 SET a = t1.a, b = t2.b WHERE t1.a = t2.a;

Portanto, o processamento do executor sobre a junção produz exatamente o mesmo conjunto de resultados que

SELECT t1.a, t2.b FROM t1, t2 WHERE t1.a = t2.a;

produz, mas existe um pequeno problema no UPDATE: para o executor não interessa para que serve o resultado da junção sendo feita. Apenas produz um conjunto de linhas de resultado. A diferença que um comando é SELECT e que o outro é UPDATE é tratado por quem chama o executor. Quem chama ainda sabe (olhando na árvore de comando) que este comando é um UPDATE, e sabe que este resultado deve ir para a tabela t1. Mas qual das linhas presentes deve ser substituída pela nova linha?

Para resolver este problema é adicionada uma outra entrada na lista de destino da instrução UPDATE (e também da instrução DELETE): o identificador da tupla corrente (CTID). Esta é uma coluna do sistema contendo o número de bloco do arquivo, e a posição da linha no bloco. Sabendo a tabela, o CTID pode ser utilizado para trazer a linha original de t1 a ser atualizada. Após adicionar CTID à lista de destino, o comando se parece com:

SELECT t1.a, t2.b, t1.ctid FROM t1, t2 WHERE t1.a = t2.a;

Agora entra em cena um outro detalhe do PostgreSQL. As linhas antigas da tabela não são sobrescritas, e por causa disto o ROLLBACK é rápido. Em uma instrução UPDATE a nova linha de resultado é inserida na tabela (após eliminar o CTID), e no cabeçalho de linha da linha antiga, para o qual CTID apontava, as entradas cmax e xmax são definidas como o contador de comando corrente e identificador de transação corrente. Portanto a linha antiga fica escondida, e após a efetivação da transação o comando VACUUM pode remover a linha.

Sabendo disso tudo, pode-se simplesmente aplicar as regras de visão exatamente da mesma maneira para qualquer comando. Não existe diferença.

33.2.3. O poder das visões no PostgreSQL

O que foi visto acima demonstra como o sistema de regras incorpora as definições de visão na árvore de comando original. No segundo exemplo um simples SELECT de uma visão criou uma árvore de comando final que é a junção de quatro tabelas (tbl_unidade foi utilizada duas vezes com nomes diferentes).

O benefício de implementar as visões pelo sistema de regras é que o planejador possui todas as informações sobre quais tabelas devem ser varridas, mais o relacionamento entre estas tabelas, mais as qualificações restritivas das visões, mais as qualificações do comando original, em uma única árvore de comando. Esta permanece sendo a situação quando o comando original já é uma junção de visões. O planejador tem que decidir qual o melhor caminho para executar o comando, e quanto mais informações o planejador tiver melhor poderá ser a decisão. A forma de implementação do sistema de regras no PostgreSQL garante que esta é toda a informação disponível sobre o comando até este ponto.

33.2.4. Atualização de visão

O que acontece se uma visão for nomeada como a relação de destino de uma instrução INSERT, UPDATE ou DELETE? Após serem feitas as substituições descritas acima, será obtida uma árvore de comando na qual a relação do resultado aponta para uma entrada na tabela de abrangência do subcomando. Isto não funciona e, portanto, o reescritor lança um erro quando vê que produziu algo deste tipo.

Para se mudar esta situação, podem ser definidas regras que modificam o comportamento destes tipos de comando. Este é o assunto da próxima seção.

SourceForge.net Logo CSS válido!