Иногда возникает задача сделать анализ строки посимвольно. В большинстве случаев на помощь приходят регулярные выражения. Но они могут помочь не во всех случаях, и тогда приходится разработчикам писать свой анализатор.
На момент написания статьи я не нашёл в документации PostgreSQL функции, которая бы выполняла разделение строки на символы. Поэтому пришлось искать альтернативные способы.
Использование regexp_split_to_array
Напишем тест для оценки производительности
CREATE OR REPLACE FUNCTION test_regexp_split_to_array() RETURNS void AS $$ DECLARE _start_time timestamp; _end_time timestamp; BEGIN _start_time := clock_timestamp(); PERFORM regexp_split_to_array(repeat('*', 1000), ''); _end_time := clock_timestamp(); RAISE LOG '1000: %', _end_time - _start_time; _start_time := clock_timestamp(); PERFORM regexp_split_to_array(repeat('*', 2000), ''); _end_time := clock_timestamp(); RAISE LOG '2000: %', _end_time - _start_time; _start_time := clock_timestamp(); PERFORM regexp_split_to_array(repeat('*', 5000), ''); _end_time := clock_timestamp(); RAISE LOG '5000: %', _end_time - _start_time; _start_time := clock_timestamp(); PERFORM regexp_split_to_array(repeat('*', 10000), ''); _end_time := clock_timestamp(); RAISE LOG '10000: %', _end_time - _start_time; _start_time := clock_timestamp(); PERFORM regexp_split_to_array(repeat('*', 30000), ''); _end_time := clock_timestamp(); RAISE LOG '30000: %', _end_time - _start_time; END $$ LANGUAGE plpgsql;SELECT test_regexp_split_to_array(); LOG: 1000: 00:00:00.000047 LOG: 2000: 00:00:00.000025 LOG: 5000: 00:00:00.000031 LOG: 10000: 00:00:00.000043 LOG: 30000: 00:00:00.000107Хороший результат. Однако он был получен лишь на серверах с версией PostgreSQL 9.2.x и 9.1.x Остальные серверы с версиями субд 9.0.x и 8.4.x показали результат на порядки хуже. Вот результат для версии 9.0.13
LOG: 1000: 00:00:00.007928 LOG: 2000: 00:00:00.027251 LOG: 5000: 00:00:00.187229 LOG: 10000: 00:00:00.549404 LOG: 30000: 00:00:05.752846Есть над чем задуматься...
Пишем свою хранимую процедуру text_to_chars
Текст процедуры прямолинеен и прост
CREATE OR REPLACE FUNCTION text_to_chars(IN text) RETURNS SETOF varchar AS $BODY$ DECLARE _text ALIAS FOR $1; _i int4; BEGIN FOR _i IN 1 .. length(_text) LOOP RETURN NEXT substring(_text from _i for 1); END LOOP; END; $BODY$ LANGUAGE plpgsql;Пишем тест
CREATE OR REPLACE FUNCTION test_text_to_chars() RETURNS void AS $$ DECLARE _start_time timestamp; _end_time timestamp; BEGIN _start_time := clock_timestamp(); PERFORM text_to_chars(repeat('*', 1000)); _end_time := clock_timestamp(); RAISE LOG '1000: %', _end_time - _start_time; _start_time := clock_timestamp(); PERFORM text_to_chars(repeat('*', 2000)); _end_time := clock_timestamp(); RAISE LOG '2000: %', _end_time - _start_time; _start_time := clock_timestamp(); PERFORM text_to_chars(repeat('*', 5000)); _end_time := clock_timestamp(); RAISE LOG '5000: %', _end_time - _start_time; _start_time := clock_timestamp(); PERFORM text_to_chars(repeat('*', 10000)); _end_time := clock_timestamp(); RAISE LOG '10000: %', _end_time - _start_time; _start_time := clock_timestamp(); PERFORM text_to_chars(repeat('*', 30000)); _end_time := clock_timestamp(); RAISE LOG '30000: %', _end_time - _start_time; END $$ LANGUAGE plpgsql;Запуск теста на сервере с версией 9.0.13
SELECT test_text_to_chars(); LOG: 1000: 00:00:00.020169 LOG: 2000: 00:00:00.051273 LOG: 5000: 00:00:00.243298 LOG: 10000: 00:00:00.742167 LOG: 30000: 00:00:05.965905Результат сопоставим с использованием встроенной функции regexp_split_to_array и это удивительно - я ожидал худших результатов.
Из результатов теста видно, что время выполнения процедуры растет в геометрической прогрессии относительно длины текста. Смею даже предположить, что функция substring получает доступ к символу в строке, отсчитав нужное количество символов от начала строки, то есть никакие индексы/оптимизации не используются. Поэтому на длинных строках большая часть процессорного времени тратится на вычисление смещения в памяти для позиционирования на нужный символ в строке. Попробую оптимизировать этот процесс, разбив строки на подстроки и уже из этих подстрок выделять символы.
CREATE OR REPLACE FUNCTION text_to_chars(IN text) RETURNS SETOF varchar AS $BODY$ DECLARE _text ALIAS FOR $1; _length int4; _i int4; _segment_count int4; _segment_length int4; _segment_index int4; _start_index int4; _end_index int4; _result varchar[]; _segment varchar; _max_segment_length int4; BEGIN _length := length(_text); IF _length = 0 THEN RETURN; END IF; _max_segment_length := ceil(sqrt(_length)); _segment_count := ceil(_length::numeric / _max_segment_length); FOR _segment_index IN 0.._segment_count - 1 LOOP _start_index := _segment_index * _max_segment_length + 1; _segment_length := LEAST(_start_index + _max_segment_length, _length + 1) - _start_index; _segment := substring(_text from _start_index for _segment_length); FOR _i IN 1.._segment_length LOOP RETURN NEXT substring(_segment from _i for 1); END LOOP; END LOOP; END $BODY$ LANGUAGE plpgsql;Запускаю тест
SELECT test_text_to_chars(); LOG: 1000: 00:00:00.016238 LOG: 2000: 00:00:00.035769 LOG: 5000: 00:00:00.075568 LOG: 10000: 00:00:00.192204 LOG: 30000: 00:00:00.529772Время выполнения в 10 раз лучше, чем в функции, которая использовала substring в лоб. Результат, конечно, далек от результата, который показывает regexp_split_to_array на серверах с версией >= 9.1, но он на порядок лучше "стандартного" решения.