Привет, Хабр!

Сегодня рассмотрим PL/V8. Это расширение PostgreSQL, которое позволяет использовать JavaScript через движок V8 (тот самый, который стоит за Node.js). Это открывает массу возможностей:

  • Писать хранимые процедуры на JavaScript.

  • Выполнять сложную бизнес-логику прямо в базе данных.

  • Создавать триггеры, которые обрабатывают данные.

Звучит круто, но сразу уточним, где это применимо:

  1. Когда вы работаете с JSON-структурами.

  2. Если нужна динамическая обработка данных.

  3. Когда хочется упростить и ускорить логику, не гоняя данные между базой и приложением.

Установим:

На Ubuntu/Debian:

sudo apt update
sudo apt install postgresql-plv8

Теперь подключаем расширение в базе:

CREATE EXTENSION plv8;

Проверим, всё ли работает:

SELECT plv8_version();

Если видите номер версии (например, 3.2.0), значит, всё окей.

Для macOS/Linux: если нужно всё делать своими руками (или на системе нет готовых пакетов), PL/V8 можно собрать из исходников:

  1. Скачиваем код:

git clone https://github.com/plv8/plv8
cd plv8
  1. Устанавливаем зависимости:

sudo apt install build-essential cmake libstdc++-12-dev
  1. Сборка:

make
sudo make install

После установки можно настроить некоторые параметры в postgresql.conf:

  • plv8.execution_timeout — время выполнения JS-кода в секундах. Значение по дефолту 300 секунд.

  • plv8.memory_limit — лимит памяти для JavaScript (в МБ). По умолчанию 256 МБ.

Пример настройки:

SET plv8.execution_timeout = 10; -- максимум 10 секунд
SET plv8.memory_limit = 128;    -- максимум 128 МБ

Применение

Работа с JSON

Допустим, есть массивы ключей и значений, которые нужно объединить в JSON. Вот как это сделать:

CREATE FUNCTION json_from_arrays(keys TEXT[], values TEXT[]) RETURNS JSON AS $$
    var result = {};
    for (var i = 0; i < keys.length; i++) {
        result[keys[i]] = values[i];
    }
    return result;
$$ LANGUAGE plv8 IMMUTABLE STRICT;

-- Проверим:
SELECT json_from_arrays(ARRAY['name', 'age'], ARRAY['Roman', '30']);

Результат:

{"name": "Roman", "age": "30"}

Подсчёт суммы в массиве

Пусть есть массив чисел, и нужно быстро посчитать их сумму:

CREATE FUNCTION array_sum(numbers plv8_int4array) RETURNS INT AS $$
    var sum = 0;
    for (var i = 0; i < numbers.length; i++) {
        sum += numbers[i];
    }
    return sum;
$$ LANGUAGE plv8 IMMUTABLE STRICT;

-- Тестируем:
SELECT array_sum(ARRAY[1, 2, 3, 4, 5]); -- 15

Триггер для логирования изменений

Допустим, нужно логировать изменения в таблице. PL/V8 позволяет реализовать эту логику:

CREATE FUNCTION log_changes() RETURNS TRIGGER AS $$
    plv8.elog(NOTICE, "Row changed: ", JSON.stringify(NEW));
    return NEW;
$$ LANGUAGE plv8;

CREATE TRIGGER log_trigger
    AFTER INSERT OR UPDATE
    ON your_table
    FOR EACH ROW
    EXECUTE FUNCTION log_changes();

Теперь каждое изменение в таблице будет выводиться в лог PostgreSQL.

SPI и подготовленные запросы

PL/V8 имеет интерфейсы для работы с SQL-запросами. Пример использования подготовленных запросов:

CREATE FUNCTION find_users_by_age(age INT) RETURNS JSON AS $$
    var plan = plv8.prepare("SELECT id, name FROM users WHERE age = $1", ['int']);
    var result = plan.execute([age]);
    plan.free();
    return result;
