Привет! Меня зовут Зураб Диаконашвили, я разработчик в компании SM Lab. Сегодня поговорим об использовании полиморфных табличных функций в Oracle и рассмотрим их работу на примерах.

При работе с Oracle используются SQL-запросы – они помогают управлять базами данных, представленными в виде таблиц. Мы получили задачу на динамическое добавление полей в стационарный набор полей сущности. Для этого мы решили попробовать PTF-функцию (пользовательскую табличную функцию, которая вызывается в предложении FROM) и сравнить результат её работы с обычным SQL PIVOT и JavaScript.

Полиморфные табличные функции (PTF) являются частью стандарта SQL: 2016. В Oracle они представлены в версии 18c.

Что необходимо знать при работе с PTF:

  • PTF – это определяемые пользователем табличные функции, которые могут быть вызваны в предложении FROM;

  • тип строки результата (набор колонок) не объявляется при создании функции;

  • тип строки результата может зависеть от аргументов функции и, следовательно, от точного синтаксиса вызова;

  • PTF может использовать произвольное количество параметров, Oracle же требует один обязательный параметр – исходная таблица. Это может быть как таблица в БД, так и CTE (with-выражение).

С помощью PTF мы можем манипулировать как набором колонок итоговой таблицы, так и набором строк, т.е. можем добавлять/удалять колонки и добавлять/удалять строки.

Также следует обратить внимание, что в PTF-пакете может быть объявлено три процедуры и одна функция:

  • DESCRIBE function (обязательно);

  • FETCH_ROWS procedure (опционально);

  • OPEN procedure (опционально);

  • CLOSE procedure (опционально).

В обязательной функции DESCRIBE (описательная часть) указываем, какие колонки мы добавляем и исключаем из исходного набора данных, а также какие колонки должны быть доступны для чтения в процедуре FETCH_ROWS.

Из процедур основной является FETCH_ROWS, в которой мы получаем вычисленные отображаемые данные при выполнении SQL-запроса. OPEN и CLOSE – это необязательные процедуры. Их смысл в инициализации данных, которые хранятся в store для дальнейшего использования, и финализации расчетов (при необходимости) соответственно.

Время выполнения процедур/функций PTF-пакета:

  • функция DESCRIBE выполняется при компиляции SQL-запроса, т.е. для каждого SQL_ID функция DESCRIBE выполняется один единственный раз;

  • остальные процедуры вызываются в процессе выполнения SQL-запроса.

Рассмотрим на примерах.

Пример 1. Самый простой пример

/* Скрипт генерации тестовых данных */
create table t (
    a number,
    b number,
    c number
);
insert into t values (1,2,3);
insert into t values (4,5,6);
insert into t values (7,8,9);
commit;

/********************************************************************/
/* Пакет для работы с PTF*/
create or replace package ptf_package_1 as

function my_ptf
    (
        -- исходная таблица - это может быть как таблица БД, так и CTE (with t as () ...)
        p_tab in out table
    ) return
        table pipelined
        row   polymorphic
        using ptf_package_1;

function describe
    (
        -- исходная таблица
        p_tab in out dbms_tf.table_t
    ) return dbms_tf.describe_t;

end ptf_package_1;
/
create or replace package body ptf_package_1 as

function describe
    (
        -- исходная таблица
        p_tab in out dbms_tf.table_t
    ) return dbms_tf.describe_t
is
begin
    return null;
end;

end ptf_package_1;
/

Это примитивный, вырожденный пример, в нем PTF ничего не изменяет, а возвращает исходные данные как есть.

В этом пакете объявлена функция my_ptf, которую мы будем вызывать в SQL-запросе. У этой функции есть только заголовок и нет тела.

Мы указываем в объявлении функции:

  • table pipelined;

  • row|table polymorphic;

  • using <имя пакета>  (пакет, в котором описаны процедуры, необходимые для работы этой функции).

Тут можно указать одну из двух возможных семантик – строковая семантика (row polymorphic) или табличная семантика (table polymorphic).

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

Здесь функция DESCRIBE возвращает null и итогом работы PTF будут данные, которые ничем не отличаются от данных в исходной таблице, в чем мы и можем убедиться, выполнив SQL-запрос:

select * from ptf_package_1.my_ptf(t);

Пример 2. Добавление и удаление строк и колонок.

create or replace package ptf_package_2 as

