До недавнего времени для реализации сложной многошаговой логики в экосистеме Apache Spark разработчикам приходилось выходить за рамки декларативного SQL. Оркестрация последовательных вызовов, вычисление промежуточных переменных и ветвление логики требовали привлечения внешних языков программирования, таких как Python (PySpark) или Scala, и дополнительных инструментов.

Spark SQL Scripting, который стал доступен, начиная с 4-й версии, кардинально меняет этот подход, представляя собой процедурное расширение классического Spark SQL. Теперь разработчики могут писать полноценные многошаговые сценарии непосредственно на уровне SQL-артефактов, внедряя в них управляющую логику.

В данной публикации мы, команда вендора Data Sapience, разберем возможности Spark scripting на практике.

Давайте для начала определимся с типовыми сценариями применения, когда использование функционала Spark SQL Scripting будет являться хорошим подспорьем»

  • Пакетные DDL/DML-последовательности обработки
    Последовательные операторы подготовки данных и выполнение трансформации данных, создание или пересоздание таблиц, заполнение их новыми расчетами, обновление объектов целевого слоя хранилища (SCD1-, SCD2-сценарии);

  • Подготовка и расчет витрин данных
    Реализация классических шагов загрузки из промежуточного слоя (staging) в целевые таблицы (target) с обязательным контролем промежуточных состояний;

  • Проверки качества данных (Data Quality / Sanity checks)
    Расчет метрик качества прямо по ходу выполнения скрипта, сравнение их с пороговыми значениями и принятие решения: продолжать выполнение процесса или аварийно остановиться;

  • Runbook-операции
    Запуск регламентных сценариев обслуживания инфраструктуры данных вручную или автоматически (например, очистка временных объектов или сбор системной статистики).

Как правило, все эти случаи используют динамический SQL, который мы не будем выделять в отдельный сценарий. Ведь зачастую именно ради генерации исполняемого SQL-кода на основе переменных или метаданных мы и используем любые процедурные расширения.

Возможность применения Spark Scripting управляется конфигурационным флагом:

spark.sql.scripting.enabled

Структура Spark SQL Script

Любой скрипт в рамках процедурного расширения Spark SQL представляет собой так называемый compound statement (составной оператор). Это означает, что логический блок всегда жестко ограничен рамками ключевых слов BEGIN ... END.

Внутри этого блока традиционно выделяются две логические части:

  1. Секция объявлений
    Здесь разработчик может задекларировать локальные переменные, задать условия и определить обработчики ошибок (handlers).

  2. Тело скрипта
    Непосредственная последовательность операторов.

В теле скрипта допускается использование широкого спектра конструкций:

  • Условные ветвления IF и CASE;

  • Циклические конструкции LOOP, WHILE, REPEAT и специализированный цикл FOR для итерации по строкам результата SQL-запроса;

  • Операторы управления циклами: LEAVE (аналог break в традиционных языках) и ITERATE (аналог continue);

  • Стандартные DDL- и DML-команды (например, CREATE, DROP, INSERT);

  • Обычные запросы SELECT, которые могут возвращать результирующие наборы вызывающей стороне;

  • Конструкции присвоения значений переменным SET и SET VAR;

  • Механизм динамического выполнения EXECUTE IMMEDIATE;

  • Вложенные блоки BEGIN ... END для изоляции областей видимости.

Крайне важным архитектурным аспектом является то, что составной оператор выполняется в режиме NOT ATOMIC. На практике это означает, что, если один из операторов внутри блока завершается ошибкой, ранее успешно выполненные шаги автоматически не откатываются.

Также это означает, что логику обработки нужно проектировать с абсолютным упором на идемпотентность и безопасность. При падении сценария на середине пути вам придется либо вручную компенсировать изменения, либо проектировать шаги обработки так, чтобы их повторный запуск не приводил к дублированию или порче данных.

Другим важным ограничением является то, что пока (апрель 2026) отсутствует команда CREATE STORED PROCEDURE для последующего вызова через CALL-оператор. 

