41.11. Detalhes técnicos do PL/pgSQL #

41.11.1. Substituição de variável
41.11.2. Cache de plano

Esta seção discute alguns detalhes de implementação do PL/pgSQL, que muitas vezes devem ser de conhecimento dos usuários.

41.11.1. Substituição de variável #

As instruções e expressões SQL no corpo de uma função escrita em PL/pgSQL podem referir-se a variáveis e parâmetros da função. Às escondidas, o PL/pgSQL substitui os parâmetros da consulta por estas referências. Os parâmetros da consulta só são substituídos nos locais onde a sintaxe permite que seja feito. Como caso extremo, considere este exemplo de estilo de programação ruim:

INSERT INTO foo (foo) VALUES (foo(foo));

A primeira ocorrência de foo deve ser sintaticamente um nome de tabela, portanto não haverá substituição, mesmo que a função tenha uma variável chamada foo. A segunda ocorrência deve ser o nome de uma coluna dessa tabela, portanto, também não haverá substituição. Da mesma maneira, a terceira ocorrência deve ser um nome de função, portanto, também não haverá substituição. Somente a última ocorrência é candidata a referir-se a uma variável da função PL/pgSQL.

Outra maneira de entender esta situação, é que a substituição de variável só pode inserir valores de dados em um comando SQL; a substituição não pode alterar dinamicamente quais objetos do banco de dados são referenciados pelo comando. (Se quiser fazer isto, deve ser criada uma cadeia de caracteres de comando dinamicamente, conforme explicado em Execução de comandos dinâmicos.)

Como os nomes das variáveis não são sintaticamente diferentes dos nomes das colunas da tabela, pode haver ambiguidade nas declarações que também se referem a tabelas: um determinado nome deve se referir a uma coluna da tabela ou a uma variável? Agora vamos mudar o exemplo anterior para:

INSERT INTO dest (col) SELECT foo + bar FROM src;

Aqui, dest e src devem ser nomes de tabelas, e col deve ser o nome de uma coluna da tabela dest, mas foo e bar podem muito bem ser variáveis da função, ou colunas da tabela src.

Por padrão, o PL/pgSQL relata um erro quando o nome em uma instrução SQL puder se referir a uma variável ou a uma coluna de tabela. Este problema pode ser corrigido mudando o nome da variável ou da coluna, qualificando a referência ambígua, ou informando ao PL/pgSQL qual interpretação usar.

A solução mais simples é mudar o nome a variável ou da coluna. Uma regra de codificação comum é usar uma convenção de nomenclatura diferente para as variáveis do PL/pgSQL da usada para os nomes de colunas. Por exemplo, se as variáveis da função receberem nomes v_alguma_coisa de forma consistente, enquanto nenhum dos nomes de coluna começar com v_, não haverá nenhum conflito.

Como alternativa, pode-se qualificar referências ambíguas para torná-las evidentes. No exemplo acima, src.foo seria uma referência inequívoca à coluna da tabela. Para criar uma referência inequívoca a uma variável, esta variável deve ser declarada em um bloco com rótulo, e usado o rótulo do bloco (veja Estrutura do PL/pgSQL). Por exemplo:

<<bloco>>
DECLARE
    foo int;
BEGIN
    foo := ...;
    INSERT INTO dest (col) SELECT bloco.foo + bar FROM src;

No exemplo acima, bloco.foo refere-se à variável, mesmo havendo uma coluna foo na tabela src. Parâmetros de função, bem como variáveis especiais como FOUND, podem ser qualificados pelo nome da função, porque são declarados implicitamente em um bloco externo rotulado com o nome da função.

Às vezes é impraticável corrigir todas as referências ambíguas em um corpo grande de código PL/pgSQL. Nesses casos, pode-se especificar que o PL/pgSQL deve resolver referências ambíguas como a variável (compatível com o comportamento do PL/pgSQL das versões anteriores ao PostgreSQL 9.0), ou como a coluna da tabela (compatível com alguns outros sistemas, como o Oracle).

