Эта статья завершает цикл о миграции с СУБД Oracle на СУБД PostgreSQL. В первых двух статьях рассматривались вопросы и устоявшиеся способы переноса данных из одной СУБД в другую (часть 1, часть 2). В третьей статье представлена часть особенностей, которые нужно учесть при переводе хранимого кода с PL/SQL на PL/pgSQL. В сегодняшнем материале рассматривается оставшаяся часть особенностей, адаптация и конвертация кода, включая выбор средств для конвертации.

Глобальные структуры данных уровня пакета

Для таких структур рекомендуется использовать модуль pg_variables. Он позволяет сохранять как скалярные значения, так и множество записей, массивы.

При этом нужно понимать, требуется ли собирать статистику для планировщика. Если да, то придётся пользоваться временными таблицами. По возможности, их лучше не использовать слишком интенсивно. Создание и удаление временных таблиц ведёт к изменениям в системном каталоге и сообщениям об инвалидации. Может возникнуть ситуация, когда серверным процессам для своей работы придётся многократно перечитывать системный каталог.

Пример: у одного клиента процессы СУБД тратили большое количество времени на планирование запросов, поскольку они многократно пытались прочитать данные  pg_statistic и pg_class и при этом взять соответствующие блокировки. pg_statistic и pg_class являются одними из наиболее часто используемых объектов СУБД, поэтому время ожидания получения блокировки было существенным. Соответственно, от создания и удаления временных таблиц на каждую транзакцию пришлось отказаться.

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

Пользовательские исключения

В языке PL/SQL разработчик может создать свои собственные пользовательские исключения для обработки ошибок. Они могут быть созданы в блоке объявлений подпрограммы, при этом использовать их можно только внутри неё. Вызвать исключение нужно явно с помощью оператора RAISE или процедуры RAISE_APPLICATION_ERROR пакета DBMS_STANDARD. Ниже приведён пример:

CREATE TABLE customers (
    id INTEGER,
    name VARCHAR2(100),
    address VARCHAR2(2000)
);

INSERT INTO customers VALUES(1, 'Тестовый пользователь 1', 'Тестовый адрес 1');
INSERT INTO customers VALUES(2, 'Тестовый пользователь 2', 'Тестовый адрес 2');
INSERT INTO customers VALUES(3, 'Тестовый пользователь 3', 'Тестовый адрес 3');

ALTER TABLE customers ADD CONSTRAINT customers_pk PRIMARY KEY (id);

DECLARE
    c_id customers.id%type := &cc_id;
    c_name customers.name%type;
    c_addr customers.address%type;
    
    ex_invalid_id EXCEPTION; 
BEGIN
    IF c_id <= 0 THEN
        RAISE ex_invalid_id;
    END IF;
    
    SELECT c.name, c.address INTO c_name, c_addr
      FROM customers c
     WHERE c.id = c_id;

    dbms_output.put_line('Имя: '||  c_name);  
    
    dbms_output.put_line('Адрес: ' || c_addr);       
EXCEPTION
    WHEN ex_invalid_id THEN
        dbms_output.put_line('Идентификатор клиента не может быть отрицательным');
    WHEN no_data_found THEN
        dbms_output.put_line('Клиент не найден');
END;

Ниже приведён пример перевода кода на СУБД PostgreSQL:

CREATE TABLE customers (
    id bigint,
    name VARCHAR(100),
    address VARCHAR(2000)
);

INSERT INTO customers VALUES(1, 'Тестовый пользователь 1', 'Тестовый адрес 1');
INSERT INTO customers VALUES(2, 'Тестовый пользователь 2', 'Тестовый адрес 2');
INSERT INTO customers VALUES(3, 'Тестовый пользователь 3', 'Тестовый адрес 3');

ALTER TABLE customers ADD CONSTRAINT customers_pk PRIMARY KEY (id);

CREATE OR REPLACE FUNCTION ex_invalid_id() RETURNS text AS
$$
    SELECT '06502';
$$
LANGUAGE SQL IMMUTABLE; 

DO
$$
DECLARE
    c_id customers.id%type := 5;
    c_name customers.name%type;
    c_addr customers.address%type;
BEGIN
    IF c_id <= 0 THEN
        RAISE EXCEPTION USING errcode = ex_invalid_id();
    END IF;
    
    SELECT c.name, c.address INTO STRICT c_name, c_addr
      FROM customers c
     WHERE c.id = c_id;
     
    RAISE NOTICE 'Имя: %', c_name;
    RAISE NOTICE 'Адрес: %', c_addr;   
EXCEPTION
    WHEN no_data_found THEN
        RAISE NOTICE 'Клиент не найден';
    WHEN others THEN
        CASE SQLSTATE
            WHEN ex_invalid_id() THEN
                RAISE NOTICE 'Идентификатор клиента не может быть отрицательным';
        END CASE;
END
$$
LANGUAGE plpgsql;

Но есть один нюанс, касающийся исключений и их обработки. Во время выполнения BEGIN создаётся точка сохранения (SAVEPOINT), т.е. подтранзакция. Если главная транзакция успешно завершена, то будут подтверждены изменения всех её подтранзакций. Но если подтранзакция по какой-то причине прервалась, то её изменения отменяются. При обработке блока EXCEPTION неявно вызывается команда ROLLBACK TO SAVEPOINT.

