Содержание

Предыдущий раздел

Особенности работы с большими массивами

Следующий раздел

Различия в работе volatile и stable функций

Разделение строки на символы

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

На момент написания статьи я не нашёл в документации 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, но он на порядок лучше "стандартного" решения.

Дополнительная информация

PostgreSQL Documentation: String Functions and Operators

comments powered by Disqus