Para mudar este comportamento em todo o sistema, deve ser definido o parâmetro de configuração plpgsql.variable_conflict como error, use_variable, ou use_column (onde error é o padrão de fábrica). Mudar este parâmetro afeta as próximas compilações de instruções nas funções escritas em PL/pgSQL, mas não as instruções já compiladas na sessão corrente. Como a alteração dessa configuração pode causar alterações inesperadas no comportamento das funções escritas em PL/pgSQL, esta configuração só pode ser alterada por um superusuário.

O comportamento também pode ser definido função por função, inserindo um desses comandos especiais no início do texto da função:

#variable_conflict error
#variable_conflict use_variable
#variable_conflict use_column

Estes comandos afetam apenas a função onde estão escritos, substituindo a configuração de plpgsql.variable_conflict. Por exemplo:

CREATE FUNCTION stamp_user(id int, comment text) RETURNS void AS $$
    #variable_conflict use_variable
    DECLARE
        curtime timestamp := now();
    BEGIN
        UPDATE users SET last_modified = curtime, comment = comment
          WHERE users.id = id;
    END;
$$ LANGUAGE plpgsql;

No comando UPDATE, curtime, comment e id vão referir-se à variável e aos parâmetros da função, quer a tabela users tenha colunas com estes nomes, ou não. Note que foi necessário qualificar a referência a users.id na cláusula WHERE, para fazê-la referir-se à coluna da tabela. Mas não foi necessário qualificar a referência a comment como destino na lista do UPDATE, porque sintaticamente deve ser uma coluna de users. A mesma função poder ser escrita sem depender da configuração de variable_conflict dessa forma:

CREATE FUNCTION stamp_user(id int, comment text) RETURNS void AS $$
    <<fn>>
    DECLARE
        curtime timestamp := now();
    BEGIN
        UPDATE users SET last_modified = fn.curtime, comment = stamp_user.comment
          WHERE users.id = stamp_user.id;
    END;
$$ LANGUAGE plpgsql;

Não ocorre substituição de variável em uma cadeia de caracteres de comando passada ao EXECUTE, ou a uma de suas variantes. Se for necessário inserir um valor variável num comando desse tipo, deve ser feito como parte da construção do valor da cadeia de caracteres, ou usado USING, como mostrado em Execução de comandos dinâmicos.

A substituição de variável atualmente funciona apenas em SELECT, INSERT, UPDATE, DELETE, MERGE e comandos contendo um desses (como EXPLAIN e CREATE TABLE ... AS SELECT), porque o mecanismo de SQL principal permite parâmetros de consulta apenas nesses comandos. Para usar um nome ou valor variável em outros tipos de instrução (chamadas genericamente de instruções utilitárias), esta instrução utilitária deve ser construída como uma cadeia de caracteres e passada para o EXECUTE.

41.11.2. Cache de plano #

O interpretador do PL/pgSQL analisa o texto do código-fonte da função e produz uma árvore binária interna de instruções na primeira vez que a função é chamada (dentro de cada sessão). A árvore de instruções inteiramente traduzida para a estrutura de instruções do PL/pgSQL, mas as expressões e comandos SQL individuais usados na função não são traduzidos imediatamente.

À medida que cada expressão e comando SQL é executado primeiro na função, o interpretador do PL/pgSQL lê e analisa o comando para criar uma instrução preparada, usando a função SPI_prepare da interface SPI. Os acessos subsequentes a esta expressão ou comando reutilizam a instrução preparada. Assim, é possível que uma função com caminhos de código condicional pouco acessados não ocasione a sobrecarga de analisar comandos que não são executados na sessão corrente. A desvantagem é que erros em uma expressão ou comando específico não podem ser detectados até que a execução dessa parte da função seja atingida. (Erros triviais de sintaxe são detectados durante a passagem de análise inicial, mas nada mais profundo será detectado até a execução.)