Работа с переменными

Процедурный SQL немыслим без механизмов сохранения промежуточного состояния. Spark SQL Scripting предлагает два уровня работы с переменными.

Различают два вида переменных:

  • Локальные переменные
    Они объявляются строго внутри блока BEGIN ... END с помощью ключевого слова DECLARE и существуют только во время выполнения данного блока.
    Если вы используете вложенные блоки BEGIN ... END, они порождают свои локальные области видимости. При этом важно помнить о «затенении»: если во внутреннем блоке объявить переменную с таким же именем, как и во внешнем, внутреннее объявление перекроет внешнее значение на время работы подблока;

  • Сессионные переменные
    Если вам необходимо передавать значения между физически разными SQL-запросами или скриптами в рамках одной Spark-сессии, на помощь приходят сессионные переменные:

  • Объявление: DECLARE VARIABLE ...

  • Изменение значения: SET VAR ...

Обратите внимание на синтаксис: Команда SET без ключевого слова VAR относится к настройкам конфигурации Spark (SQLConf) и не умеет работать с SQL-переменными.

Практические примеры

Для самостоятельного воспроизведения практических примеров вам необходимо создать тестовый объект следующим скриптом

DROP TABLE IF EXISTS demo_orders;

CREATE TABLE demo_orders (
  order_id      BIGINT,
  customer_id   BIGINT,
  region        STRING,
  amount        DECIMAL(12,2),
  order_ts      TIMESTAMP,
  status        STRING,          -- NEW / OK / CANCELLED
  ingestion_dt  DATE
) USING parquet;

INSERT INTO demo_orders VALUES
  (1001, 501, 'EU',   10.00, TIMESTAMP '2026-03-01 10:00:00', 'NEW', DATE '2026-03-01'),
  (1002, 501, 'EU',   35.50, TIMESTAMP '2026-03-01 11:00:00', 'NEW', DATE '2026-03-01'),
  (1003, 502, 'EU',  120.00, TIMESTAMP '2026-03-01 12:00:00', 'OK',  DATE '2026-03-01'),
  (1004, 503, 'US',    5.00, TIMESTAMP '2026-03-01 10:30:00', 'NEW', DATE '2026-03-01'),
  (1005, 503, 'US',   60.00, TIMESTAMP '2026-03-01 13:00:00', 'OK',  DATE '2026-03-01'),
  (1006, 504, 'US',   15.00, TIMESTAMP '2026-03-02 09:00:00', 'OK',  DATE '2026-03-02'),
  (1007, 505, 'APAC',200.00, TIMESTAMP '2026-03-02 10:00:00', 'OK',  DATE '2026-03-02'),
  (1008, 506, 'APAC', 12.00, TIMESTAMP '2026-03-02 11:00:00', 'NEW', DATE '2026-03-02'),
  (1009, 507, 'APAC', 90.00, TIMESTAMP '2026-03-03 08:00:00', 'OK',  DATE '2026-03-03'),
  (1010, 508, 'EU',    1.00, TIMESTAMP '2026-03-03 09:00:00', 'NEW', DATE '2026-03-03');
—-

—-

Сценарий: один и тот же скрипт запускается с разными параметрами (дата/регион/порог) без правки текста.

Параметры запуска (1 раз на сессию объявить, дальше только SET VAR):

DECLARE VARIABLE p_dt DATE DEFAULT DATE '2026-03-01';
DECLARE VARIABLE p_region STRING DEFAULT 'EU';
DECLARE VARIABLE p_min_amount DECIMAL(12,2) DEFAULT 50.00;

SET VAR p_dt = DATE '2026-03-02';
SET VAR p_region = 'APAC';
SET VAR p_min_amount = 10.00;
--

Скрипт:

BEGIN
  DECLARE v_dt DATE DEFAULT session.p_dt;
  DECLARE v_region STRING DEFAULT session.p_region;
  DECLARE v_min DECIMAL(12,2) DEFAULT session.p_min_amount;

  SELECT
    v_dt AS ingestion_dt,
    v_region AS region,
    v_min AS min_amount,
    COUNT(*) AS cnt,
    CAST(COALESCE(SUM(amount), 0) AS DECIMAL(12,2)) AS sum_amount
  FROM demo_orders
  WHERE ingestion_dt = v_dt
    AND region = v_region
    AND amount >= v_min;
END;

Сценарий: посчитать метрики батча за переданную дату.

BEGIN
  DECLARE v_dt DATE DEFAULT session.p_dt;
  DECLARE v_cnt BIGINT;
  DECLARE v_sum DECIMAL(12,2);

  SET v_cnt = (SELECT COUNT(*) FROM demo_orders WHERE ingestion_dt = v_dt);
  SET v_sum = (
    SELECT CAST(COALESCE(SUM(amount), 0) AS DECIMAL(12,2))
    FROM demo_orders
    WHERE ingestion_dt = v_dt
  );
  SELECT v_dt AS ingestion_dt, v_cnt AS rows_cnt, v_sum AS amount_sum;
END;

В сложных запросах может возникнуть конфликт между именами переменных и именами колонок в таблицах. По умолчанию Spark пытается разрешить имя как колонку или алиас в таблице. И только если такое имя не найдено, оно будет интерпретировано как переменная.

Чтобы избежать случайных логических ошибок и коллизий, рекомендуется явно квалифицировать переменные префиксами:

  • session.<имя_переменной>

  • system.session.<имя_переменной>

Ограничение: сессионные переменные не могут использоваться в персистентных (сохраненных в метасторе) объектах — например, в сохраненных представлениях (persisted view), выражениях по умолчанию для колонок или генерируемых колонках. Их жизненный цикл привязан только к текущей активной сессии.

Динамический SQL

EXECUTE IMMEDIATE выполняет SQL-оператор, сформированный как строка.

Поддерживает:

  • USING — биндинг параметров (позиционно или по имени);

  • INTO — запись результата single-row query в переменные:

    • 0 строк → NULL;

    • > 1 строки → ошибка;

    • INTO разрешен только для query (не для DDL/DML).

Ограничения, которые важно учитывать:

  • вложенные EXECUTE IMMEDIATE запрещены;

  • в EXECUTE IMMEDIATE нельзя передавать SQL Script: строка не должна содержать BEGIN … END.

Динамическое формирование запросов — мощный, но опасный инструмент. Spark SQL Scripting предоставляет встроенные механизмы для минимизации рисков SQL-инъекций.

Если вам необходимо подставить в запрос динамическое имя таблицы, схемы или функции, никогда не делайте это через обычную конкатенацию строк. Используйте встроенную функцию:

  • IDENTIFIER(strExpr) – это механизм безопасного темплейтинга идентификаторов, снижающий риски синтаксических сбоев и злонамеренных инъекций.

Давайте проверять на практике

 Сценарий: динамический запрос с биндингом значений + получение скаляров в переменные. EXECUTE IMMEDIATE + USING + INTO

BEGIN
  DECLARE v_region STRING DEFAULT 'EU';
  DECLARE v_min DECIMAL(12,2) DEFAULT 20.00;
  DECLARE v_cnt BIGINT;
  DECLARE v_sum DECIMAL(12,2);
  EXECUTE IMMEDIATE
    'SELECT COUNT(*) FROM demo_orders WHERE region = ? AND amount >= ?'
    INTO v_cnt
    USING v_region, v_min;
  EXECUTE IMMEDIATE
    'SELECT CAST(COALESCE(SUM(amount), 0) AS DECIMAL(12,2))
       FROM demo_orders
      WHERE region = :r AND amount >= :m'
    INTO v_sum
    USING (v_region AS r, v_min AS m);
  SELECT v_region AS region, v_min AS min_amount, v_cnt AS cnt, v_sum AS sum_amount;
END;

Пример с  IDENTIFIER(strExpr). Сценарий: имя таблицы задается строкой (безопасная подстановка идентификатора).

