Procedimientos almacenados de Python en PostgreSQL

Ivan Panchenko, cofundador de Postgres Professional, un desarrollador de software ruso

Un aspecto de la extensibilidad de Postgres es la interfaz para agregar lenguajes de procedimiento (PL). Gracias a esto, podemos escribir procedimientos de servidor no solo en PL/PgSQL similar a SQL, sino también en lenguajes de programación imperativos convencionales, incluido Python. Específicamente, PL/Python es Python con SPI incorporado: una interfaz de programación de servidor que permite que los procedimientos de Python realicen consultas en la base de datos.

Junto con PL/Perl y PL/v8 (Javascript), PL/Python es uno de los lenguajes de procedimiento no SQL más comunes para la programación del lado del servidor en Postgres. Se incluye en la distribución principal y está presente en todos los repositorios de paquetes.

¿Programar en Python o no?

¿Cuándo tiene sentido usar PL/Python y otros lenguajes de procedimiento que no sean SQL? ¿Cuándo falta el poder de SQL y PL/pgSQL?

  • Para trabajar con algoritmos y estructuras de datos no SQL. Puede ser recorrido con un árbol, análisis, extracción de datos HTML o XML, algún tipo de cálculo, etc.
  • Para la generación dinámica de SQL complejos, como para informes, o en un ORM.
  • Para usar el rico conjunto de bibliotecas disponibles en Python.
  • Para trabajar con datos externos, recibir datos a través de la red, desde archivos, iniciar programas externos, enviar cartas, etc.
  • Para las extensiones de creación de prototipos que planea implementar en C a continuación.

PL/Python es un lenguaje NO CONFIABLE. Estos lenguajes incluyen aquellos que pueden realizar E/S directamente (trabajando con disco, red, etc.). Por razones de seguridad, solo el superusuario de la base de datos puede crear funciones en estos idiomas.

Un intérprete de PL/Python se crea la primera vez que se accede a él, es decir, la primera vez que se llama a una función, procedimiento o bloque anónimo de PL/Python. Y aquí es importante decidir inmediatamente qué versión de Python quieres usar: la segunda o la tercera.

Los intérpretes de Python 2 y Python 3 no se llevan bien en el mismo proceso: hay un conflicto de nombres. Si trabajó con uno de ellos en una sesión y luego cargó otro, Postgres se bloqueará y para el proceso del servidor (backend) será un error fatal. Es posible acceder a otra versión, pero deberá abrir otra sesión para hacerlo.

Debido a las diferencias entre Python 2 y Python 3, Postgres define dos lenguajes de procedimiento diferentes, plpython2u y plpython3u. La letra u al final indica que el idioma es NO CONFIABLE.

¿Para qué sirve Python?

Todas las funciones y procedimientos de PL/Python definen dos diccionarios, un SD estático y un GD global. El global permite que todas las funciones de un solo backend compartan datos, lo cual es a la vez atractivo y peligroso. Cada función tiene su propio diccionario estático. Estos diccionarios se pueden usar para guardar (caché) ciertos datos entre llamadas a funciones.

Veamos algunos ejemplos simples para aprender a usar PL/Python.

Escribamos un bloque anónimo que muestre el mensaje «¡Hola mundo!»

DO $      plpy.notice('Hello World!', hint="Будь здоров", detail="В деталях") $ LANGUAGE plpython2u; NOTICE:  Hello World! DETAIL:  В деталях HINT:  Будь здоров DO 

Los mensajes de Postgres pueden contener, además del texto del mensaje en sí, los campos Sugerencia y Detalles, el número de línea y muchos otros parámetros. También para enviar un mensaje de texto resultará en throw 'Errmsg'

En PL/Python, cada nivel de registro de Postgres tiene su propia función: AVISO, ADVERTENCIA, DEPURACIÓN, REGISTRO, INFORMACIÓN, FATAL. Si es ERROR, la transacción falló; si es FATAL, todo el backend falló. Afortunadamente, el caso no llegó a PÁNICO. Usted puede leer sobre ello aquí.