--------------------------------------------------------------------------------
function my_ptf
    (
        p_tab          in out table,                 -- исходная таблица
        p_hide_cols    in     columns default null,  -- колонки к удалению
        p_new_col_name in     varchar2 default null, -- новая колонка
        p_new_col_val  in     varchar2 default null  -- значение для новой колонки
    ) return
        table pipelined
        row   polymorphic
        using ptf_package_2;

--------------------------------------------------------------------------------
function describe
    (
        p_tab          in out dbms_tf.table_t,          -- исходная таблица
        p_hide_cols    in     dbms_tf.columns_t default null, -- колонки к удалению
        p_new_col_name in     varchar2 default null,    -- новая колонка
        p_new_col_val  in     varchar2 default null     -- значение для новой колонки
    ) return DBMS_TF.DESCRIBE_T;

--------------------------------------------------------------------------------
procedure fetch_rows
    (
        p_new_col_name  in varchar2 default null,   -- новая колонка
        p_new_col_val   in varchar2 default null    -- значение для новой колонки
    );

end ptf_package_2;
/
create or replace package body ptf_package_2 as

--------------------------------------------------------------------------------
function describe
    (
        p_tab          in out dbms_tf.table_t,        -- исходная таблица
        p_hide_cols    in     dbms_tf.columns_t default null,  -- колонки к удалению
        p_new_col_name in     varchar2 default null,  -- новая колонка
        p_new_col_val  in     varchar2 default null   -- значение для новой колонки
    ) return dbms_tf.describe_t
is
    v_new_columns dbms_tf.columns_new_t;
begin
    -- пометим колонки которые нужно убрать: PASS_THROUGH := false
    for i in 1 .. p_tab.column.count
    loop
        if p_tab.column(i).description.name member of p_hide_cols then
            p_tab.column(i).pass_through := false;
            p_tab.column(i).for_read := false; -- значение по умолчанию, здесь для наглядности
        end if;
    end loop;

    -- добавим новую колонку
    v_new_columns(1) :=
        dbms_tf.column_metadata_t
        (
            name => p_new_col_name,
            type => dbms_tf.type_varchar2
        );

    -- вернем список добавляемых колонок
    return dbms_tf.describe_t( new_columns => v_new_columns );
end;

--------------------------------------------------------------------------------
procedure fetch_rows
    (
        p_new_col_name  in varchar2 default null,   -- новая колонка
        p_new_col_val   in varchar2 default null    -- значение для новой колонки
    )
as
    v_column_values dbms_tf.tab_varchar2_t;
    v_env           dbms_tf.env_t := dbms_tf.get_env();
begin
    DBMS_TF.Trace(v_env);
    -- Для всех строк задаем одно и тоже значение для новой колонки
    for i in 1..v_env.row_count loop
        v_column_values(nvl(v_column_values.last + 1, 1)) := p_new_col_val;
    end loop;

    -- вставим коллекцию значений новой колонки в итоговую таблицу
    dbms_tf.put_col(1, v_column_values);
end;

end ptf_package_2;
/


/* SQL-запрос для вызова PTF */
select * from ptf_package_2.my_ptf(t, columns(c), 'D', 'Hello world!');

Этот пример гораздо интереснее первого. Как было указано выше, с помощью PTF мы можем добавлять строки и колонки и удалять их из исходной таблицы. С исходной таблицей ничего не происходит, меняется только результирующий набор данных.

В функции DESCRIBE задается:

  • какие колонки следует скрыть из результата;

  • какие новые колонки следует добавить;

  • какие колонки следует прокидывать в результат;

  • значения каких колонок должны быть доступны в процедуре FETCH_ROWS.

Функция DESCRIBE возвращает тип DBMS_TF.DESCRIBE_T – список новых полей, которые должна вернуть PTF.

TYPE column_t IS RECORD (	
    description  COLUMN_METADATA_T,
    pass_through BOOLEAN,	-- прокидывать ли колонку в результат
    for_read     BOOLEAN	-- доступна ли колонка в FETCH_ROWS
);

При работе с PTF важно помнить о единственном ограничении: параметры и их наименования, указанные в этой функции, должны совпадать с параметрами, которые мы указываем в функции DESCRIBE; но при этом есть разница. В DESCRIBE мы указываем специальные типы, которые описаны в пакете DBMS_TF – это специальный пакет под полиморфные табличные функции. В PTF-функции первый параметр указан как table – это так называемый псевдооператор, который был создан специально под технологию PTF. Второй псевдооператор – это columns. Эти псевдооператоры мы указываем только в описании FTF-функции, которая будет вызываться в SQL-запросе. 

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

