41.11. PL/pgSQL bajo el capó #

41.11.1. Sustitución de variables
41.11.2. Almacenamiento en caché de planes

Esta sección describe algunos detalles de implementación que con frecuencia es importante que conozcan los usuarios de PL/pgSQL.

41.11.1. Sustitución de variables #

Las instrucciones y expresiones SQL dentro de una función PL/pgSQL pueden hacer referencia a variables y parámetros de la función. Tras bambalinas, PL/pgSQL sustituye los parámetros de consulta por dichas referencias. Los parámetros de consulta solo se sustituirán en lugares donde sean sintácticamente permitidos. Como caso extremo, considera este ejemplo de mal estilo de programación:

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

La primera aparición de foo debe ser sintácticamente un nombre de tabla, por lo que no se sustituirá, incluso si la función tiene una variable llamada foo. La segunda aparición debe ser el nombre de una columna de esa tabla, por lo que tampoco se sustituirá. Del mismo modo, la tercera aparición debe ser un nombre de función, por lo que tampoco se sustituirá. Solo la última aparición es candidata a ser una referencia a una variable de la función PL/pgSQL.

Otra forma de entender esto es que la sustitución de variables solo puede insertar valores de datos en un comando SQL; no puede cambiar dinámicamente a qué objetos de base de datos hace referencia el comando. (Si deseas hacer eso, debes construir una cadena de comando dinámicamente, como se explica en la Section 41.5.4).

Dado que los nombres de las variables sintácticamente no difieren de los nombres de las columnas de la tabla, puede haber ambigüedad en las instrucciones que también hacen referencia a tablas: ¿se refiere un nombre dado a una columna de tabla o a una variable? Cambiemos el ejemplo anterior a:

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

Aquí, dest y src deben ser nombres de tabla, y col debe ser una columna de dest, pero foo y bar podrían ser razonablemente tanto variables de la función como columnas de src.

Por defecto, PL/pgSQL reportará un error si un nombre en una instrucción SQL pudiera referirse tanto a una variable como a una columna de tabla. Puedes solucionar este problema cambiando el nombre de la variable o columna, calificando la referencia ambigua o indicándole a PL/pgSQL qué interpretación preferir.

La solución más sencilla es cambiar el nombre de la variable o columna. Una regla de codificación común es utilizar una convención de nomenclatura diferente para las variables de PL/pgSQL de la que se utiliza para los nombres de columnas. Por ejemplo, si nombras consistentemente las variables de función como v_algo mientras que ninguno de los nombres de tus columnas comienza con v_, no ocurrirá ningún conflicto.

Alternativamente, puedes calificar las referencias ambiguas para aclararlas. En el ejemplo anterior, src.foo sería una referencia inequívoca a la columna de la tabla. Para crear una referencia inequívoca a una variable, declárala en un bloque etiquetado y utiliza la etiqueta del bloque (consulta la Section 41.2). Por ejemplo:

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

Aquí, block.foo significa la variable incluso si hay una columna foo en src. Los parámetros de la función, así como las variables especiales como FOUND, se pueden calificar con el nombre de la función, porque se declaran implícitamente en un bloque externo etiquetado con el nombre de la función.

A veces no es práctico corregir todas las referencias ambiguas en un gran cuerpo de código PL/pgSQL. En tales casos, puedes especificar que PL/pgSQL resuelva las referencias ambiguas como la variable (lo que es compatible con el comportamiento de PL/pgSQL antes de PostgreSQL 9.0), o como la columna de la tabla (lo que es compatible con algunos otros sistemas como Oracle).

Para cambiar este comportamiento para todo el sistema, establece el parámetro de configuración plpgsql.variable_conflict a uno de error, use_variable o use_column (donde error es el valor predeterminado de fábrica). Este parámetro afecta a las compilaciones posteriores de instrucciones en las funciones PL/pgSQL, pero no a las instrucciones ya compiladas en la sesión actual. Debido a que cambiar esta configuración puede causar cambios inesperados en el comportamiento de las funciones PL/pgSQL, solo puede ser modificado por un superusuario.

También puedes establecer el comportamiento función por función, insertando uno de estos comandos especiales al inicio del texto de la función:

#variable_conflict error
#variable_conflict use_variable
#variable_conflict use_column

Estos comandos afectan solo a la función en la que están escritos y anulan la configuración de plpgsql.variable_conflict. Un ejemplo es:

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;

En el comando UPDATE, curtime, comment e id se referirán a la variable y parámetros de la función, tengan o no las tablas users columnas con esos nombres. Ten en cuenta que tuvimos que calificar la referencia a users.id en la cláusula WHERE para que se refiera a la columna de la tabla. Pero no tuvimos que calificar la referencia a comment como destino en la lista de UPDATE, porque sintácticamente debe ser una columna de users. Podríamos escribir la misma función sin depender de la configuración de variable_conflict de esta manera:

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;

La sustitución de variables no ocurre en una cadena de comando dada a EXECUTE o a una de sus variantes. Si necesitas insertar un valor variable en dicho comando, hazlo como parte de la construcción del valor de la cadena, o utiliza USING, como se ilustra en la Section 41.5.4.