BEGIN
  DECLARE v_table STRING DEFAULT 'demo_orders';
  DECLARE v_cnt BIGINT;
  SET v_cnt = (SELECT COUNT(*) FROM IDENTIFIER(v_table));
  SELECT v_table AS table_name, v_cnt AS row_count;
END;

Управляющие конструкции: ветвления и циклы

Управляющие конструкции – важнейший функционал, позволяющий реагировать на данные «на лету». Наряду с динамическим SQL является одной из самых востребованных функций процедурных расширений.

Внутри блоков доступны стандартные операторы условных ветвлений:

  • IF ... THEN ... ELSEIF ... ELSE ... END IF

  • CASE ... END CASE

Они идеально подходят для анализа вычисленных ранее метрик. Например, посчитав количество некорректных строк, вы можете направить скрипт либо по ветке фиксации данных, либо по ветке генерации алерта.

Примеры

Сценарий: DataQuality-гейт по батчу: если есть отрицательные суммы — стоп, иначе — proceed.

BEGIN

  DECLARE v_dt DATE DEFAULT session.p_dt;
  DECLARE v_bad BIGINT;
  DECLARE v_decision STRING;

  SET v_bad = (SELECT COUNT(*) FROM demo_orders WHERE ingestion_dt = v_dt AND amount < 0);
  IF v_bad = 0 THEN
    SET v_decision = 'PROCEED';
  ELSEIF v_bad < 10 THEN
    SET v_decision = 'PROCEED_WITH_WARNING';
  ELSE
    SET v_decision = 'STOP';
  END IF;
  SELECT v_dt AS ingestion_dt, v_bad AS bad_rows, v_decision AS decision;

END;

Сценарий:

  • CASE-оператор выбирает ставку комиссии по региону (параметр session.p_region);

  • CASE-выражение маркирует строки по размеру заказа.

BEGIN

  DECLARE v_region STRING DEFAULT session.p_region;
  DECLARE v_fee_rate DECIMAL(6,4);

  CASE v_region
    WHEN 'EU'   THEN SET v_fee_rate = CAST(0.0200 AS DECIMAL(6,4));
    WHEN 'US'   THEN SET v_fee_rate = CAST(0.0300 AS DECIMAL(6,4));
    WHEN 'APAC' THEN SET v_fee_rate = CAST(0.0500 AS DECIMAL(6,4));
    ELSE            SET v_fee_rate = CAST(0.0100 AS DECIMAL(6,4));
  END CASE;
  SELECT
    order_id,
    region,
    amount,
    CASE
      WHEN amount >= 100 THEN 'HIGH'
      WHEN amount >= 50  THEN 'MEDIUM'
      ELSE 'LOW'
    END AS amount_band,
    CAST(amount * v_fee_rate AS DECIMAL(12,2)) AS fee
  FROM demo_orders
  WHERE region = v_region
  ORDER BY order_id;

END;

Для циклической обработки доступны следующие конструкции:

  • LOOP ... END LOOP — бесконечный цикл, требующий явного выхода;

  • WHILE <condition> DO ... END WHILE — цикл с предусловием;

  • REPEAT ... UNTIL <condition> END REPEAT — цикл с постусловием;

  • FOR [var AS] <query> DO ... END FOR — потрясающий инструмент для итерации по результирующему набору строк.

Для гибкого управления итерациями используются операторы:

  • LEAVE <label> — немедленный выход из помеченного блока или цикла;

  • ITERATE <label> — переход к следующему витку помеченного цикла.

Использование меток (label:) обязательно при наличии сложной вложенности, чтобы интерпретатор точно понимал, из какого именно цикла вы хотите выйти или какую итерацию пропустить.

Практические примеры

Сценарий: сканируем заказы по order_id, пропускаем «мелочь» < 10 (ITERATE), выходим, когда накопили порог (LEAVE).