При этом у каждой транзакции есть свой кеш подтранзакций, в котором хранятся их статусы. При создании снимка данных и проверки того, видно ту или иную строку или нет, нужно посмотреть статусы транзакции и её подтранзакций. В Postgres у каждого процесса кеш из 64 подтранзакций.

Если у хотя бы одной транзакции кэш переполнится, все процессы будут обращаться к структуре pg_subtrans. Был случай, когда все процессы ждали, когда в общий буферный кэш запишется нужная информация из pg_subtrans. То есть вместо выполнения расчётов все процессы ждали, это негативно сказалось на производительности приложения. Чтобы не попасть в такую ситуацию, разработчику приложения придётся думать над тем, сколько точек сохранения будет создано при обработке PL/pgSQL кода.

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

Операторы MERGE, INSERT FIRST и INSERT ALL

В СУБД Oracle часто применяется оператор MERGE для добавления новых записей и изменения существующих. Ниже приведён пример:

CREATE TABLE members (
    member_id NUMBER PRIMARY KEY,
    first_name VARCHAR2(50) NOT NULL,
    last_name VARCHAR2(50) NOT NULL,
    rank VARCHAR2(20)
);

CREATE TABLE member_staging AS 
SELECT * FROM members;

INSERT INTO members(member_id, first_name, last_name, rank) 
VALUES(1, 'Имя1', 'Фамилия1', 'Золото');

INSERT INTO members(member_id, first_name, last_name, rank) 
VALUES(2, 'Имя2', 'Фамилия2', 'Платина');

INSERT INTO member_staging(member_id, first_name, last_name, rank) 
VALUES(1, 'Имя1', 'Фамилия1', 'Золото1');

INSERT INTO member_staging(member_id, first_name, last_name, rank) 
VALUES(2, 'Имя2', 'Фамилия2', 'Платина1');

INSERT INTO member_staging(member_id, first_name, last_name, rank) 
VALUES(3, 'Имя3', 'Фамилия3', 'Серебро1');

MERGE INTO member_staging x
USING (SELECT member_id, first_name, last_name, rank FROM members) y
   ON (x.member_id  = y.member_id)
 WHEN MATCHED THEN
     UPDATE 
        SET x.first_name = y.first_name
          , x.last_name = y.last_name
          , x.rank = y.rank
      WHERE x.first_name <> y.first_name OR 
            x.last_name <> y.last_name OR 
            x.rank <> y.rank 
 WHEN NOT MATCHED THEN
      INSERT (x.member_id, x.first_name, x.last_name, x.rank)
      VALUES (y.member_id, y.first_name, y.last_name, y.rank);

В СУБД PostgreSQL оператор MERGE добавлен в 15-ую версию, в ранних выпусках вместо него в ряде случаев возможно использовать INSERT ON CONFLICT DO UPDATE:

CREATE TABLE members (
    member_id bigint,
    first_name VARCHAR(50) NOT NULL,
    last_name VARCHAR(50) NOT NULL,
    rank VARCHAR(20)
);

CREATE TABLE member_staging (
    member_id bigint,
    first_name VARCHAR(50) NOT NULL,
    last_name VARCHAR(50) NOT NULL,
    rank VARCHAR(20)
);

INSERT INTO members(member_id, first_name, last_name, rank) 
VALUES(1, 'Имя1', 'Фамилия1', 'Золото');

INSERT INTO members(member_id, first_name, last_name, rank) 
VALUES(2, 'Имя2', 'Фамилия2', 'Платина');

INSERT INTO member_staging(member_id, first_name, last_name, rank) 
VALUES(1, 'Имя1', 'Фамилия1', 'Золото1');

INSERT INTO member_staging(member_id, first_name, last_name, rank) 
VALUES(2, 'Имя2', 'Фамилия2', 'Платина1');

INSERT INTO member_staging(member_id, first_name, last_name, rank) 
VALUES(3, 'Имя3', 'Фамилия3', 'Серебро1');

ALTER TABLE members ADD CONSTRAINT members_pk PRIMARY KEY(member_id);

ALTER TABLE member_staging 
  ADD CONSTRAINT member_staging_pk PRIMARY KEY(member_id);

INSERT INTO member_staging AS x
SELECT m.member_id
     , m.first_name
     , m.last_name
     , m.rank
  FROM members m
    ON CONFLICT (member_id)
    DO UPDATE
          SET first_name = EXCLUDED.first_name
            , last_name = EXCLUDED.last_name
            , rank = EXCLUDED.rank
        WHERE x.member_id = EXCLUDED.member_id 
          AND (x.first_name <> EXCLUDED.first_name OR 
               x.last_name <> EXCLUDED.last_name OR 
               x.rank <> EXCLUDED.rank);

В СУБД PostgreSQL при одновременном выполнении нескольких команд вида INSERT ON CONFLICT DO UPDATE не происходит нарушений целостности данных, в отличии от СУБД Oracle. Там при одновременном выполнении нескольких команд MERGE возможно появление дубликатов, что приводит к нарушениям ограничений первичного ключа.