$$ LANGUAGE plv8;

-- Используем:
SELECT find_users_by_age(30);

Это ускоряет выполнение часто используемых запросов, так как они заранее компилируются PostgreSQL.

Обработка данных с транзакциями

Предположим, есть таблица заказов, и нужно в одной транзакции обновить статусы всех заказов, а также записать лог об этом:

CREATE FUNCTION process_orders() RETURNS VOID AS $$
    plv8.subtransaction(function () {
        // Обновляем статусы заказов
        plv8.execute("UPDATE orders SET status = 'processed' WHERE status = 'pending'");
        
        // Логируем изменения
        var log_message = "Processed orders at " + new Date().toISOString();
        plv8.execute("INSERT INTO logs (message) VALUES ($1)", [log_message]);
    });
$$ LANGUAGE plv8;

-- Запуск функции
SELECT process_orders();

Если одна из операций внутри транзакции не выполнится (например, ошибка в INSERT INTO logs), все изменения будут откатаны.

Генерация отчёта с использованием JavaScript

Допустим, нужно сгенерировать отчёт в формате JSON, который объединяет данные из нескольких таблиц. PL/V8 позволяет это сделать без привлечения прочих инструментов:

CREATE FUNCTION generate_report() RETURNS JSON AS $$
    // Получаем данные о пользователях
    var users = plv8.execute("SELECT id, name FROM users");

    // Получаем данные о заказах
    var orders = plv8.execute("SELECT user_id, amount FROM orders");

    // Объединяем данные
    var report = {};
    users.forEach(function(user) {
        report[user.name] = orders
            .filter(order => order.user_id === user.id)
            .map(order => order.amount);
    });

    return report;
$$ LANGUAGE plv8;

-- Генерация отчёта
SELECT generate_report();

Прочие нюансы

  1. Каждый вызов функции PL/V8 изолирован, но можно сохранять глобальные данные в объекте plv8.global.

    CREATE FUNCTION set_global_var(val TEXT) RETURNS VOID AS $$
        plv8.global.my_var = val;
    $$ LANGUAGE plv8;
    
    CREATE FUNCTION get_global_var() RETURNS TEXT AS $$
        return plv8.global.my_var || ' is global!';
    $$ LANGUAGE plv8;
    
    -- Устанавливаем значение:
    SELECT set_global_var('Hello');
    SELECT get_global_var(); -- "Hello is global!"
  2. Избегайте использования eval() и динамических SQL-запросов. Лушче использовать параметры вместо конкатенации строк:

    plv8.elog(NOTICE, "Debug info: ", some_variable);
  3. Для отладки используйте plv8.elog:

    plv8.elog(NOTICE, "Debug info: ", some_variable);
  4. PL/V8 поддерживает выполнение JavaScript-кода на лету с помощью plv8.run_script. Полезно для выполнения динамических операций:

    DO $$ 
        plv8.run_script('globalThis.dynamicVar = "I am dynamic!"');
        plv8.elog(NOTICE, globalThis.dynamicVar);
    $$ LANGUAGE plv8;
    

Более подробно с библиотекой можно ознакомиться здесь.

В заключение рекомендую всем желающим обратить внимание на следующие открытые уроки:

  • 10 февраля: «Почему ClickHouse становится выбором №1 при импортозамещении: сравнение с другими NoSQL решениями и практический кейс». Подробнее

  • 11 февраля: «Запуск СУБД (MySQL, PostgreSQL) в Docker». Подробнее

Список всех бесплатных уроков по аналитике и анализу, а также по другим IT-направлениям, можно посмотреть в календаре.

Комментарии (2)


  1. Sabirman
    28.01.2025 09:23

    >> var plan = plv8.prepare("SELECT id, name FROM users WHERE age = $1", ['int']);

    Выглядит, как будто план каждый раз новый создается.


  1. Sabirman
    28.01.2025 09:23

    Какие есть ограничения при работе с глобальными переменными ?