Записки жертвы высоких технологий

    Здравствуйте! Мой блог посвящен в большей своей части моей профессиональной деятельности на поприще использования технологий ORACLE для разработки баз данных OLTP и OLAP хранилищ данных. В заметках я периодически размещаю разнообразные SQL, PL/SQL и Java скрипты написанные мной и не только мной, ссылки на интересные источники в сети либо другую полезную информацию, которая каким либо образом касается моей работы с замечательными продуктами компании ORACLE.
    Вы можете связаться со мной по контактному емейлу, если у вас есть какие-либо вопросы, связанные с разработкой баз данных на основе продуктов ORACLE, буду рад помочь вам, если это будет в моих силах.

7 окт. 2015 г.

Получение названия таблиц из произвольного SQL запроса

Проблема: Понадобилось сделать генератор SQL скриптов для переноса данных по таблицам. Простой путь: Нашел в сети топорный шаблон под названием fn_gen_inserts(updates) и вознамерился было попользоваться им, но понял, что код не удовлетворяет моим и без того скромным требованиям к генерации скриптов. У меня SQL скриптами осуществляется перенос данных с сервера разработки на сервер тестирования и промышленный сервер. Поэтому задавать имена целевых таблиц и их владельцев, как того требуют авторы исходных кодов вышеназванных функций, как-то несуразно. Как известно, пакет DBMS_SQL имеет несколько простых и удобных методов описания столбцов заданного динамически SQL запроса, но описания таблиц - нет. Как мне кажется, в случае анализа простых SQL запросов, вполне можно попытаться определить название таблицы и ее владельца в автоматическом режиме.
Задача: По заданному строкой SQL запросу определять название опорной таблицы и ее владельца. Итак, кусочек PL/SQL кода, где переменная p_sql - передаваемый SQL запрос

     ...

    -- 1) Evaluate execution plan for SQL query
    begin
        execute immediate 'explain plan for '||p_sql;
    exception when others then
        raise_application_error(-20001, 'ERROR: SQL query cannot be parsed ('||SQLERRM||')');
    end;
    -- 2) Getting the name of the first table in access order list (99,99%)
    begin
        select table_name into l_base_table_name from (
            select regexp_substr(acc, '^[|]\s*(\w+)\s*[|]',1,1,'i',1) idx, regexp_substr(acc, '[|]\s*((\w|[$#_])+)\s*[|]$',1,1,'i',1) table_name 
            from (
                        select plan_table_output acc from table(dbms_xplan.display(nullnull'BASIC -ROWS -COST -PREDICATE -PARALLEL -PARTITION -PROJECTION -ALIAS -NOTE -REMOTE'))
            ) tabaccinfo
            where tabaccinfo.acc like '%TABLE ACCESS%' order by idx
        ) maintabacc where rownum = 1;
    exception when others then
        raise_application_error(-20002, 'ERROR: Cannot get the name of the source table ('||SQLERRM||')');
    end;
    -- 3) Get primary table owner from SQL query as a first name from the list of possible owners
    begin
        select owner into l_base_table_owner from (
            select owner from all_tables t where t.table_name = l_base_table_name order by decode(t.owner, user12)
        ) t where rownum = 1;
    exception when others then
        raise_application_error(-20003, 'ERROR: Cannot get the owner of the source table ('||SQLERRM||')');
    end;

     ...

    после этого можно обработать переданный запрос в DBMS_SQL, чтобы обработать описания столбцов запроса. 

Разумеется, есть несколько допущений, которые значительно ограничивают применимость данного подхода. Вот они:
1) Совершенно необязательно, что основная таблица запроса будет идти первой в порядке доступа в плане выполнения SQL запроса или в случае выгрузки из пайплайновой функции такой таблицы вообще может не быть. В таких случаях я явно задаю название целевой таблицы
2) Совершенно не факт, что владельца таблицы можно получить просто получив отсортированный список владельцев одноименного объекта. В случае, если владелец объекта не может быть определен таким образом, я задаю его вручную, явно.

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

24 дек. 2013 г.

Аббревиатуры из названий

В нашей серьезной организации все отделы должны иметь аббревиатурные названия, непонятные стороннему человеку (сиречь потенциальному шпиёну). Должны, но не обязаны. Поэтому у нескольких отделов таких аббревиатур нет. При переносе справочника на другой сервер пришлось озадачиться автоматической генерацией аббревиатур из названий отделов. Разумеется, помогли регулярные выражения. Приведенный ниже пример формирует аббревиатуры из названий отделов, выбрасывая союз " и " и тире из названий, как ненужные.