В СУБД Oracle конструкция INSERT ALL позволяет добавлять данные в несколько таблиц одновременно при выполнении одного запроса. При этом можно добавлять условия, что позволяет добавлять строки в определённую таблицу:

CREATE TABLE orders_t (
    order_id NUMBER(12) PRIMARY KEY,
    customer_id NUMBER(6) NOT NULL,
    status VARCHAR2(20) NOT NULL,
    salesman_id NUMBER(6),
    order_date DATE NOT NULL
);
 
CREATE TABLE order_items_t (
    order_id NUMBER(12),
    item_id NUMBER(12),
    product_id NUMBER(12) NOT NULL,
    quantity NUMBER(8) NOT NULL,
    unit_price NUMBER(8, 2) NOT NULL
);

CREATE TABLE small_orders (
    order_id NUMBER(12) NOT NULL,
    customer_id NUMBER(6) NOT NULL,
    amount NUMBER(8,2) 
);

CREATE TABLE medium_orders AS
SELECT *
  FROM small_orders;

CREATE TABLE big_orders AS
SELECT *
  FROM small_orders;

INSERT ALL
  WHEN amount < 10000 THEN INTO small_orders
  WHEN amount BETWEEN 10000 AND 30000 THEN INTO medium_orders
  WHEN amount > 30000 THEN INTO big_orders
SELECT o.order_id
     , o.customer_id
     , oi.quantity * oi.unit_price AS amount
  FROM orders_t o
  JOIN order_items_t oi 
    ON oi.order_id = o.order_id;

Ниже приведён пример команды INSERT FIRST, при выполнении которой каждая строка подзапроса проверяется на выполнение условий WHEN. При выполнении условия строка запишется в соответствующую таблицу, остальные условия проверяться не будут.

INSERT FIRST
  WHEN amount > 30000 THEN INTO big_orders
  WHEN amount >= 10000 THEN INTO medium_orders
  WHEN amount > 0 THEN INTO small_orders
SELECT o.order_id
     , o.customer_id
     , oi.quantity * oi.unit_price AS amount
  FROM orders_t o
  JOIN order_items_t oi 
    ON oi.order_id = o.order_id;

В СУБД PostgreSQL применяются общие табличные выражения (CTE) и, при необходимости, ключевое слово RETURNING.

CREATE TABLE orders_t (
    order_id bigint PRIMARY KEY,
    customer_id int NOT NULL,
    status VARCHAR(20) NOT NULL,
    salesman_id int,
    order_date timestamp NOT NULL
);
 
CREATE TABLE order_items_t (
    order_id bigint,
    item_id bigint,
    product_id bigint NOT NULL,
    quantity int NOT NULL,
    unit_price numeric(8,2) NOT NULL
);

CREATE TABLE small_orders (
    order_id bigint NOT NULL,
    customer_id int NOT NULL,
    amount numeric(8,2) 
);

CREATE TABLE medium_orders AS
SELECT *
FROM small_orders;

CREATE TABLE big_orders AS
SELECT *
FROM small_orders LIKE small_orders(all);

WITH orders AS (
SELECT o.order_id
     , o.customer_id
     , oi.quantity * oi.unit_price AS amount
  FROM orders_t o
  JOIN order_items_t oi 
    ON oi.order_id = o.order_id
),
so AS (
INSERT INTO small_orders
SELECT s.*
  FROM orders s
 WHERE s.amount < 10000 
),
mo AS (
INSERT INTO medium_orders
SELECT s.*
  FROM orders s
 WHERE s.amount BETWEEN 10000 AND 30000
)
INSERT INTO big_orders
SELECT s.*
  FROM orders s
 WHERE s.amount > 30000;

Стоит отметить, что начиная с 12-ой версии СУБД PostgreSQL поведение CTE можно контролировать в плане материализации полученного множества. Также запросы в WITH в общем случае могут выполняться параллельно и потому порядок их выполнения непредсказуем. В частности, для контроля порядка выполнения и используется конструкция RETURNING.

Пользуясь случаем, остановимся подробнее на INSERT ON CONFLICT DO UPDATE. Часто возникают ситуации, при которых разработчики применяют конструкции INSERT INTO TABLE VALUES следующим образом.

insert into "user_sequence" ("user_id","seq","timestamp","rkey","mapping")
values ($1,$2,$3,$4,$5),($6,$7,$8,$9,$10),($11,$12,$13,$14,$15),
($16,$17,$18,$19,$20),($21,$22,$23,$24,$25),($26,$27,$28,$29,$30),
($31,$32,$33,$34,$35),($36,$37,$38,$39,$40),($41,$42,$43,$44,$45),
($46,$47,$48,$49,$50),($51,$52,$53,$54,$55),($56,$57,$58,$59,$60),
($61,$62,$63,$64,$65),($66,$67,$68,$69,$70),($71,$72,$73,$74,$75),
($76,$77,$78,$79,$80),($81,$82,$83,$84,$85),($86,$87,$88,$89,$90),
($91,$92,$93,$94,$95),($96,$97,$98,$99,$100),
($101,$102,$103,$104,$105),($106,$107,$108,$109,$110),
($111,$112,$113,$114,$115),($116,$117,$118,$119,$120),
($121,$122,$123,$124,$125),($126,$127,$128,$129,$130),
($131,$132,$133,$134,$135),($136,$137,$138,$139,$140),
($141,$142,$143,$144,$145),($146,$147,$148,$149,$150),
($151,$152,$153,$154,$155),($156,$157,$158,$159,$160);