Снова обратимся к примеру. Мы исключаем колонки, которые перечислили в параметре hide_cols, и указываем, что параметр pass_through = false.

За что отвечает параметр PASS_THROUGH? Его смысл прост: если PASS_THROUGH имеет значение true, то это означает, что колонка прокидывается из первоначальной таблицы в выходную таблицу, выходные данные. Второй параметр FOR_READ отвечает за то, данные каких колонок будут доступны внутри FETCH_ROWS.

По умолчанию PASS_THROUGH = true , а FOR_READ = false;

Теперь добавление новой колонки. Новые колонки мы добавляем следующим образом: есть объект, который возвращает функция DESCRIBE, и одним из его атрибутов является коллекция, содержащая список новых колонок. 

В этом примере в процедуре FETCH_ROWS мы задаем для всех строк одно и то же значение, которое заполняется в коллекцию, и эта колонка вставляется в итоговые, результирующие данные. Здесь с помощью процедуры DBMS_TF.put_col вставляется новая колонка с указанными данными. Единичка тут означает первую колонку из вставляемых.

Важный момент: параметры в FETCH_ROWS должны быть правильными. Что значит правильными: по документации мы обязаны в FETCH_ROWS передать все параметры, которые указаны в PTF-функции, кроме тех параметров, которые указаны с помощью псевдооператоров, т.е. с помощью table и columns

Давайте посмотрим на результат работы.

Исходные данные:

Результат работы PTF:

Пример 3. Добавление реальных данных в новую колонку

create or replace package ptf_package_3 as

--------------------------------------------------------------------------------
function my_ptf
    (
        p_tab          in out table,                 -- исходная таблица
        p_new_col_name in     varchar2 default null  -- новая колонка
    ) return
        table pipelined
        row   polymorphic
        using ptf_package_3;

--------------------------------------------------------------------------------
function describe
    (
        p_tab          in out dbms_tf.table_t,       -- исходная таблица
        p_new_col_name in     varchar2 default null  -- новая колонка
    ) return DBMS_TF.DESCRIBE_T;

--------------------------------------------------------------------------------
procedure fetch_rows
    (
        p_new_col_name in     varchar2 default null  -- новая колонка
    );

end ptf_package_3;
/
create or replace package body ptf_package_3 as

--------------------------------------------------------------------------------
function describe
    (
        p_tab          in out dbms_tf.table_t,       -- исходная таблица
        p_new_col_name in     varchar2 default null  -- новая колонка
    ) return dbms_tf.describe_t
is
    v_new_columns dbms_tf.columns_new_t;
begin
    -- пометим все колонки доступными для чтения в процедуре FETCH_ROWS
    for i in 1 .. p_tab.column.count
    loop
        p_tab.column(i).for_read := true;
    end loop;

    -- добавим новую колонку
    v_new_columns(1) :=
        dbms_tf.column_metadata_t
        (
            name => p_new_col_name,
            type => dbms_tf.type_varchar2
        );

    -- вернем список НОВЫХ, добавляемых колонок
    return dbms_tf.describe_t( new_columns => v_new_columns );
end;

--------------------------------------------------------------------------------
procedure fetch_rows
    (
        p_new_col_name in     varchar2 default null  -- новая колонка
    )
as
    v_row_set    dbms_tf.row_set_t;
    v_new_column dbms_tf.tab_varchar2_t;
    v_row_count  pls_integer;
    v_env        dbms_tf.env_t := dbms_tf.get_env();
begin
    -- получаем данные из исходной таблицы
    DBMS_TF.Get_Row_Set
        (
            rowset    => v_row_set,
            row_count => v_row_count
        );

    -- выводим трассировку
    dbms_output.put_line('v_row_count = ' || v_row_count);
    dbms_tf.trace(v_row_set);
    dbms_tf.Trace(v_env);

    -- вычисляем значение для новой колонки - входные данные в JSON формате
    for i in 1..v_env.row_count loop
        v_new_column(i) := dbms_tf.row_to_char(v_row_set, i);
    end loop;

    -- вставим коллекцию значений новой колонки в строку итоговой таблицы
    DBMS_TF.Put_Col(1, v_new_column);