La sustitución de variables actualmente funciona solo en SELECT, INSERT, UPDATE, DELETE, MERGE y comandos que contienen uno de estos (como EXPLAIN y CREATE TABLE ... AS SELECT), porque el motor SQL principal permite parámetros de consulta solo en estos comandos. Para utilizar un nombre o valor no constante en otros tipos de instrucciones (genéricamente llamadas instrucciones de utilidad), debes construir la instrucción de utilidad como una cadena y ejecutarla con EXECUTE.

41.11.2. Almacenamiento en caché de planes #

El intérprete de PL/pgSQL analiza el texto fuente de la función y produce un árbol de instrucciones binario interno la primera vez que se llama a la función (dentro de cada sesión). El árbol de instrucciones traduce completamente la estructura de la instrucción PL/pgSQL, pero las expresiones SQL individuales y los comandos SQL utilizados en la función no se traducen de inmediato.

A medida que cada expresión y comando SQL se ejecuta por primera vez en la función, el intérprete de PL/pgSQL analiza y examina el comando para crear una instrucción preparada, utilizando la función SPI_prepare del gestor SPI. Las visitas posteriores a esa expresión o comando reutilizan la instrucción preparada. Por lo tanto, una función con rutas de código condicionales que rara vez se visitan nunca incurrirá en la sobrecarga de analizar aquellos comandos que nunca se ejecutan dentro de la sesión actual. Una desventaja es que los errores en una expresión o comando específico no se pueden detectar hasta que esa parte de la función se alcanza en la ejecución. (Los errores de sintaxis triviales se detectarán durante la fase de análisis inicial, pero cualquier error más profundo no se detectará hasta la ejecución).

PL/pgSQL (o más precisamente, el gestor SPI) puede además intentar almacenar en caché el plan de ejecución asociado con cualquier instrucción preparada en particular. Si no se utiliza un plan en caché, se genera un plan de ejecución nuevo en cada visita a la instrucción, y los valores de los parámetros actuales (es decir, los valores de las variables de PL/pgSQL) se pueden utilizar para optimizar el plan seleccionado. Si la instrucción no tiene parámetros, o se ejecuta muchas veces, el gestor SPI considerará la creación de un plan genérico que no dependa de valores de parámetros específicos, y lo almacenará en caché para su reutilización. Normalmente, esto ocurrirá solo si el plan de ejecución no es muy sensible a los valores de las variables de PL/pgSQL a las que se hace referencia en él. Si lo es, generar un plan cada vez es una ventaja neta. Consulta la PREPARE para obtener más información sobre el comportamiento de las instrucciones preparadas.

Debido a que PL/pgSQL guarda las instrucciones preparadas y a veces los planes de ejecución de esta manera, los comandos SQL que aparecen directamente en una función PL/pgSQL deben hacer referencia a las mismas tablas y columnas en cada ejecución; es decir, no puedes utilizar un parámetro como nombre de una tabla o columna en un comando SQL. Para evitar esta restricción, puedes construir comandos dinámicos utilizando la instrucción EXECUTE de PL/pgSQL, a costa de realizar un nuevo análisis sintáctico y construir un nuevo plan de ejecución en cada ejecución.

La naturaleza mutable de las variables de registro presenta otro problema en este sentido. Cuando se utilizan campos de una variable de registro en expresiones o instrucciones, los tipos de datos de los campos no deben cambiar de una llamada de la función a la siguiente, ya que cada expresión se analizará utilizando el tipo de datos que está presente cuando se alcanza la expresión por primera vez. Se puede utilizar EXECUTE para evitar este problema cuando sea necesario.

Si la misma función se utiliza como disparador para más de una tabla, PL/pgSQL prepara y almacena en caché las instrucciones de forma independiente para cada una de esas tablas; es decir, hay una caché para cada combinación de función disparadora y tabla, no solo para cada función. Esto alivia algunos de los problemas con los tipos de datos variables; por ejemplo, una función disparadora podrá trabajar correctamente con una columna llamada key incluso si resulta tener diferentes tipos en diferentes tablas.

Del mismo modo, las funciones que tienen tipos de argumentos polimórficos tienen una caché de instrucciones independiente para cada combinación de tipos de argumentos reales para los que han sido invocadas, de modo que las diferencias en los tipos de datos no causen fallos inesperados.

El almacenamiento en caché de instrucciones a veces puede tener efectos sorprendentes en la interpretación de valores sensibles al tiempo. Por ejemplo, hay una diferencia entre lo que hacen estas dos funciones:

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

y:

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

En el caso de logfunc1, el analizador principal de PostgreSQL sabe al analizar el INSERT que la cadena 'now' debe interpretarse como timestamp, porque la columna de destino de logtable es de ese tipo. Por lo tanto, 'now' se convertirá en una constante timestamp cuando se analice el INSERT, y luego se utilizará en todas las invocaciones de logfunc1 durante la vida útil de la sesión. De más está decir que esto no es lo que el programador quería. Una mejor idea es utilizar la función now() o current_timestamp.

En el caso de logfunc2, el analizador principal de PostgreSQL no sabe en qué tipo debe convertirse 'now' y, por lo tanto, devuelve un valor de datos de tipo text que contiene la cadena now. Durante la asignación subsiguiente a la variable local curtime, el intérprete de PL/pgSQL convierte esta cadena al tipo timestamp llamando a las funciones textout y timestamp_in para la conversión. Así, la marca de tiempo calculada se actualiza en cada ejecución como espera el programador. Aunque esto funcione como se espera, no es terriblemente eficiente, por lo que el uso de la función now() seguiría siendo una mejor idea.