При этом разработчики используют связанные переменные (bind variables) и надеются на хорошую производительность. Но её не будет, поскольку текст запроса меняется в зависимости от количества строк. PostgreSQL будет постоянно разбирать и планировать эти запросы, производительность будет не очень высокой.

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

Во-первых, можно использовать команду COPY. Если известно, что данные только добавляются, но никогда не меняются, можно заменить этот длинный INSERT на команду COPY. Она работает быстрее, вопросов с планированием здесь не возникнет.

Во-вторых, если требуется INSERT ON CONFLICT DO UPDATE, можно поступить так:

  1. Взять промежуточную таблицу.

  2. Опустошить её с помощью TRUNCATE.

  3. Сохранить туда данные командой COPY.

  4. Сделать INSERT INTO (таблица) SELECT FROM (промежуточная таблица) DO UPDATE.

Конструкция DECODE

Конструкция DECODE очень распространена в мире Oracle, для её замены в Postgres можно использовать CASE. В работе DECODE есть нюансы.

Предположим, что в DECODE есть 12 условий и четвёртое из них первой прошла проверку. В этом случае будет вычислено выражение, соответствующее ему, а все остальные условия от 5-го до 12-го не будут вычисляться.

В модуле orafce есть DECODE, но в виде функции. Это означает, что до вызова функции нужно вычислить все её аргументы. В процессе функционального тестирования одной из конструкций DECODE выяснилось, что её последним выражением является деление единицы на ноль. В Oracle ошибки не было, потому что до этого выражения DECODE бы не дошёл. PostgreSQL же считает все аргументы функции до её выполнения ,поэтому возникла ошибка деления на ноль. Пришлось переписывать конструкцию, используя CASE. Код стал чуть более громоздким, но ошибки при этом перестали появляться.

Вложенные определения функций

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

Функция определения схожести строк

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

  • Jaro-Winkler.

  • Levenshtein.

  • Soundex distance.

Схожий функционал предлагает модуль pg_similarity

Алгоритм Levenshtein также поддерживается модулем fuzzystrmatch, входящий в стандартную поставку СУБД PostgreSQL.

Автономные транзакции

Для создания аналога автономных транзакций Oracle в СУБД PostgreSQL существуют три способа:

  1.  dblink. Создаётся отдельное подключение и отдельная независимая транзакция. Но стоит помнить, что при подключении к СУБД создаётся обслуживающий процесс, что является дорогой операцией.

  2. Использование фоновых процессов при помощи pg_background. Потребуется оформить бизнес-логику в виде функции и поручить фоновому процессу её выполнить с помощью функции pg_background_launch(). Проверить результат выполнения можно с помощью функции pg_background_result().

  3. Механизм автономных транзакций уже реализован в Postgres Pro Enterprise с версии 9.6.

Регрессионное тестирование хранимого кода

Ни одно приложение не обходится без тестов. Как минимум, нужно убедиться в том, что разработанное приложение корректно выполняет требования бизнеса. Это так называемое функциональное тестирование. Для него в PostgreSQL предусмотрено два решения - pgtap и pg_prove.

Что касается pgtap, то это набор PL/pgSQL функций для написания тестов. В частности, если есть функция и некоторые значения параметров, то можно прописать, что данная функция при таких-то условиях должна возвращать определённое значение или выбрасывать исключения.

Если говорить о pg_prove, то это написанная на Perl утилита для запуска разработанных с помощью pgtap регрессионных тестов.

Выполнение задач по расписанию

Здесь тоже есть ряд решений на выбор:

  • pg_cron.

  • pgAgent.

  • Модуль СУБД Postgres Pro Enterprise pgpro_scheduler.

  • Любой планировщик заданий прикладного ЯП.

В третьей части цикла было указано, что pgAgent можно устанавливать и использовать отдельно от pgAdmin. Он используется только для того, чтобы задавать задания для pgAgent в графическом интерфейсе. Для корректной работы pgAgent совсем необязательно использовать pgAdmin, можно обойтись обычным SQL-интерфейсом.

Если Вы клиент Postgres Professional и у Вас установлена версия Postgres Pro Enterprise, можно пользоваться pgpro_scheduler.

Можно  также использовать любой реализованный на прикладном языке программирования планировщик заданий и адаптировать его под свои требования.

Конструкции-подсказки планировщику

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

pg_hint_plan позволяет управлять:

  • Методами доступа к строкам таблицы.

  • Методами соединения множеств.

  • Порядком соединения множеств (этого же можно добиться с помощью параметрами join_collapse_limit и from_collapse_limit).

  • Методами исправления количества возвращаемых строк.

  • Параметрами GUC во время работы планировщика.