end;

end ptf_package_3;
/

Давайте сразу посмотрим на результат её работы:

Исходная таблица та же самая. Мы добавляем новую колонку, которую мы заполняем данными в формате JSON, т.е. здесь мы уже работаем с реальными данными. 

А как же мы это делаем?

В функции DESCRIBE указываем, что данные всех колонок нам нужны для чтения в процедуре FETCH_ROWS, т.е. в той процедуре, которая выполняется при SQL-запросе. Соответственно, мы указываем FOR_READ = true и добавляем новую колонку, указываем первым элементом коллекции эту колонку и передаем ее в результат. 

В процедуре FETCH_ROWS нам сперва нужно получить все данные из исходной таблицы с помощью DBMS_TF.Get_Row_Set. Мы получаем их в переменную v_row_set и в v_row_count количество строк, которые эта функция вернула.

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

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

Давайте рассмотрим результат трассировки. В разделе Get Columns перечисляется информация по тем колонкам, которые мы указали FOR_READ = true (в данном случае мы везде указали, что FOR_READ = true, поэтому здесь указаны все колонки).

И в разделе трассировки Put Columns выводится информация по новой колонке.

Пример 4. Разворачивание полей из структуры EAV (Entity Attribute Value) в обычный плоский табличный вид – главная цель тестирования PTF.

Скрипт генерации данных для примера:

-- create mock data

--
drop table ptf_100;
create table ptf_100 ( code number, field_name varchar2(100 char), value varchar2(100 char));
create unique index ptf_100_ui on ptf_100 (code, field_name);
declare
    c_rows constant number := 100;
    v_cur_count number(30) := 1;
    v_check number(30);
begin
    for i in
        (
            select rownum as code from dual t connect by level <= c_rows
        )
    loop
        -- dbms_output.put_line(i.code);
        insert into ptf_100 (code, field_name, value) values (i.code,  'FIRST_NAME', to_char(dbms_random.string('U', trunc(dbms_random.value(5, 10)))));
        insert into ptf_100 (code, field_name, value) values (i.code,  'LAST_NAME',  to_char(dbms_random.string('U', trunc(dbms_random.value(5, 20)))));
        insert into ptf_100 (code, field_name, value) values (i.code,  'GENDER',     to_char(decode(round(dbms_random.value(1, 2)), 1, 'MALE', 2, 'FEMALE')));
        insert into ptf_100 (code, field_name, value) values (i.code,  'PET',        to_char(decode(round(dbms_random.value(1, 4)), 1, 'Dog', 2, 'Cat', 3, 'Bird', 4, 'Reptile')));
        insert into ptf_100 (code, field_name, value) values (i.code,  'CITY',       to_char(decode(round(dbms_random.value(1, 10)), 1, 'Moscow', 2, 'Tokyo', 3, 'Osaka', 4, 'Cairo', 5, 'New York', 6, 'Mexico City', 7, 'Shanghai', 8, 'Dhaka', 9, 'Delhi', 10, 'Sao Paulo')));
        insert into ptf_100 (code, field_name, value) values (i.code,  'POSTCODE',   round(dbms_random.value(100000, 999999)));
        v_cur_count := v_cur_count + 1;
    end loop;
    -- dbms_output.put_line(v_cur_count);
    commit;
end;
/

Данные подготовлены, смотрим, как выглядит исходная таблица:

Это обычная EAV-структура, всем знакомая: у нас есть код сущности и атрибуты этой сущности, в данном случае имя, фамилия, пол, питомец, город, почтовый индекс. Они у нас даны в вертикальной структуре, а цель нашей задачи развернуть их, сделать аналог стандартной операции PIVOT.

create or replace package ptf_package_4 is

--------------------------------------------------------------------------------
function pivot_props
    (
        -- исходная таблица
        p_tab           in out table,
        -- заранее вычисленный список наименований пользовательских полей
        p_pivot_fields  in     columns,
        -- наименование ключевого поля в исходной таблице
        p_key_field     in     varchar2 := 'CODE',
        -- наименование поля в исходной таблице, содержащего наименования полей в итоговой таблице
        p_name_field    in     varchar2 := 'FIELD_NAME',
        -- наименование поля в исходной таблице, содержащего значения полей в итоговой таблице
        p_value_field   in     varchar2 := 'VALUE'
    ) return
        table pipelined
        table polymorphic
        using ptf_package_4;