Ahora intentemos ejecutar la consulta SQL más simple y verifiquemos la velocidad de trabajo con PL/pgSQL. Preste atención a las funciones plpy.* – son, como tales, interesantes para el DBMS disponible en PL/Python. Puede leer más sobre ellos en la documentación.

A PL/pgSQL:

DO $      DECLARE a int;      BEGIN           SELECT count(*) INTO a FROM pg_class;      END; $ LANGUAGE plpgsql; 0.7 ms 

A PL/Python:

DO $      x = plpy.execute('SELECT count(*) FROM pg_class'); $ LANGUAGE  plpython2u; 0.8 ms 

Era Python 2. En Python 3 es lo mismo, pero quizás un 10 % más lento.

Pero es un poco muy rápido, casi cero. Intentemos ejecutar la consulta 1 millón de veces, para que la diferencia sea más notable:

A PL/pgSQL:

DO $      DECLARE a int; i int;      BEGIN FOR i IN 0..999999 LOOP           SELECT count(*) INTO a FROM pg_class;     END LOOP; END; $ LANGUAGE plpgsql; 53s 

A PL/Python 3:

DO $      for i in range (0,1000000) :           plpy.execute('SELECT count(*) FROM pg_class') $ LANGUAGE plpython3u; 98s 

PL/pgSQL ya es el doble de rápido que PL/Python. Pero los posgresistas experimentados lo entenderán: algo anda mal. PL/pgSQL puede almacenar automáticamente en caché los planes de consulta, mientras que PL/Python reprograma la consulta cada vez. En el buen sentido, las consultas deben prepararse una vez, debe construirse un plan de consultas y luego, de acuerdo con ese plan, deben ejecutarse tantas veces como sea necesario. En PL/Python puede trabajar explícitamente con planes de consulta. Por ejemplo, así:

DO $      h = plpy.prepare('SELECT count(*) FROM pg_class')      for i in range (0,1000000):             plpy.execute(h) $ LANGUAGE plpython3u; 62s 

Ahora vemos que el rendimiento de los idiomas es casi igual, esto se basó en trabajar con la base. Para comparar idiomas entre sí en su forma más pura, intentemos calcular algo sobre ellos sin referirnos a la base, por ejemplo, la suma de cuadrados.

PL/pgSQL:

DO $      DECLARE i bigint; a bigint;      BEGIN a=0;      FOR i IN 0..1000000 LOOP           a=a+i*i::bigint;      END LOOP; END; $ LANGUAGE plpgsql; 280ms 

PL/Python 3:

DO $ a=0 for i in range(1,1000001): a=a+i*i $ LANGUAGE plpython3u; 73ms 

PL/Python alcanzó y superó a PL/pgSQL, para computación era unas cuatro veces más rápido.

¿Cómo pasar parámetros y devolver un resultado de una función en PL/Python?

En general, como siempre. El único problema es la falta de coincidencia entre el rico conjunto de tipos de datos de Postgres y el más bien modesto de Python. Los tipos numéricos y de cadena se pasan normalmente. Mesas también. Pero los datos de tipo JSON (JSONB) llegarán a Python como una cadena de texto y deberán analizarse para poder trabajar con esos datos.

O use la función TRANSFORM para configurar conversiones de tipo personalizadas. Para que JSONB se pase correctamente hacia y desde PL/Python, debe instalar la extensión jsonb_plpython y especificar el parámetro TRANSFORM al crear la función:

CREATE EXTENSION jsonb_plpython3u;   CREATE FUNCTION func( ……) RETURNS …. LANGUAGE plpython3 TRANSFORM FOR TYPE jsonb …; 

Si está utilizando jsonb con PL/Python, este TRANSFORM es muy recomendable.

TRANSFORM es una función relativamente nueva y hay muy pocas. Para Python, por ejemplo, solo se admiten los tipos jsonb y hstore. Así que hay algo que hacer. Para jsonb, además, sería posible desarrollar una «transformación diferida», que no desplegaría todo el objeto, sino solo las partes solicitadas. Esto aceleraría significativamente el procesamiento de JSON grandes. Entonces, hay tareas para los entusiastas del desarrollo de código abierto, ¡bienvenidos!

Deja un comentario