BEGIN
  DECLARE v_threshold DECIMAL(12,2) DEFAULT CAST(250 AS DECIMAL(12,2));
  DECLARE v_sum DECIMAL(12,2) DEFAULT CAST(0 AS DECIMAL(12,2));
  DECLARE v_last BIGINT DEFAULT 0;
  DECLARE v_next BIGINT;
  DECLARE v_amt  DECIMAL(12,2);

scan: LOOP

    SET v_next = (SELECT MIN(order_id) FROM demo_orders WHERE order_id > v_last);
    IF v_next IS NULL THEN LEAVE scan; END IF;
    SET v_amt  = (SELECT amount FROM demo_orders WHERE order_id = v_next);
    SET v_last = v_next;
    IF v_amt < 10 THEN
      ITERATE scan;   -- continue
    END IF;
    SET v_sum = v_sum + v_amt;
    IF v_sum >= v_threshold THEN
      LEAVE scan;     -- break
    END IF;

  END LOOP scan;

  SELECT v_last AS last_order_id, v_sum AS accumulated_sum;

END;

Сценарий: пройти диапазон дат (параметры) и подсчитать общий объем

Параметры диапазона:

DECLARE VARIABLE p_from_dt DATE DEFAULT DATE '2026-03-01';
DECLARE VARIABLE p_to_dt   DATE DEFAULT DATE '2026-03-03';

SET VAR p_from_dt = DATE '2026-03-01';
SET VAR p_to_dt   = DATE '2026-03-03';

Скрипт:

BEGIN
  DECLARE v_dt  DATE DEFAULT session.p_from_dt;
  DECLARE v_end DATE DEFAULT session.p_to_dt;

  DECLARE v_days INT DEFAULT 0;
  DECLARE v_total DECIMAL(12,2) DEFAULT CAST(0 AS DECIMAL(12,2));
  DECLARE v_day_sum DECIMAL(12,2);

  WHILE v_dt <= v_end DO
    SET v_day_sum = (
      SELECT CAST(COALESCE(SUM(amount), 0) AS DECIMAL(12,2))
      FROM demo_orders
      WHERE ingestion_dt = v_dt
    );
    SET v_total = v_total + v_day_sum;
    SET v_days = v_days + 1;
    SET v_dt = date_add(v_dt, 1);

  END WHILE;

  SELECT v_days AS days_processed, v_total AS total_amount;
END;

Сценарий: polling с лимитом попыток: ждем «NEW >= target», иначе выходим по лимиту.

Скрипт:

BEGIN
  DECLARE v_target BIGINT DEFAULT 10;      -- специально больше, чем есть в данных
  DECLARE v_attempt INT DEFAULT 0;
  DECLARE v_max_attempts INT DEFAULT 4;
  DECLARE v_cnt BIGINT DEFAULT 0;
  DECLARE v_reason STRING;

  REPEAT

    SET v_attempt = v_attempt + 1;
    SET v_cnt = (SELECT COUNT(*) FROM demo_orders WHERE status = 'NEW');
  UNTIL v_cnt >= v_target OR v_attempt >= v_max_attempts

  END REPEAT;

  IF v_cnt >= v_target THEN
    SET v_reason = 'FOUND';
  ELSE
    SET v_reason = 'MAX_ATTEMPTS';
  END IF;

  SELECT v_cnt AS new_cnt, v_attempt AS attempts, v_reason AS stop_reason;

END;

Сценарий: пройти по регионам и посчитать сумму по заказам amount >= min (итерация по result set).

Скрипт:

BEGIN

  DECLARE v_min DECIMAL(12,2) DEFAULT CAST(10 AS DECIMAL(12,2));
  DECLARE v_regions INT DEFAULT 0;
  DECLARE v_total DECIMAL(12,2) DEFAULT CAST(0 AS DECIMAL(12,2));

  FOR r AS
    SELECT DISTINCT region
    FROM demo_orders
    ORDER BY region
  DO
    SET v_total = v_total + (
      SELECT CAST(COALESCE(SUM(amount), 0) AS DECIMAL(12,2))
      FROM demo_orders
      WHERE region = r.region AND amount >= v_min
    );

    SET v_regions = v_regions + 1;
  END FOR;

  SELECT v_regions AS regions_cnt, v_min AS min_amount, v_total AS total_amount;