--------------------------------------------------------------------------------
function describe
    (
        -- исходная таблица
        p_tab           in out dbms_tf.table_t,
        -- заранее вычисленный список наименований пользовательских полей
        p_pivot_fields  in     dbms_tf.columns_t,
        -- наименование ключевого поля в исходной таблице
        p_key_field     in     varchar2 := 'CODE',
        -- наименование поля в исходной таблице, содержащего наименования полей в итоговой таблице
        p_name_field    in     varchar2 := 'FIELD_NAME',
        -- наименование поля в исходной таблице, содержащего значения полей в итоговой таблице
        p_value_field   in     varchar2 := 'VALUE'
    ) return dbms_tf.describe_t;

--------------------------------------------------------------------------------
procedure fetch_rows
    (
        -- наименование ключевого поля в исходной таблице
        p_key_field     in varchar2 := 'CODE',
        -- наименование поля в исходной таблице, содержащего наименования полей в итоговой таблице
        p_name_field    in varchar2 := 'FIELD_NAME',
        -- наименование поля в исходной таблице, содержащего значения полей в итоговой таблице
        p_value_field   in varchar2 := 'VALUE'
    );

end ptf_package_4;
/
create or replace package body ptf_package_4 is

--------------------------------------------------------------------------------
/* В функции DESCRIBE определяюм описание итогового набора полей - можно добавлять/удалять поля.
Т.е. это описательная часть PTF, про метаданные */
function describe
    (
        -- исходная таблица
        p_tab           in out dbms_tf.table_t,
        -- заранее вычисленный список наименований пользовательских полей
        p_pivot_fields  in     dbms_tf.columns_t,
        -- наименование ключевого поля в исходной таблице
        p_key_field     in     varchar2 := 'CODE',
        -- наименование поля в исходной таблице, содержащего наименования полей в итоговой таблице
        p_name_field    in     varchar2 := 'FIELD_NAME',
        -- наименование поля в исходной таблице, содержащего значения полей в итоговой таблице
        p_value_field   in     varchar2 := 'VALUE'
    ) return dbms_tf.describe_t
is
    v_key_idx         pls_integer := 1;
    v_new_columns     dbms_tf.columns_new_t;
    v_descr           dbms_tf.describe_t;
    v_name_field_idx  pls_integer;
    v_value_field_idx pls_integer;
    v_key_field       varchar2(255 char) := upper(p_key_field);
    v_name_field      varchar2(255 char) := upper(p_name_field);
    v_value_field     varchar2(255 char) := upper(p_value_field);
    v_column_name     varchar2(255 char);
begin
    -- dbms_tf.trace(p_tab);
    -- Помечаем все колонки:
    --      PASS_THROUGH := false   -- колонки НЕ прокидываются из исходных данных в выходящие
    --      FOR_READ := true        -- чтобы их значения были доступны в процедуре FETCH_ROWS
    for i in 1 .. p_tab.column.count
    loop
        -- ни одну колонку исходной таблицы мы НЕ прокидываем в конечную, ключевая колонка в итоговой таблице будет заполняться вручную
        p_tab.column(i).pass_through := false;
        -- и соответственно каждую колонку помечаем for_read, чтобы их значения были доступны в FETCH_ROWS
        p_tab.column(i).for_read := true;

        -- получаем имя текущей колонки
        v_column_name := upper(replace(p_tab.column(i).description.name, '"'));

        -- вычисляем порядковые номера полей с наименованиями и со значениями разворачиваемых полей
        if v_column_name = v_name_field then
            -- порядковый номер поля с наименованиями
            v_name_field_idx := i;
        elsif v_column_name = v_value_field then
            -- порядковый номер поля со значениями
            v_value_field_idx := i;
        end if;

        -- ключевое поле указываем как вновь добавляемое
        if v_column_name = v_key_field then
            v_new_columns(v_key_idx) :=
                dbms_tf.column_metadata_t
                (
                    name => p_tab.column(i).description.name,
                    type => p_tab.column(i).description.type
                );
        end if;
    end loop;

    -- Добавляем новые поля (пользовательские, в данном случае)
    /* помним, что функция DESCRIBE выполняется при компиляции SQL запроса, а не при каждом выполнении SQL,
    соотв-но, если текст запроса не изменился, то и процедура DESCRIBE не будет вызвана и
    список ноых полей останется таким каким он был вычислен при последней компиляции SQL */
    if p_pivot_fields is not null then
        for i in 1 .. p_pivot_fields.count
        loop
            v_new_columns(i + 1/* учитываем ключевое поле */) :=
                dbms_tf.column_metadata_t
                (
                    name => p_pivot_fields(i),
                    type => dbms_tf.type_varchar2
                );
        end loop;
    end if;

    -- сохраняем в стор порядковые номера колонок с наименованиями атрибутов и их значениями
    v_descr.cstore_num('name_field_idx') := v_name_field_idx;
    v_descr.cstore_num('value_field_idx') := v_value_field_idx;

    -- добавим новые поля в результат
    v_descr.new_columns := v_new_columns;
    -- взводим флаг репликации, чтобы мы могли манипулировать количеством строк в итоговой таблице
    v_descr.row_replication := true;

    -- Вернем описание новых полей
    return v_descr;
