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.
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.
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:
As tabelas de abrangência possuem entradas para as tabelas t1 e t2.
As listas de destino contêm uma variável que aponta para a coluna b da entrada na tabela de abrangência para a tabela t2.
As expressões de qualificação comparam as colunas a das duas entradas na tabela de abrangência com relação a igualdade.
As árvores de junção mostram uma junção simples entre t1 e t2.
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.
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.
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.