Утилиты для автоматизации конвертации хранимого кода

Поговорим о решениях, позволяющих обеспечить автоматическую конвертацию хранимого кода:

  • ora2pg и производные от него решения (LUI4ORA2PG).

  • ANTLR4 и грамматика для PL/SQL кода.

Уже упоминавшийся в предыдущих статьях модуль ora2pg может преобразовать часть хранимого кода, но при работе со сложными вещами возможны нюансы, о которых будет рассказано далее. LUI4ORA2PG - разработка компании «ФОРС». Это дорабатываемый инструмент, в котором можно создавать свои проекты миграции, в качестве основы разработчики используют ora2pg.

Пожалуй, наиболее правильным подходом является использование грамматики для PL/SQL кода и техники ANTLR4. В этом случае исходный код фактически преобразуется в дерево разбора. Разработчику придётся разработать логику обхода этого дерева самостоятельно, в результате, хранимый код исходной СУБД будет преобразован в соответствующий код целевой СУБД.

Платные конвертеры сделаны на основе упомянутой выше технологии. В данном докладе описан процесс автоматического перевода кода с PL/SQL на PL/pgSQL с помощью ANTLR4.

Особенности конвертации кода утилитой ora2pg

Инструмент ora2pg не покрывает всевозможные конструкции хранимого кода. Приведём несколько примеров:

  1. Не поддерживается конвертация старого синтаксиса соединения таблиц в СУБД Oracle.

  2. Частичная конвертация конструкции DECODE в CASE.

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

  4. Не поддерживается конвертация старого и нового синтаксиса рекурсивных запросов.

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

  6. Не поддерживается конвертация конструкции KEEP (DENSE_RANK LAST ORDER BY) OVER(PARTITION BY ), нужно использовать расширение first_last_agg.

  7. Не всегда корректно преобразуется код динамических запросов. Иногда в хранимом коде требуется динамически формировать команды внутри функций, поскольку при каждом выполнении могут использоваться разные таблицы или типы данных. В СУБД Oracle для этого используется команда EXECUTE IMMEDIATE, а в СУБД PostgreSQL - EXECUTE.

    Были случаи, когда динамический код просто оставался в исходном виде, нужно было его переписывать. В частности, в СУБД Postgres нет таких типов, как BLOB, VARCHAR, CLOB.

  8. Не конвертируется код с использованием пакетов dbms_xmldom, dbms_lob и.т.д.

Модуль orafce

Модуль orafce предоставляет функции и операторы для замены части функций и пакетов:

Использование вспомогательных функций при работе планировщика

Стоит сказать пару слов и о функциях, которые возвращают множества. В Oracle речь идёт о конвейерной (pipeline) функции, которая на выходе возвращает множество. Рассмотрим пример.

CREATE OR REPLACE TYPE varchar_t AS TABLE OF VARCHAR2(4000);

CREATE OR REPLACE FUNCTION split_str(p_str VARCHAR, p_delim VARCHAR) 
RETURN varchar_t PIPELINED AS
BEGIN
FOR v_row IN 
(SELECT regexp_substr(p_str, '[^' || p_delim || ']+', 1, level) AS elem
      , '[^' || p_delim || ']+' AS delim 
   FROM DUAL
CONNECT BY regexp_substr(p_str, '[^' || p_delim || ']+', 1, level) is not null  
  ORDER BY LEVEL
)
LOOP
  PIPE ROW(v_row.elem);
END LOOP;
END;

SELECT * FROM TABLE(split_str('XXX,Y,ММ,AAAAA,B,CCC,D,E,F,GGG', ','));

Как правило, данный код заменяется в СУБД PostgreSQL на конструкцию вида:

SELECT * FROM regexp_split_to_table('XXX,Y,ММ,AAAAA,B,CCC,D,E,F,GGG', ',');

Ниже приведён её план выполнения.

                                  QUERY PLAN
-----------------------------------------------------------------------------
 Function Scan on regexp_split_to_table  (cost=0.00..10.00 rows=1000 width=32) 
 (actual time=0.025..0.025 rows=10 loops=1)

Видно, что расчётное количество строк в 100 раз превышает фактическое. До 12-ой версии СУБД PostgreSQL с точки зрения планировщика функции были чёрным ящиком, в качестве расчётного количества строк бралось значение prorows из pg_proc. Для функции regexp_split_to_table данное значение = 1000. При дальнейшем использовании этого расчётного количества, например, при соединении с другим множеством существует возможность выбора некорректного метода соединения, что приведёт к деградации работы запроса в целом.

В 12-ой версии команда CREATE FUNCTION была расширена следующим образом:

CREATE FUNCTION name (...) RETURNS ...
SUPPORT supportfunction
AS

Т.е, появилась возможность предоставления вспомогательной функции, обладающей информацией об основной функции и способствующей выбору лучшего плана выполнения. При работе с функцией, возвращающей множество, PostgreSQL вызывает вспомогательную функцию для оценки количества получаемых строк. Это было разработано для функции unnest(), что позволяет заменить regex_split_to_table() на следующую конструкцию:

SELECT * FROM unnest(regexp_split_to_array('XXX,Y,ММ,AAAAA,B,CCC,D,E,F,GGG', ','));

Ниже приведён план выполнения.

EXPLAIN(ANALYZE) 
SELECT * FROM unnest(regexp_split_to_array('XXX,Y,ММ,AAAAA,B,CCC,D,E,F,GGG', ','));
                                               QUERY PLAN
----------------------------------------------------------------------------- 
Function Scan on unnest  (cost=0.00..0.10 rows=10 width=32) 
(actual time=0.012..0.014 rows=10 loops=1)
 Planning Time: 2.591 ms
 Execution Time: 0.353 ms

В этом случае расчётное количество строк не отличается от фактического.

Расширенная статистика в СУБД PostgreSQL

В СУБД Postgres предусмотрено три вида расширенной статистики:

  1. dependencies (функциональные зависимости).

  2. n_distinct (многовариантное число различных значений).

  3. mcv (многовариантные списки MCV).

Первый тип, dependencies, появился ещё в PostgreSQL 10 и служит для отслеживания функциональных зависимостей между столбцами. Например, если город - Казань, то его код 843, если город Москва, код  - 495. Эта расширенная статистика подходит для операций равенства, сравнивающих значения столбцов с константами, и условий IN с константами. Она не используется при:

  1. Проверке равенства двух столбцов или сравнении столбца с выражением.

  2. Проверке условий диапазонов.

  3. Проверке условий LIKE.

Для чего нужен ndistinct? Изначально в статистике СУБД PostgreSQL хранится информация о количестве уникальных значений столбца, но не для нескольких столбцов. Данный вид расширенной статистики используется в оценке количества уникальных групп, полученных при выполнении GROUP BY.

Статистика mcv - список наиболее часто встречающихся значений. Она доступна в PostgreSQL с версии 12 и позволяет определить наиболее часто встречающиеся значения для комбинации столбцов. Раньше они хранились только по одиночным столбцам. Если до этого в запросе было много условий фильтрации, объединённых логическими И (AND), то планировщик, как правило, ошибался в оценке расчётного количества строк. mcv позволяет планировщику оценить количество строк более точно.

Расширенные статистики можно комбинировать между собой. Пусть есть запрос с несколькими условиями фильтрации, объединённых операцией И (AND), с конструкцией GROUP BY по нескольким столбцам. Тогда можно создать расширенную статистику типа mcv и ndistinct, планировщик воспользуется ими для определения расчётного количества строк.

Также в PostgreSQL 14 появилась возможность создавать статистику на набор вычисляемых столбцов. Подробнее можно прочитать об этом здесь.

Поиск некорректного PL/pgSQL кода и зависимостей

Существуют средства для проверки PL/pgSQL кода. Модуль plpgsql_check позволяет отследить:

  1. Некорректное использование типов параметров функций. Из-за этого, например, может использоваться полное сканирование таблицы вместо сканирования по индексу.

  2. Неиспользуемые переменные и функции.

  3. Отсутствие команды возврата значения.

  4. Недостижимый код.

Ниже приведён пример вывода результата проверки функции.

Также можно определять зависимости между объектами.

Модуль plprofiler

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

Во-первых, он предоставляет визуальное отображение профилей нагрузки с указанием проблемных мест в хранимом коде.

Во-вторых, он генерирует HTML-отчёты, которые можно проанализировать. Они создаются для профилей производительности PL/pgSQL функций и процедур.

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

Проблема здесь заключается в том, что у таблицы pgbench_accounts нет индекса, у которого ключевое поле aid является лидирующим. Там есть индекс с полем aid, но оно не лидирующее. В результате запрос выполняется неоптимально.

Ещё один визуальный пример. Видно, что у tpcb_upd_accounts() есть некие проблемы, да и у tpcb_fetch_abalance() тоже, потому что её выполнение занимает половину общего времени. Причина также заключается в отсутствии индекса в таблице pgbench_accounts, в котором столбец aid является лидирующим.

Стоит отметить, что модуль plprofiler можно использовать в рамках трёх сценариев:

  1. Проверка и замер скорости работы определённой функции.

  2. Замер скорости работы пользовательских процессов, оценка общего профиля нагрузки.

  3. Использование модуля в промышленном окружении. Стоит отметить, что в промышленном окружении нельзя включить замеры работы для всех сессий, иначе просядет производительность. Тем не менее, профилирование можно включить на время. Есть риск, что часть нужной информации не будет записана. Некоторые функции могут начать свою работу до включения замеров.

Поэтому для точного анализа сессий, которые выбиваются из общей картины, можно использовать модуль pg_query state. Он покажет запрос, выполняемый в настоящий момент времени, и его план выполнения.