Além disso, o PL/pgSQL (mais precisamente, o gerenciador do SPI) pode tentar armazenar em cache o plano de execução associado a qualquer instrução preparada. Se não for usado um plano armazenado em cache, então será gerado um novo plano de execução a cada acesso à instrução, e os valores correntes dos parâmetros (ou seja, os valores das variáveis do PL/pgSQL) poderão ser usados para otimizar o plano selecionado. Se a instrução não tiver parâmetros, ou for executada várias vezes, o gerenciador do SPI vai considerar a criação de um plano genérico, independente de valores de parâmetros, e o seu armazenamento em cache para ser reutilizado. Normalmente, isto só acontece quando o plano de execução não é muito sensível aos valores das variáveis do PL/pgSQL referenciadas por ele. Caso contrário, gerar um plano a cada vez é um ganho certo. Veja PREPARE para obter mais informações sobre o comportamento das instruções preparadas.

Como o PL/pgSQL salva as instruções preparadas e, às vezes, os planos de execução dessa maneira, os comandos SQL que aparecem diretamente em uma função PL/pgSQL devem referir-se às mesmas tabelas e colunas em cada execução; ou seja, não se pode usar um parâmetro como nome de tabela ou coluna em um comando SQL. Para contornar esta restrição, podem ser construídos comandos dinâmicos usando a instrução EXECUTE do PL/pgSQL — ao preço de realizar a análise de uma nova leitura e construir um novo plano de execução a cada execução.

A natureza mutável das variáveis de tipo registro apresenta outro problema nesta situação. Quando os campos de uma variável de registro são usados em expressões ou instruções, os tipos de dados dos campos não devem mudar de uma chamada da função para outra, porque cada expressão será analisada usando o tipo de dados que estiver presente quando a expressão for alcançada da primeira vez. A instrução EXECUTE pode ser usada para contornar este problema, se houver necessidade.

Se a mesma função for usada como gatilho para mais de uma tabela, o PL/pgSQL prepara e armazena em cache as instruções de forma independente para cada uma dessas tabelas — ou seja, há um cache para cada combinação de função de gatilho e tabela, e não apenas para cada função. Isto alivia alguns dos problemas com tipos de dados variados; por exemplo, uma função de gatilho poderá funcionar com sucesso com uma coluna chamada chave, mesmo que tenha tipos diferentes em tabelas diferentes.

Da mesma forma, as funções com argumentos de tipos de dados polimórficos têm um cache de instrução separado para cada combinação de tipos de dados de argumentos reais para as quais foram chamadas, de modo que as diferenças de tipo de dados não causem falhas inesperadas.

O cache de instrução pode, às vezes, ter efeitos surpreendentes na interpretação de valores sensíveis ao tempo. Por exemplo, há uma diferença entre o que estas duas funções fazem:

CREATE FUNCTION logfunc1(logtxt text) RETURNS void AS $$
    BEGIN
        INSERT INTO logtable VALUES (logtxt, 'now');
    END;
$$ LANGUAGE plpgsql;

e:

CREATE FUNCTION logfunc2(logtxt text) RETURNS void AS $$
    DECLARE
        curtime timestamp;
    BEGIN
        curtime := 'now';
        INSERT INTO logtable VALUES (logtxt, curtime);
    END;
$$ LANGUAGE plpgsql;

No caso da função logfunc1, o analisador principal do PostgreSQL sabe ao analisar o comando INSERT que a cadeia de caracteres 'now' deve ser interpretado como timestamp, porque a coluna de destino de logtable é desse tipo de dados. Assim, 'now' será convertido em uma constante do tipo timestamp quando o INSERT for analisado e, então, este valor será usado em todas as chamadas de logfunc1 durante o tempo de vida da sessão. Desnecessário dizer que não era isto que o programador queria. Uma ideia melhor é usar a função now() ou current_timestamp.

No caso da função logfunc2, o analisador principal do PostgreSQL não sabe qual tipo 'now' deve se tornar, portanto, retorna um valor de dados do tipo text contendo a cadeia de caracteres now. Durante a atribuição subsequente à variável local curtime, o interpretador do PL/pgSQL converte esta cadeia de caracteres para o tipo timestamp, chamando as funções textout e timestamp_in para realizar a conversão. Assim, o carimbo de data e hora calculado é atualizado a cada execução, conforme o esperado pelo programador. Embora funcione como esperado, não é muito eficiente, assim usar a função now() é uma ideia melhor.