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

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

2 апр. 2025 г.

Разбиение (split) строки на части по длине с помощью SQL

Попросили меня выполнить code-review скрипта, сгенерированного AI. Скрипт написан на PL/SQL, и выполняет следующие действия:

- во входной строке ищет и заменяет различные написания слова "строение", например "с.", "ст.", "стр-е" итп и заменяет на стандартизированное написание "стр.",

- затем разбивает полученную строку на части по набору разделителей (в данном примере это "," и ";")

- полученный набор строк дополнительно разбивается на подстроки по длине в 10 символов

Вот исходный код:

DECLARE
original_text VARCHAR2(4000) := 'Это строение, которое находится рядом со ст. 5; также есть стр-е в центре города. Это очень важное стро.';
modified_text VARCHAR2(4000);
chunk_size INTEGER := 10;
start_pos INTEGER := 1;
delimiter VARCHAR2(10) := ',;'; -- Разделители
current_chunk VARCHAR2(4000);
BEGIN
-- Заменяем слова
modified_text := REPLACE(original_text, 'строение', 'стр.');
modified_text := REPLACE(modified_text, 'стр-е', 'стр.');
modified_text := REPLACE(modified_text, 'стро.', 'стр.');
modified_text := REPLACE(modified_text, 'ст.', 'стр.');
DBMS_OUTPUT.PUT_LINE('Измененный текст: ' || modified_text);
-- Если строка больше 10 символов, делим ее на части
IF LENGTH(modified_text) > chunk_size THEN
-- Разделяем строку по разделителям
FOR chunk IN ( SELECT REGEXP_SUBSTR(modified_text, '[^'||delimiter||']+', 1, LEVEL) AS part FROM dual CONNECT BY REGEXP_SUBSTR(modified_text, '[^'||delimiter||']+', 1, LEVEL) IS NOT NULL ) LOOP current_chunk := TRIM(chunk.part); SELECT REGEXP_SUBSTR(modified_text, '[^'||delimiter||']+', 1, LEVEL) AS part
FROM dual
CONNECT BY REGEXP_SUBSTR(modified_text, '[^'||delimiter||']+', 1, LEVEL) IS NOT NULL
) LOOP
current_chunk := TRIM(chunk.part);
start_pos := 1; -- Делим текущую часть на подстроки по 10 символов
WHILE start_pos <= LENGTH(current_chunk) LOOP
DBMS_OUTPUT.PUT_LINE(SUBSTR(current_chunk, start_pos, chunk_size));
start_pos := start_pos + chunk_size;
END LOOP;
END LOOP;
ELSE
DBMS_OUTPUT.PUT_LINE(modified_text);
END IF;
END;
/

Казалось бы, ну чего тут такого... ну код как код, написан без особых изысков, можно сказать, что "в лоб". Но работает, ничего особо плохого сказать не могу. По ощущениям, написан программистом среднего уровня, ближе к начинающему. 

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

declare
    original_text varchar2(4000) := 'Это строение, которое находится рядом со ст. 5; также есть стр-е в центре города. Это очень важное стро.';
    modified_text varchar2(4000);
    chunk_size INTEGER := 10;
    delimiter     varchar2(10) := ',;'; -- разделители

begin
    dbms_output.put_line('Исходный текст  : ' || original_text);
    -- заменяем слова
    modified_text := regexp_replace(srcstr     => original_text,
                                pattern    => '(\W|^)(строение|стр-е|стро.|ст.)(\W|$)',
                                replacestr => '\1стр.\3',
                                position   => 1,
                                occurrence => 0,
                                modifier   => 'i');
    dbms_output.put_line('Измененный текст: ' || modified_text);

    -- если строка больше 10 символов, делим ее на части
    for chunk in (
        with q_chunks as (
            select rownum rn, regexp_replace(regexp_substr(modified_text, '[^'||delimiter||']+', 1, level), '(^\s+|\s+$)', null) t 
            from dual 
            connect by level <= regexp_count(modified_text, '[^'||delimiter||']+'))
       
select distinct chunk.rn, level, regexp_substr(chunk.t, '.{1,'||chunk_size||'}', 1, level) t
        from q_chunks chunk
        connect by regexp_substr(chunk.t, '.{1,'||chunk_size||'}', 1, level) is not null
        order by rn, level

    ) loop
       dbms_output.put_line(chunk.t);
    end loop;
end;
/

    Во первых, очевидно, что заменять различные написания на нечто стандартное - это вотчина регулярных выражений, непонятно, почему AI решил не пользоваться ими. Тем более, что очевидно, что требуется менять только отдельные слова, но не части слов, что нельзя сделать обычными сериями REPLACE. Т.е. исходный код уже логически неверен. Ну ладно, идем дальше. 
    Часть кода, которая делит текст по разделителям, в принципе работает корректно, причем тут как раз и использованы регулярные выражения. Я фактически оставил ее без изменений, только добавил вызов regexp_replace для удаления пробельных символов спереди и сзади порезанных частей строк. Непонятно, почему в исходном коде это действие вынесено в отдельный вызов TRIM, который удаляет только пробелы. 
    Далее начинается самое интересное, собственно тема поста. Я задумался - можно ли используя SQL разбить текст на строки по длине. Оказалось, что да - можно это сделать используя известный прием с select ... from dual connect by ...
Таким образом можно совместить шаг разбиения строки по разделителям с шагом разбиения подстрок по длине. Тут хотелось бы заметить, что у меня, как всегда, возникли некоторые трудности с увязыванием обоих иерархических запросов, поэтому не взыщите за использование rownum, distinct и order by, предлагаю списать это на небрежность написания примера.
    В целом результат моего code review - оптимизация предложенного кода, устранение логических ошибок, минимизация переключения контекстов выполнения, а также использование иерархического SQL для разбиения строки на подстроки по длине. Для более ясного понимания прикладываю простейший пример по теме с нарезкой строки по 5 символов с помощью SQL:

with q_data_to_split as (select '0123456789abcdefghijklnmopqrstuvwxyzABCDEFGHIJKLNMOPQRSTUVWXYZ' t, '.{1,5}' p from dual)
select regexp_substr(t, p, 1, level) t
from q_data_to_split
connect by regexp_substr(t, p, 1, level) is not null

На мой взгляд - это проще чем разбиение строки циклами в PL/SQL с использованием переменных. Вот PL/SQL аналог вышеприведенного запроса:

declare
    t varchar2(4000) := '0123456789abcdefghijklnmopqrstuvwxyzABCDEFGHIJKLNMOPQRSTUVWXYZ';
    chunk_size    integer := 5;
    start_pos integer := 1;
begin
    WHILE start_pos <= LENGTH(t) LOOP
        DBMS_OUTPUT.PUT_LINE(SUBSTR(t, start_pos, chunk_size));
        start_pos := start_pos + chunk_size;
    END LOOP;
end;