В рамках цикла о миграции с СУБД Oracle и СУБД PostgreSQL была рассмотрена лишь часть случаев. На деле каждый случай миграции уникален. Многие клиенты всё чаще задумываются о переходе на PostgreSQL, но могут не учесть всех особенностей перевода. Специалисты компании Postgres Professional готовы восполнить эти пробелы.

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


  1. WondeRu
    27.08.2022 20:59
    +1

    Давно уже занимаюсь переводом на постгрю. Самое веселое было, когда нам дали Экзадату (оракл на стероидах) и попросили перевести на постгрю. Экзадата хороша тем, что можно быть нубом в sql, не знать, что существуют планы запросов и получать неимоверные быстрые селекты с группировками и прочим. Так вот, нам пришлось перелопачивать абсолютно каждый из нескольких сотен запросов, чтобы это хоть как-то крутилось на постгре. Стоимости лицензий и специального железа ушли, но возникла потребность в классных базистах.


    1. ppetrov91 Автор
      27.08.2022 21:31
      +2

      Добрый вечер!

      Своё карьеру я начинал PL/SQL разработчиком. В моём случае, нужно было знать как можно больше про планы выполнения и про приёмы написания хорошего SQL-кода.

      Мне также доводилось конвертировать запросы из exadata в СУБД PostgreSQL на одном из пилотных проектов. Там было над чем подумать, в том числе и над оптимальной схемой секционирования.

      Правда, все запросы очень на OLAP были похожи хотя бы потому, что пришлось работать с сотнями миллионов строк.

      Такие запросы можно сконвертировать в СУБД PostgreSQL, но запускать их стоит на асинхронной реплике с выключенным hot_standby_feedback и с max_standby_streaming_delay в -1.

      Возможно, что в exadata есть свои продвинутые алгоритмы, но, к сожалению, это чёрный ящик, нельзя посмотреть, что там используется.

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

      Если не секрет, какие особенности exadata использовались на проекте? И как сейчас живётся без них в PG?

      Заранее спасибо.


      1. WondeRu
        27.08.2022 22:26
        +1

        Добрый день. За давностью лет, к сожалению, подробности стерлись из головы. Мы занимались переездом, а дальше уже команда заказчика брала на себя сопровождение.


  1. IgorM23
    27.08.2022 23:11

    Большое спасибо за статью.

    Но Вы ничего не сказали, что делать с Oracle Advanced Queuing и Advanced Compression.

    Что же делать с объектными типами PL/SQL, если они содержат еще и методы, а не только атрибуты?

    Что делать с глобальными контекстами?

    И куда засунуть в PG Oracle Flashback Archive?


    1. ppetrov91 Автор
      27.08.2022 23:29

      Добрый вечер!

      На часть вопросов ответ дан во 2-ой и 3-ей части цикла.

      Oracle Advanced Queuing - секция "Использование механизма очередей", статья https://habr.com/ru/company/postgrespro/blog/683748/

      В настоящий момент компрессия есть в СУБД Postgres Pro Enterprise, но там данные сжимаются целиком, если создать табличное пространство с опцией WITH (compression=true)

      https://postgrespro.ru/docs/enterprise/14/cfs-usage

      Объектные типы PL/SQL с методами - секция "Использование PL/SQL коллекций и объектов", статья https://habr.com/ru/company/postgrespro/blog/683748/

      Глобальные контексты - секция "Расширение pg_variables", статья https://habr.com/ru/company/postgrespro/blog/679808/

      Flashback, к сожалению, в PG отсутствует из-за того, что не хранится история об активных транзакциях в тот или иной момент времени в прошлом. Об этом подробно рассказывается, в частности, на курсе DBA-2 в лекции "Снимки данных"

      https://edu.postgrespro.ru/dba2/dba2_03_mvcc_snapshots.pdf

      Также об этом говорится в книге "PostgreSQL изнутри"

      https://postgrespro.ru/education/books/internals


      1. IgorM23
        28.08.2022 08:49

        Спасибо!

        Но pg_varibales НЕ позволяет создать константу существующую для нескольких сессий.

        Глобальные константы существуют в оракле в SGA - видимы сразу для всех сессий.

        Как это реализовать в PG?


        1. ikamil
          28.08.2022 10:11

          Часть кейсов можно заменить immutable функциями : хранят и оченьбыстро отдают значения, меняются execute


        1. ppetrov91 Автор
          28.08.2022 10:18

          Добрый день!

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


          1. ikamil
            28.08.2022 10:59

            а pg_variables имеет опцию а-ля "глобальный / сессионный контекст" ?


            1. ppetrov91 Автор
              28.08.2022 11:00

              Добрый день!

              В настоящее время только сессионный.


              1. ikamil
                28.08.2022 11:27

                значит pg_variables умеет в глобально видимую память? только как-то без этой опции?


                1. ppetrov91 Автор
                  28.08.2022 11:28

                  Пока не умеет.

                  Модуль pg_variables содержит функции для работы с переменными различных типов. Созданные переменные существуют в течение текущей пользовательской сессии.

                  https://postgrespro.ru/docs/postgrespro/14/pg-variables


          1. ikamil
            28.08.2022 11:04

            ну если нужен глобальный, то вот тогда вариант с иммутабл,
            вполне схоже по производительности с вызовом переменной из пакета, хоть и не так прозрачно выглядит, и нет удобной автоматизации в виде [пере-]инициации значений в теле пакета при его перекомпиляции


            1. ppetrov91 Автор
              28.08.2022 11:08

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


              1. ikamil
                28.08.2022 11:21

                придётся execute -> commit делать при обновлении значения, возможно, одним из аналогов автономной транзакции, тогда другой процесс увидит новое значение


                1. ppetrov91 Автор
                  28.08.2022 11:24

                  Полагаю, что всё-таки наиболее правильным было бы использование сегмента общей памяти и примитивов его синхронизации. Об этом и был первоначальный вопрос.


  1. Tellamonid
    28.08.2022 00:12

    Не подскажете, какие есть подходы к тому, что в PostgreSQL нет глобальных индексов?
    Я пока вижу только вариант иметь «глобальную» таблицу вместо индекса, и поддерживать её из кода. Есть ли ещё какие-то опции?


    1. ppetrov91 Автор
      28.08.2022 10:10

      Добрый день!

      Да, придётся поддерживать глобальную таблицу, например, с помощью триггеров


    1. ikamil
      28.08.2022 10:58
      +1

      текстовый локальный примари кей из двух кусков 'idpartition'||sequence.nextval


      1. ppetrov91 Автор
        28.08.2022 11:00

        Добрый день!

        А если требуется искать не по первичному ключу?


        1. ikamil
          28.08.2022 11:01

          это же может быть и просто индекс


  1. PaulIsh
    28.08.2022 14:09

    Отличная серия статей. Есть ли что-то похожее на переезд с MS SQL на Pg?


    1. ppetrov91 Автор
      28.08.2022 16:30

      Добрый день! Спасибо.

      Пока такой серии нет, но, наверное, сделать её можно


  1. fquantum
    28.08.2022 21:34

    Добрый день!

    Oracle. Там при одновременном выполнении нескольких команд MERGE возможно появление дубликатов, что приводит к нарушениям ограничений первичного ключа

    Откуда такая информация? Можно ссылки?


    1. ppetrov91 Автор
      28.08.2022 22:05
      +1

      Добрый вечер!

      В комментарии ниже приведён пример такой ситуации в СУБД Oracle и PostgreSQL


  1. ppetrov91 Автор
    28.08.2022 22:02

    Добрый вечер!

    Пусть имеется следующая таблица в СУБД Oracle:

    CREATE TABLE merge_test(
     id NUMBER NOT NULL,
     value VARCHAR2(10),
     CONSTRAINT PK_MERGE_TEST PRIMARY KEY (id)
    );

    Пусть к ней обращаются две сессии с autocommit = off следующим образом:

    Первая сессия:

    MERGE INTO merge_test d
    USING (SELECT 1 id, 'A' value 
    	     FROM dual
    	    ) s
       ON (d.id = s.id)
     WHEN matched THEN
         UPDATE SET d.value = s.value
     WHEN not matched THEN
         INSERT (d.id, d.value) VALUES (s.id, s.value);

    Вторая сессия выполняет запрос ниже, ждёт освобождения блокировки:

    MERGE INTO merge_test d
    USING (SELECT 1 id, 'B' value 
    	     FROM dual
    	    ) s
       ON (d.id = s.id)
     WHEN matched THEN
         UPDATE SET d.value = s.value
     WHEN not matched THEN
         INSERT (d.id, d.value) VALUES (s.id, s.value);

    Первая сессия делает COMMIT, а во второй сессии запрос завершается с ошибкой:

    Error report -
    ORA-00001: нарушено ограничение уникальности (CISADM.PK_MERGE_TEST)

    А теперь посмотрим на аналогичную ситуацию в СУБД PostgreSQL. Ниже приведена команда создания таблицы:

    CREATE TABLE merge_test (
      id bigint NOT NULL,
      value VARCHAR(10),
      CONSTRAINT PK_MERGE_TEST PRIMARY KEY (id)
    );

    Первая сессия:

    BEGIN;
    
    INSERT INTO merge_test AS t
    SELECT 1, 'A'
        ON CONFLICT (id)
        DO UPDATE
              SET value = EXCLUDED.value
            WHERE t.id = EXCLUDED.id;
    
    INSERT 0 1

    Вторая сессия ждёт снятия блокировки:

    BEGIN;
    
    INSERT INTO merge_test AS t
    SELECT 1, 'B'
        ON CONFLICT (id)
        DO UPDATE
              SET value = EXCLUDED.value
            WHERE t.id = EXCLUDED.id;

    Первая сессия выполняет COMMIT:

    COMMIT;

    Вторая сессия успешно завершается успешно:

    INSERT 0 1
    
    SELECT * FROM merge_test;
     id | value
    ----+-------
      1 | B
    
    COMMIT;

    Ниже приведено содержимое таблицы:

    SELECT * FROM merge_test;
    
     id | value
    ----+-------
      1 | B


    1. RekGRpth
      29.08.2022 05:32

      В PostgreSQL тоже сделали MERGE


      1. ppetrov91 Автор
        29.08.2022 10:47

        Добрый день!

        Вопрос был не про то, что в PG сделали MERGE, о чём также написано в статьях.

        А про то, что в Oracle могут быть ошибки ограничения целостности первичного ключа при работе нескольких MERGE одновременно.

        Попросили привести ссылку, я привёл пример.

        Кроме того, не все могут сразу обновиться на новую версию СУБД.