select
    -- переводим строку в верхний регистр
    upper(
        -- удаляем пробелы, схлопывая оставшиеся буквы в аббревиатуру
        regexp_replace(
            -- удаляем сформированные последовательности из исходной строки
            regexp_replace(
                rs_name,
                -- формируем шаблон типа (<динамическая последовательность1> | <динамическая последовательность2>[|...] | <фиксированная последовательность1>[|...])
                '('||
                -- убираем размноженные перечислители, заменяя их на одиночные
                regexp_replace(
                    -- убираем лишний перечислитель из начала предложения
                    regexp_replace(
                        -- убираем начальные буквы из каждого слова в предложении, заменяя их на перечислители "|"
                        regexp_replace(
                            rs_name,
                            '(^|\s)\w',
                            '|'
                        ),
                        '^[|]+',
                        null
                    ),
                    '[|][|]+',
                    '|'
                )||
                -- добавляем блок фиксированных последовательностей " и "
                '|\s+и\s+|\s*-]s*)',
                null
            ),
            '\s',
            null
        )
    )
from
    (
        select regexp_replace(rs_name, '(Радио|радио)(навигационн|локационн|астрономическ|вещательн)''\1 \2') rs_name, rs_abbr from DIC_RADIOSERVICE t
    ) t

Приведенный алгоритм также учитывает некоторые общепринятые сокращения отдельных слов, таких как: радионавигационная - РН, а не Р, радиолокационная - РЛ, а не Р, радиоастрономическая - РА, но не Р, радиовещательная - РВ, но не Р. Для этого в исходное название между "Радио" и остатком названия службы вставляется пробел, чтобы в результате получилось бы правильное сокращение.

5 дек. 2013 г.

Очередной "шедевр" формирования результирующего набора из строки с разделителями

Ковырялся в коде старого приложения на предмет его анализа и наткнулся на "мощный алгорим", формирующий результирующий набор строк из строки с разделителями, например из такой: "1,2,3,4,5,6,7,8,9,10,a,b,c,d,e,f":

with src as (SELECT ','||'1,2,3,4,5,6,7,8,9,10,a,b,c,d,e,f'||',' str from dual)
select 
level lvl
trim(substr(Src.Str,instr(Src.Str,',',1,level)+chunk, 
instr (Src.Str,',',1,level+1)-instr(Src.Str,',',1,level)-1)) op_num
from 
src
connect by 
rownum<=(length(Src.Str)-length(replace(Src.Str,',')))-1

Код, конечно, работает, но:
1. В этом коде реально неудобно разбираться
2. Этот код не учитывает наличие пробелов или каких-либо "лишних символов" между разделителями, а по идее - должен

Варианты альтернативного исполнения:

1. Конвейерная (pipelined) функция
2. Функция, возвращающая массив
3. Regexp'ы (пример есть у меня в разделе "Сравнение строк по сигнатурам".

PS. Я бы вот так вот написал:

with params as (select '\s*([^,]+)\s*(,|$)' pattern,'1,2,3,4,5,6,7,   8,  9    ,10,,12, a, bb, ccc , dddd' string from dual)
select 
regexp_substr(params.string, params.pattern, 1level'i',1chunk 
from 
params 
connect by
regexp_substr(params.string, params.pattern, 1level'i',1is not null

9 окт. 2013 г.

Сравнение строк по сигнатурам

Мне понадобилось сравнивать две строки по сигнатурам (вхождениям символов в строку), то есть строки <A><B><C> и <B><A><C> в моем случае должны быть равны. Я, например, использую это сравнение при анализе имеющихся у таблицы индексов, чтобы в коде понять, проиндексирован ли интересующий меня список столбцов, если на входе у меня есть только строки с перечислением имен этих столбцов, чтобы отбросить дублированные списки.

declare
    l_1 varchar2(1024) := '<a><b><c>';
    l_2 varchar2(1024) := '<b><c><a>';
    l_equal varchar2(32);
begin
   
    select
        decode(count(*),0,'EQUAL''NOT EQUAL') is_equal
    into
        l_equal
    from
        (select regexp_substr(l_1, '\<[^>]*\>',1,level,'i'chunk from dual t1 connect by regexp_substr(l_1, '\<[^>]*\>',1,level,'i'is not null) t1
        full outer join (select regexp_substr(l_2, '\<[^>]*\>',1,level,'i'chunk from dual t2 connect by regexp_substr(l_2, '\<[^>]*\>',1,level,'i'is not null) t2on t1.chunk = t2.chunk
    where t1.chunk is null or t2.chunk is null;
    dbms_output.put_line(l_equal);

end;

4 окт. 2013 г.

Случайно на GitHub'е наткнулся на неискомую, но давно желанную вещь - набор интерфейсных функций для использования Java-библиотек регулярных выражений непосредственно на уровне PL/SQL для того, чтобы обойти ограничения, налагаемые POSIX стандартом на реализацию регулярных выражений в ORACLE


С помощью этой библиотеки, например, становится возможным выполнять замены с опережающими и ретроспективными проверками:

select xt_regexp.replace_all('Please, make me happy!','(?<=Please)(,)','$1 Scott!!!!') from dual;
Result: Please, Scott!!!! make me happy!**


13 сент. 2013 г.

Получение имени текущего метода с помощью директив $$PLSQL_*

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


"Ошибка в строке #17 метода PKG_PERSONS.CreateNewPerson(..): Не указана фамилия физического лица"

Очередная она потому что какой дурак ее не пишет разработчик эту проблему не решает. Уродливая она потому, что в PL/SQL просто обязана быть системная переменная, типа $$PLSQL_METHOD_NAME, содержащая имя текущего PL/SQL метода/процедуры/функции, а все прочие методы получения - очевидно уродливы. Кстати, вроде бы в 12-ой версии ORACLE эту проблему кое-как решили, добавив пакет UTL_CALL_STACK, о чем счастливо отрапортовали Том Кайт и документация ORACLE. А, что делать нам, несчастным и отсталым?!...

Короче, выглядит все это вот так:


   function get_fname(p_unit in varchar2, p_line in numberreturn varchar2
    is
        l_name varchar2(30);
    begin
        select distinct
            max(tt.proc_name) keep (dense_rank last order by tt.line) over() text
        into l_name
        from
        (
        select
            t.proc_name,
            t.line
        from
        (
            select
                regexp_substr(text, '(^|;\s*|\s+is\s+|)(procedure|function)\s+(return)*(\w+)\s*(\(|is)*',1,1,'i',4) proc_name,
                line
            from
                user_source s
            where
                s.type in ('TYPE BODY''PACKAGE BODY''TRIGGER''PROCEDURE''FUNCTION'and
                s.name = upper(p_unit) and
                line <= p_line
        ) t
        where t.proc_name is not null and
        upper(t.proc_name) in (select procedure_name from user_procedures p where p.object_name = upper(p_unit))
        order by line
        ) tt;
        return '"' || UPPER(sys_context('USERENV''CURRENT_SCHEMA')) || '"' || '.' || '"' || l_name || '"';
    end;


Пакет, в котором размещена функция, создан с указанием AUTHID CURRENT_USER. 

Вызов метода в коде:

...
raise_application_error(-20001, 'Ошибка в строке #'||$$PLSQL_LINE||' метода ' || PKG_DEBUG_TOOLS.get_fname($$PLSQL_UNIT, $$PLSQL_LINE)  || '(..): Не указана фамилия физического лица');'
...


P.S. Функцию поиска имени метода в USER_SOURCE, конечно, можно обмануть, вставив, например, между объявлением метода пакета и вызовом функции литерал, в котором будет описан вызов какого-либо другого метода пакета, только вот зачем...

P.P.S. 02.10.2013 Переписал метод, чтобы было более красиво и менее ресурсоемко, заодно исправил ошибку в разборе имени функции

P.P,P.S. 10.03.2023 Понаписал за прошедшее время разных определялок текущих методов и пакетов. Вот прилагаю, если у кого-то еще остались вопросы. Выбираем на свой вкус. Я сейчас предпочитаю п2 для определения текущего метода и п3 для определения текущего пакета (oracle 19c).

  1. ThisPackageAndMethod varchar2(128) := regexp_substr(regexp_substr(dbms_utility.format_call_stack, 'package\s+body\s+([^[:space:]]+)',1,1,'i',1), '[^.]+[.][^.]+$');
  2. ThisMethod           varchar2(128) := utl_call_stack.concatenate_subprogram(utl_call_stack.subprogram(1));
  3. ThisPackage          varchar2(128) := $$PLSQL_UNIT;
  4. ThisPackage          varchar2(128) := nvl(regexp_substr(dbms_utility.format_call_stack, 'package\sbody\s(((\w)+[.])?(\w+))',1,1,'i',4), 'task_'||to_char(sysdate, 'YYYYMMDDHH24MISS'));