Эта статья завершает цикл о миграции с СУБД 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, можно поступить так:
Взять промежуточную таблицу.
Опустошить её с помощью TRUNCATE.
Сохранить туда данные командой COPY.
Сделать 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 существуют три способа:
dblink. Создаётся отдельное подключение и отдельная независимая транзакция. Но стоит помнить, что при подключении к СУБД создаётся обслуживающий процесс, что является дорогой операцией.
Использование фоновых процессов при помощи pg_background. Потребуется оформить бизнес-логику в виде функции и поручить фоновому процессу её выполнить с помощью функции pg_background_launch(). Проверить результат выполнения можно с помощью функции pg_background_result().
Механизм автономных транзакций уже реализован в Postgres Pro Enterprise с версии 9.6.
Регрессионное тестирование хранимого кода
Ни одно приложение не обходится без тестов. Как минимум, нужно убедиться в том, что разработанное приложение корректно выполняет требования бизнеса. Это так называемое функциональное тестирование. Для него в PostgreSQL предусмотрено два решения - pgtap и pg_prove.
Что касается pgtap, то это набор PL/pgSQL функций для написания тестов. В частности, если есть функция и некоторые значения параметров, то можно прописать, что данная функция при таких-то условиях должна возвращать определённое значение или выбрасывать исключения.
Если говорить о pg_prove, то это написанная на Perl утилита для запуска разработанных с помощью pgtap регрессионных тестов.
Выполнение задач по расписанию
Здесь тоже есть ряд решений на выбор:
Модуль СУБД 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 не покрывает всевозможные конструкции хранимого кода. Приведём несколько примеров:
Не поддерживается конвертация старого синтаксиса соединения таблиц в СУБД Oracle.
Частичная конвертация конструкции DECODE в CASE.
При конвертации строки в число исходная СУБД (Oracle) убирает лидирующие нули. Речь идёт о неявном преобразовании типов. В PostgreSQL их надо убирать самостоятельно, в противном случае, запрос будет работать неверно. В частности, при соединении множеств нужно убедиться, что поля соединения одинакового типа и при этом корректно сравниваются. В противном случае, может быть синтаксическая ошибка или неверный результат выполнения.
Не поддерживается конвертация старого и нового синтаксиса рекурсивных запросов.
Не всегда корректно преобразуются запросы с использованием двойных кавычек. Возможна потеря регистра. Иногда ora2pg меняет регистр идентификаторов, заключённых в двойные кавычки. В некоторых случаях регистр имеет значение.
Не поддерживается конвертация конструкции KEEP (DENSE_RANK LAST ORDER BY) OVER(PARTITION BY ), нужно использовать расширение first_last_agg.
-
Не всегда корректно преобразуется код динамических запросов. Иногда в хранимом коде требуется динамически формировать команды внутри функций, поскольку при каждом выполнении могут использоваться разные таблицы или типы данных. В СУБД Oracle для этого используется команда EXECUTE IMMEDIATE, а в СУБД PostgreSQL - EXECUTE.
Были случаи, когда динамический код просто оставался в исходном виде, нужно было его переписывать. В частности, в СУБД Postgres нет таких типов, как BLOB, VARCHAR, CLOB.
Не конвертируется код с использованием пакетов dbms_xmldom, dbms_lob и.т.д.
Модуль orafce
Модуль orafce предоставляет функции и операторы для замены части функций и пакетов:
Тип date для хранения значений "дата + время”.
Функции для работы с типом date (add_months, next_day, last_day, months_between, trunc).
Пакеты dbms_output, utl_file, dbms_pipe, dbms_alert.
PLVstr и PLVchr для работы со строками.
Использование вспомогательных функций при работе планировщика
Стоит сказать пару слов и о функциях, которые возвращают множества. В 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 предусмотрено три вида расширенной статистики:
dependencies (функциональные зависимости).
n_distinct (многовариантное число различных значений).
mcv (многовариантные списки MCV).
Первый тип, dependencies, появился ещё в PostgreSQL 10 и служит для отслеживания функциональных зависимостей между столбцами. Например, если город - Казань, то его код 843, если город Москва, код - 495. Эта расширенная статистика подходит для операций равенства, сравнивающих значения столбцов с константами, и условий IN с константами. Она не используется при:
Проверке равенства двух столбцов или сравнении столбца с выражением.
Проверке условий диапазонов.
Проверке условий LIKE.
Для чего нужен ndistinct? Изначально в статистике СУБД PostgreSQL хранится информация о количестве уникальных значений столбца, но не для нескольких столбцов. Данный вид расширенной статистики используется в оценке количества уникальных групп, полученных при выполнении GROUP BY.
Статистика mcv - список наиболее часто встречающихся значений. Она доступна в PostgreSQL с версии 12 и позволяет определить наиболее часто встречающиеся значения для комбинации столбцов. Раньше они хранились только по одиночным столбцам. Если до этого в запросе было много условий фильтрации, объединённых логическими И (AND), то планировщик, как правило, ошибался в оценке расчётного количества строк. mcv позволяет планировщику оценить количество строк более точно.
Расширенные статистики можно комбинировать между собой. Пусть есть запрос с несколькими условиями фильтрации, объединённых операцией И (AND), с конструкцией GROUP BY по нескольким столбцам. Тогда можно создать расширенную статистику типа mcv и ndistinct, планировщик воспользуется ими для определения расчётного количества строк.
Также в PostgreSQL 14 появилась возможность создавать статистику на набор вычисляемых столбцов. Подробнее можно прочитать об этом здесь.
Поиск некорректного PL/pgSQL кода и зависимостей
Существуют средства для проверки PL/pgSQL кода. Модуль plpgsql_check позволяет отследить:
Некорректное использование типов параметров функций. Из-за этого, например, может использоваться полное сканирование таблицы вместо сканирования по индексу.
Неиспользуемые переменные и функции.
Отсутствие команды возврата значения.
Недостижимый код.
Ниже приведён пример вывода результата проверки функции.
Также можно определять зависимости между объектами.
Модуль plprofiler
Иногда хранимый код работает недостаточно быстро, при этом причина сходу непонятна. В таких условиях можно использовать plprofiler.
Во-первых, он предоставляет визуальное отображение профилей нагрузки с указанием проблемных мест в хранимом коде.
Во-вторых, он генерирует HTML-отчёты, которые можно проанализировать. Они создаются для профилей производительности PL/pgSQL функций и процедур.
В-третьих, для той или иной функции с помощью этого модуля можно узнать процент от общего времени выполнения. Вот как это выглядит.
Проблема здесь заключается в том, что у таблицы pgbench_accounts нет индекса, у которого ключевое поле aid является лидирующим. Там есть индекс с полем aid, но оно не лидирующее. В результате запрос выполняется неоптимально.
Ещё один визуальный пример. Видно, что у tpcb_upd_accounts() есть некие проблемы, да и у tpcb_fetch_abalance() тоже, потому что её выполнение занимает половину общего времени. Причина также заключается в отсутствии индекса в таблице pgbench_accounts, в котором столбец aid является лидирующим.
Стоит отметить, что модуль plprofiler можно использовать в рамках трёх сценариев:
Проверка и замер скорости работы определённой функции.
Замер скорости работы пользовательских процессов, оценка общего профиля нагрузки.
Использование модуля в промышленном окружении. Стоит отметить, что в промышленном окружении нельзя включить замеры работы для всех сессий, иначе просядет производительность. Тем не менее, профилирование можно включить на время. Есть риск, что часть нужной информации не будет записана. Некоторые функции могут начать свою работу до включения замеров.
Поэтому для точного анализа сессий, которые выбиваются из общей картины, можно использовать модуль pg_query state. Он покажет запрос, выполняемый в настоящий момент времени, и его план выполнения.
В рамках цикла о миграции с СУБД Oracle и СУБД PostgreSQL была рассмотрена лишь часть случаев. На деле каждый случай миграции уникален. Многие клиенты всё чаще задумываются о переходе на PostgreSQL, но могут не учесть всех особенностей перевода. Специалисты компании Postgres Professional готовы восполнить эти пробелы.
Комментарии (28)
IgorM23
27.08.2022 23:11Большое спасибо за статью.
Но Вы ничего не сказали, что делать с Oracle Advanced Queuing и Advanced Compression.
Что же делать с объектными типами PL/SQL, если они содержат еще и методы, а не только атрибуты?
Что делать с глобальными контекстами?
И куда засунуть в PG Oracle Flashback Archive?
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/internalsIgorM23
28.08.2022 08:49Спасибо!
Но pg_varibales НЕ позволяет создать константу существующую для нескольких сессий.
Глобальные константы существуют в оракле в SGA - видимы сразу для всех сессий.
Как это реализовать в PG?
ikamil
28.08.2022 10:11Часть кейсов можно заменить immutable функциями : хранят и оченьбыстро отдают значения, меняются execute
ppetrov91 Автор
28.08.2022 10:18Добрый день!
В данном случае, возможно, придётся вносить изменения в pg_variables так, чтобы данные хранились в общей памяти, только так они будут видны всем сессиям.ikamil
28.08.2022 10:59а pg_variables имеет опцию а-ля "глобальный / сессионный контекст" ?
ppetrov91 Автор
28.08.2022 11:00Добрый день!
В настоящее время только сессионный.ikamil
28.08.2022 11:27значит pg_variables умеет в глобально видимую память? только как-то без этой опции?
ppetrov91 Автор
28.08.2022 11:28Пока не умеет.
Модульpg_variables
содержит функции для работы с переменными различных типов. Созданные переменные существуют в течение текущей пользовательской сессии.
https://postgrespro.ru/docs/postgrespro/14/pg-variables
ikamil
28.08.2022 11:04ну если нужен глобальный, то вот тогда вариант с иммутабл,
вполне схоже по производительности с вызовом переменной из пакета, хоть и не так прозрачно выглядит, и нет удобной автоматизации в виде [пере-]инициации значений в теле пакета при его перекомпиляцииppetrov91 Автор
28.08.2022 11:08В простом случае, да, immutable-функции помогают, но может быть случай, когда что-то вычисляется, помещается в переменную глобального контекста, а потом другой процесс из этой переменной достаёт значение и использует в своих расчётах
ikamil
28.08.2022 11:21придётся execute -> commit делать при обновлении значения, возможно, одним из аналогов автономной транзакции, тогда другой процесс увидит новое значение
ppetrov91 Автор
28.08.2022 11:24Полагаю, что всё-таки наиболее правильным было бы использование сегмента общей памяти и примитивов его синхронизации. Об этом и был первоначальный вопрос.
Tellamonid
28.08.2022 00:12Не подскажете, какие есть подходы к тому, что в PostgreSQL нет глобальных индексов?
Я пока вижу только вариант иметь «глобальную» таблицу вместо индекса, и поддерживать её из кода. Есть ли ещё какие-то опции?ppetrov91 Автор
28.08.2022 10:10Добрый день!
Да, придётся поддерживать глобальную таблицу, например, с помощью триггеров
fquantum
28.08.2022 21:34Добрый день!
Oracle. Там при одновременном выполнении нескольких команд MERGE возможно появление дубликатов, что приводит к нарушениям ограничений первичного ключа
Откуда такая информация? Можно ссылки?
ppetrov91 Автор
28.08.2022 22:05+1Добрый вечер!
В комментарии ниже приведён пример такой ситуации в СУБД Oracle и PostgreSQL
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
RekGRpth
29.08.2022 05:32В PostgreSQL тоже сделали MERGE
ppetrov91 Автор
29.08.2022 10:47Добрый день!
Вопрос был не про то, что в PG сделали MERGE, о чём также написано в статьях.
А про то, что в Oracle могут быть ошибки ограничения целостности первичного ключа при работе нескольких MERGE одновременно.
Попросили привести ссылку, я привёл пример.
Кроме того, не все могут сразу обновиться на новую версию СУБД.
WondeRu
Давно уже занимаюсь переводом на постгрю. Самое веселое было, когда нам дали Экзадату (оракл на стероидах) и попросили перевести на постгрю. Экзадата хороша тем, что можно быть нубом в sql, не знать, что существуют планы запросов и получать неимоверные быстрые селекты с группировками и прочим. Так вот, нам пришлось перелопачивать абсолютно каждый из нескольких сотен запросов, чтобы это хоть как-то крутилось на постгре. Стоимости лицензий и специального железа ушли, но возникла потребность в классных базистах.
ppetrov91 Автор
Добрый вечер!
Своё карьеру я начинал PL/SQL разработчиком. В моём случае, нужно было знать как можно больше про планы выполнения и про приёмы написания хорошего SQL-кода.
Мне также доводилось конвертировать запросы из exadata в СУБД PostgreSQL на одном из пилотных проектов. Там было над чем подумать, в том числе и над оптимальной схемой секционирования.
Правда, все запросы очень на OLAP были похожи хотя бы потому, что пришлось работать с сотнями миллионов строк.
Такие запросы можно сконвертировать в СУБД PostgreSQL, но запускать их стоит на асинхронной реплике с выключенным hot_standby_feedback и с max_standby_streaming_delay в -1.
Возможно, что в exadata есть свои продвинутые алгоритмы, но, к сожалению, это чёрный ящик, нельзя посмотреть, что там используется.
На самом деле, важно понимать, в каких случаях отчёты стоит выполнять на OLTP-базе, а в каких стоит использовать другие решения.
Если не секрет, какие особенности exadata использовались на проекте? И как сейчас живётся без них в PG?
Заранее спасибо.
WondeRu
Добрый день. За давностью лет, к сожалению, подробности стерлись из головы. Мы занимались переездом, а дальше уже команда заказчика брала на себя сопровождение.