end;

--------------------------------------------------------------------------------
/* Процедура FETCH_ROWS вызывается при выполнении SQL она отвечает за данные, выводимые в полях, заданных в функции DESCRIBE */
procedure fetch_rows
    (
        -- наименование ключевого поля в исходной таблице
        p_key_field     in varchar2 := 'CODE',
        -- наименование поля в исходной таблице, содержащего наименования полей в итоговой таблице
        p_name_field    in varchar2 := 'FIELD_NAME',
        -- наименование поля в исходной таблице, содержащего значения полей в итоговой таблице
        p_value_field   in varchar2 := 'VALUE'
    )
is
    v_inp_rs          dbms_tf.row_set_t;
    v_out_rs          dbms_tf.row_set_t;
    v_env             dbms_tf.env_t := dbms_tf.get_env();
    v_colcnt_in       pls_integer;
    v_rowcnt_in       pls_integer;
    repfac            dbms_tf.tab_naturaln_t;  -- массив флагов удален/неуадлен для всех строк
    v_name_field_idx  pls_integer;
    v_value_field_idx pls_integer;
    v_key_idx         pls_integer := 1;
    v_col_name        varchar2(256 char);
    v_cur_row_name    varchar2(256 char);
    v_found           boolean;
begin
    -- получаем значения из стора - используем DBMS_TF.CStore_Get для чтения данных, записанных в функции DESCRIBE
    DBMS_TF.CStore_Get('name_field_idx', v_name_field_idx);
    DBMS_TF.CStore_Get('value_field_idx', v_value_field_idx);

    /* DBMS_TF.XStore_Get - если потребуется хранение и передача данных между разными вызовами FETCH_ROWS */

    -- получаем исходные данные для тех полей, которые помечены как for_read
    dbms_tf.get_row_set(v_inp_rs, v_rowcnt_in, v_colcnt_in);

    -- по умолчанию все строки помечаем удаленными
    for i in 1 .. v_rowcnt_in loop
        repfac(i) := 0;
    end loop;

    -- и только первую строку сохраняем
    repfac(1) := 1;

    -- переносим значение ключевой колонки из входных данных в выходные (первая колонка: v_key_idx = 1)
    if v_env.put_columns(v_key_idx).type = dbms_tf.type_number then
        v_out_rs(v_key_idx).tab_number(1) := v_inp_rs(v_key_idx).tab_number(1);
    elsif v_env.put_columns(v_key_idx).type = dbms_tf.type_varchar2 then
        v_out_rs(v_key_idx).tab_varchar2(1) := v_inp_rs(v_key_idx).tab_varchar2(1);
    end if;

    -- формируем строку в итоговой таблице (начиная со следующей после ключевой колонки)
    for c in v_key_idx + 1 .. v_env.put_columns.count loop
        v_found := false;
        v_col_name := upper(trim(replace(v_env.put_columns(c).name, '"')));
        for row_idx in 1 .. v_rowcnt_in loop
            exit when v_found;
            v_cur_row_name := upper(trim(v_inp_rs(v_name_field_idx).tab_varchar2(row_idx)));
            -- заполняем значения для новых полей
            if v_cur_row_name = v_col_name then
                v_found := true;
                v_out_rs(c).tab_varchar2(1) := v_inp_rs(v_value_field_idx).tab_varchar2(row_idx);
            end if;
        end loop;
        if not v_found then
            -- значение для новых полей обязательно заполняем, иначе PTF заглючит, но ошибку не выдаст!!
            -- это связано с тем, что где-то внутри механизма PTF выскакивет ошибка NO_DATA_FOUND, но наверх ошибка не райзится
            v_out_rs(c).tab_varchar2(1):= null;
        end if;
    end loop;

    -- вставляем строку в набор выходных данных
    DBMS_TF.Put_Row_Set(v_out_rs, repfac);