END;

Обработка исключений и системные обработчики (handlers)

Чтобы ваши скрипты не падали «молча» или не оставляли после себя заблокированные ресурсы и грязные данные, вы можете описывать обработчики ошибок.

Конструкция DECLARE EXIT HANDLER FOR позволяет реагировать на сбои. Такой обработчик перехватывает ошибку, выполняет заданную логику и полностью прекращает выполнение текущего составного оператора.

Обработчики могут настраиваться на:

  • Конкретные именованные классы ошибок;

  • Стандарты SQLSTATE;

  • Обобщенное исключение SQLEXCEPTION;

  • Состояние NOT FOUND (полезно при чтении данных, когда отсутствие строк является допустимым бизнес-сценарием).

Сценарий: перехватить деление на ноль и вернуть понятный результат вместо «падения».

-- Чтобы деление на 0 гарантированно было ошибкой (а не NULL), включаем ANSI-режим.
SET spark.sql.ansi.enabled = true;

BEGIN
  DECLARE EXIT HANDLER FOR DIVIDE_BY_ZERO

  h: BEGIN
    SELECT 'FAILED' AS status, 'DIVIDE_BY_ZERO' AS error;
  END h;

  -- провоцируем ошибку
  SELECT 1 / 0;
  SELECT 'OK' AS status;  -- не выполнится
END;

Сценарий: любая ошибка → понятный финальный результат.

BEGIN

  DECLARE v_step STRING DEFAULT 'start';
  DECLARE EXIT HANDLER FOR SQLEXCEPTION

  h: BEGIN

    SELECT 'FAILED' AS status, v_step AS failed_step;

  END h;

  SET v_step = 'compute';
  SELECT 1 / 0;  -- ошибка для демонстрации
  SET v_step = 'never';
  SELECT 'OK' AS status;

END;

Сценарий: ошибка на одном элементе цикла не валит весь run: считаем warnings и продолжаем.

BEGIN

  DECLARE v_warn INT DEFAULT 0;
  DECLARE v_total DECIMAL(12,2) DEFAULT CAST(0 AS DECIMAL(12,2));

  FOR r AS
    SELECT DISTINCT region FROM demo_orders ORDER BY region
  DO
    maybe_fail: BEGIN
      DECLARE EXIT HANDLER FOR SQLEXCEPTION
      h: BEGIN
        SET v_warn = v_warn + 1;
      END h;
      IF r.region = 'APAC' THEN
        SELECT 1 / 0;   -- «ломаем» один шаг
      END IF;
    END maybe_fail;
    SET v_total = v_total + (
      SELECT CAST(COALESCE(SUM(amount), 0) AS DECIMAL(12,2))
      FROM demo_orders
      WHERE region = r.region
    );
  END FOR;

  SELECT v_total AS sum_amount, v_warn AS warnings;

END;

Сценарий: вставили строку → ошибка → вставка уже видна (при первом запуске после 6.0 будет 1 строка). Успешные шаги не откатываются автоматически!

BEGIN

  DECLARE EXIT HANDLER FOR SQLEXCEPTION

  h: BEGIN
    SELECT COUNT(*) AS test_rows
    FROM demo_orders
    WHERE status = 'TEST_FAIL';
  END h;

  INSERT INTO demo_orders VALUES
    (9999, 999, 'EU', 0.01, current_timestamp(), 'TEST_FAIL', current_date());
  SELECT 1 / 0;  -- ошибка

END;

Работа с вложенными блоками

Работа с вложенными блоками BEGIN ... END в Spark SQL Scripting — это не только способ сделать код «красивым», это возможность управлять изоляцией ошибок, контролем видимости и структурной чистотой кода.

Вложенные блоки позволяют визуально и логически разделить скрипт на этапы: Блок инициализации, Блок трансформации, Блок очистки.