end;

end ptf_package_4;
/

И результат работы функции:

По-прежнему самое интересное здесь в DESCRIBE и FETCH_ROWS. Важно помнить: DESCRIBE у нас выполняется один раз при компиляции SQL-запроса, т.е. для каждого SQL_ID функция DESCRIBE выполняется один единственный раз, в то время как FETCH_ROWS выполняется при каждом SQL-запросе с участием данной PTF-функции. 

Небольшая ремарка: до сих пор мы рассматривали только строковые семантики, а в данном случае, обратите внимание, параметр table не row_polymorphic как в предыдущих случаях, а table_polymorphic – это как раз означает, что это табличная семантика и если посмотреть на запрос, то там указано PARTITION BY CODE.

Для PTF с табличной семантикой нам обязательно надо это учитывать, т.к. если мы указываем, что это табличная семантика, но при этом не указываем PARTITION BY CODE или ORDER BY, она будет работать неправильно и выдавать некорректную информацию.

Каким образом мы добились того, что из четырех строк исходной таблицы мы получили одну единственную строчку?

Рассмотрим детально как это сделано и как это работает.

Мы должны указать системе, что в процессе выполнения процедуры FETCH_ROWS из той пачки строк, которая нам пришла (данные нам приходят пачками, т.к. мы здесь указали PARTITION BY CODE), все строки, кроме первой, удаляем и все колонки удаляем и добавляем новые колонки со всеми нужными атрибутами. 

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

Далее мы запоминаем порядковые номера полей с названием и значением атрибута, т.е. мы записываем их в отдельные переменные, которые в дальнейшем с помощью механизма работы со store передаем в  процедуру FETCH_ROWS.

Соответственно, мы все колонки из этого списка добавляем в результат работы функции DESCRIBE как новые колонки.

Итого DESCRIBE у нас выполнил свои задачи: мы указали FOR_READ = true для каждой колонки, т.е. не прокидываем ни одну колонку, зато все колонки открываем для чтения в FETCH_ROWS и указываем, какие новые колонки у нас будут в нашей результирующей таблице.

Идем в процедуру FETCH_ROWS.

Что здесь у нас происходит? Во-первых, мы получаем из store сохраненные значения, индексы полей с названием и значением атрибутов. Хочу заметить, что есть также возможность передачи данных не только между DESCRIBE и FETCH_ROWS, но и между несколькими вызовами этого FETCH_ROWS, т.е. если нам необходимо сделать какие-то накопительные вычисления, агрегации, то с помощью  XStore_Get мы можем передавать значения между разными вызовами FETCH_ROWS.

После прочтения номеров полей из store начинается самое интересное: мы все строки, кроме первой, пометили к удалению, т.е. у нас есть специальная переменная – это массив флагов “удален / не удален” для строк. По сути в этой коллекции будет столько элементов, сколько у нас строк обработки в данном вызове FETCH_ROWS.

Далее мы сначала заполняем значение ключевой колонки, т.е. значение поля CODE, а потом в цикле значения всех атрибутов записываем соответствующие поля строки.

Важный момент. Нам обязательно нужно присвоить новым колонкам либо какие-то вычисленные значения, либо, если мы не смогли вычислить ничего, обязательно указать null. Иначе это приведет к проблемам. Я это сейчас продемонстрирую.

Исключаем код, который заполняет колонку с невычисленными значениями:

И получаем результат:

То есть, если мы исключим из логики код, который заполняет колонку с невычисленными значениями, получаем ерунду. Это связано со следующим: где-то внутри логики, внутри реализации самого PTF выскакивает ошибка NO_DATA_FOUND, но до нас этот NO_DATA_FOUND, этот exception не доходит. Он выскакивает внутри и там же перехватывается, причем перехватывается так, что мы никаких ошибок не видим, видим только печальный результат. 

Вернемся к примеру. Мы заполнили все поля и далее с помощью DBMS_TF.Put_Row_Set вставляем набор строк и вторым параметром указываем нашу коллекцию, в которой перечислены флаги с удаленными/неудаленными строками. В нашем случае, напоминаю еще раз,  у нас пришла пачка строк. Мы фактически всем этим строкам указываем, что они удаленные, оставляем только первую строчку. Её наполняем уже своими новыми колонками и в результате получаем одну строчку вместо всей пачки строк.

Несколько слов про возможные ошибки при выполнении SQL с PTF:

  • нужно быть внимательными с параметрами, т.к. при выполнении SQL могут возникать самые неожиданные ошибки: например, я несколько раз получал ошибки типа “ORA-03 113: end‑of‑file on communication channel”;

  • если внутри FETCH_ROWS возникнет NO_DATA_FOUND, то есть высокая вероятность, что ошибка будет подавлена и вы получите некорректные данные;

  • если параметры процедур не соответствуют правилам, вылезет ошибка "ORA-62573: new column (…) is not allowed with describe only polymorphic table function".

Сравнение производительности по сравнению с обычным PIVOT и JavaScript

Я тестировал PTF на следующих данных: 

И что мы получили? PTF сильно уступает. Значительные потери производительности возникают на уровне большого объема данных.

В целом можно сказать, что у PTF есть какая-то своя ниша, но нужно хорошо думать нужно вам это использовать или нет.

Где ее можно реально использовать? В таких задачах, где мы обязаны на стороне базы выполнить динамический разворот из вертикальной структуры в горизонтальную (именно на стороне базы). Результирующий набор колонок нам при этом заранее неизвестен это динамический момент. Только в таком случае есть смысл использовать PTF, в других он себя совершенно не оправдывает, учитывая, что есть разные неожиданные ошибки.

На нескольких форумах я также встречал описания бредовых не только ошибок, но и результатов, связанных с какой-то хитрой внутренней логикой, до понимания которой никто не доходил. Из всего этого можно сделать вывод, что технология пока сырая. Следовательно, без острой нужды к PTF можно не обращаться.

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


  1. Akina
    04.02.2025 11:58

    Я правильно понимаю, что в общем в том, что положить во второй и прочие параметры, мы никак не лимитированы? То есть мы можем передать там имя второй, связанной, таблицы, и вернуть соответственно паровоз с дополнительными данными в дополнительных полях?

    Опять же, я правильно понимаю, что строки возвращаемого функцией набора формально никак не коррелируют с исходными строками по количеству? Во всяком случае в последнем примере pivot-таблица должна вернуть куда как меньше строк, чем было в исходной таблице, и, видимо, возможна обратная ситуация, когда количество выходных строк будет больше, чем в исходной таблице (ну формально тот же unpivot).

    Ну и вторичный вопрос - как порождается выходной набор физически? Сначала код полностью обрабатывает источник данных, формируя полный выходной набор, и только потом строки выходного набора фетчатся? Если да, означает ли это, что выходной набор безусловно материализуется?


    1. Zurab-D
      04.02.2025 11:58

      Я правильно понимаю, что в общем в том, что положить во второй и прочие параметры, мы никак не лимитированы?

      Да, все так

      То есть мы можем передать там имя второй, связанной, таблицы, и вернуть соответственно паровоз с дополнительными данными в дополнительных полях?

      Да, конечно

      Опять же, я правильно понимаю, что строки возвращаемого функцией набора формально никак не коррелируют с исходными строками по количеству? Во всяком случае в последнем примере pivot-таблица должна вернуть куда как меньше строк, чем было в исходной таблице, и, видимо, возможна обратная ситуация, когда количество выходных строк будет больше, чем в исходной таблице (ну формально тот же unpivot).

      Я не пробовал добавлять лишние строки, на насколько я понимаю, нам ничто не мешает это сделать - см. DBMS_TF.Put_Row_Set()

      На последний вопрос не готов ответить


  1. svi0105
    04.02.2025 11:58

    А где сам пакет ptf_package_4 ?


    1. Reverie_moon
      04.02.2025 11:58

      Забыли про него) добавили уже


      1. svi0105
        04.02.2025 11:58

        Спасибо)


  1. Mur466
    04.02.2025 11:58

    В таблице производительности вертикальная шкала в каких единицах?