Блокам можно давать метки (например, step_1: BEGIN ... END step_1). Это облегчает отладку и понимание того, на каком именно этапе произошел сбой или задержка.

Пример

Сценарий: показать, что переменная во вложенном BEGIN … END может затенять внешнюю, и это не ломает внешний контекст.

BEGIN

  DECLARE v_region STRING DEFAULT 'EU';
  DECLARE v_outer_sum DECIMAL(12,2);
  DECLARE v_inner_sum DECIMAL(12,2);

  SET v_outer_sum = (
    SELECT CAST(COALESCE(SUM(amount), 0) AS DECIMAL(12,2))
    FROM demo_orders
    WHERE region = v_region AND status IN ('NEW','OK','CANCELLED')
  );
  inner_block: BEGIN
    DECLARE v_region STRING DEFAULT 'APAC';  -- затеняет внешнюю v_region
    SET v_inner_sum = (
      SELECT CAST(COALESCE(SUM(amount), 0) AS DECIMAL(12,2))
      FROM demo_orders
      WHERE region = v_region AND status IN ('NEW','OK','CANCELLED')
    );
  END inner_block;

  SELECT
    v_outer_sum AS eu_sum,
    v_inner_sum AS apac_sum;

END;

Заключение

Давайте сформулируем свод практических рекомендаций и архитектурных паттернов.

Опираясь на архитектурные особенности Spark SQL Scripting, мы на практике выделили набор правил разработки:

  1. В Lakehouse-платформе Data Ocean Nova используйте Spark scripting, только если вы работаете с окружением Spark. Для быстрого старта мы заранее создали готовые Spark-ноутбуки с примерами использования всех операторов. Если основная обработка ваших данных или сервисов реализована с помощью MPP-движков StarRocks или Impala, то стоит использовать процедурное расширение платформы LPSQL, возможности которого даже шире, чем Spark SQL Scripting;

  2. Помните, что в текущей версии (апрель 2026) отсутствует возможность сохранения процедуры для дальнейшего вызова через команду CALL. Стоит архитектурно предусмотреть хранение кода;

  3. Проектируйте с учетом NOT ATOMIC – забудьте про классические транзакции. Ваши операции должны быть идемпотентными: используйте CREATE IF NOT EXISTS, INSERT OVERWRITE вместо обычной вставки, а также атомарный оператор MERGE;

  4. Изолируйте области видимости переменных. При работе в больших скриптах с вложенными блоками необходимо избегать дублирования имен переменных, чтобы не запутаться в логике перекрытия контекстов;

  5. Строго разделяйте шаблонизацию и передачу значений. Никогда не склеивайте строки динамического SQL вручную. Используйте строго IDENTIFIER() для объектов и USING для параметров;

  6. Контролируйте размерность в EXECUTE IMMEDIATE INTO. Если вы запрашиваете скалярное значение в переменную через INTO, гарантируйте на уровне SQL, что вернется не более одной строки (например, используя агрегацию или LIMIT 1);

  7. Помните о природе распределенных систем. Циклы отлично подходят для задач оркестрации (пройти по списку дат или конечному списку значений), но они категорически не подходят для построчной (row-by-row) обработки больших массивов данных, ведь мы имеем дело не с операционной OLTP СУБД. Для тяжелых расчетов всегда используйте классические реляционные операции (JOIN, GROUP BY), оставляя циклы только для управляющих структур.

Spark SQL Scripting — это не просто синтаксический сахар, а эволюционный шаг в сторону сближения классического функционала аналитических СУБД (таких как Oracle PL/SQL, MS SQL Server T-SQL) с мощью распределенных вычислений Apache Spark. Использование Scripting позволяет инженерам данных собирать пайплайны обработки на «чистом SQL», не прибегая к сторонним компонентам и языкам разработки, тем самым сокращая кодовую базу и снижая барьер входа для дата-аналитиков.

Подписывайтесь на блог Data Sapience на Habr, чтобы узнавать о наших публикациях первыми.