Отчёт оценки миграции говорит «уровень A, ручной правки нет», а функции после конвертации возвращают другое число или падают. Причина в устройстве ora2pg: он переписывает PL/SQL набором регулярных замен и считает стоимость уже по переписанному коду, а семантику Oracle (пустая строка равна NULL, ROWNUM нумерует строки до сортировки) не знает. Соответствия понятий двух баз лежат в статье про Oracle и SQL Server для DBA из PostgreSQL и здесь не повторяются. Ниже — учебная схема на 6 строк, которую прогнали через ora2pg 25.0 и через живой Oracle Free: из четырёх функций ни одна не вернула ответ Oracle, пока их не поправили руками.
Фазы миграции: что показал опыт на каждой
Стенд
docker network create dba-migr-net
docker run -d --name dba-migr-ora --network dba-migr-net --memory 3g -e ORACLE_PASSWORD=ora gvenzl/oracle-free:23-slim
docker run -d --name dba-migr-pg --network dba-migr-net --memory 2g -e POSTGRES_PASSWORD=pg postgres:18
Oracle Free распаковывает файлы базы при первом старте: на --memory 2g в логе контейнера появилось Killed на шаге 7zzs x, на 3g база поднялась (образ занимает 2,84 ГБ). ora2pg собирается внутри контейнера PostgreSQL из архива тега v25.0; для отчёта оценки нужны ещё DBD::Oracle и Oracle Instant Client (basiclite и sdk 23.26 в /opt/oracle, плюс ссылка libaio.so.1 на libaio.so.1t64):
docker exec dba-migr-pg sh -c 'apt-get update -qq && apt-get install -y -qq --no-install-recommends perl make libdbi-perl libdbd-pg-perl unzip gcc libc6-dev libaio1t64 cpanminus'
docker exec dba-migr-pg sh -c 'cd /tmp && tar xzf ora2pg-v25.0.tar.gz && cd ora2pg-25.0 && perl Makefile.PL && make && make install'
docker exec dba-migr-pg sh -c 'ORACLE_HOME=/opt/oracle/instantclient_23_26 LD_LIBRARY_PATH=$ORACLE_HOME cpanm --notest DBD::Oracle; cp /etc/ora2pg/ora2pg.conf.dist /tmp/ora2pg.conf; ora2pg --version; psql -U postgres -Atc "select version()"'
вывод (ora2pg --version и select version())
Ora2Pg v25.0
PostgreSQL 18.6 (Debian 18.6-1.pgdg13+2) on x86_64-pc-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
Учебная схема: шесть сотрудников, дерево 1 → (2, 3), 2 → (4, 5), 3 → 6; у сотрудников 2 и 4 note = '', у 1 и 5 NULL, у 3 и 6 ok; зарплаты 5000.50, 4000.25, 3000.10, 2000.75, 900.00, 1500.00. Четыре функции: ROWNUM над подзапросом с сортировкой, обход CONNECT BY и две функции пакета с переменной g_min_salary и NVL.
CREATE TABLE employees (
emp_id NUMBER(10) NOT NULL, name VARCHAR2(100) NOT NULL, salary NUMBER(10,2),
hired DATE, manager_id NUMBER(10), note VARCHAR2(200),
CONSTRAINT pk_employees PRIMARY KEY (emp_id));
CREATE OR REPLACE FUNCTION top_salary_sum(p_n IN NUMBER) RETURN NUMBER IS
v_sum NUMBER;
BEGIN
SELECT SUM(salary) INTO v_sum
FROM (SELECT salary FROM employees ORDER BY salary DESC)
WHERE ROWNUM <= p_n;
RETURN v_sum;
END;
/
CREATE OR REPLACE FUNCTION team_size(p_root IN NUMBER) RETURN NUMBER IS
v_cnt NUMBER;
BEGIN
SELECT COUNT(*) INTO v_cnt FROM employees
START WITH emp_id = p_root CONNECT BY PRIOR emp_id = manager_id;
RETURN v_cnt;
END;
/
CREATE OR REPLACE PACKAGE pay AS
g_min_salary NUMBER := 1000;
FUNCTION without_note RETURN NUMBER; -- SELECT COUNT(*) ... WHERE NVL(note, 'x') = 'x'
FUNCTION below_min RETURN NUMBER; -- SELECT COUNT(*) ... WHERE salary < g_min_salary
END pay;
/
Опыт 1: ora2pg конвертирует файл без Oracle
Ключ -i читает PL/SQL из файла и подключения к Oracle не открывает, тип экспорта задаёт -t. Каждый объект конвертируется отдельным вызовом (при общем файле тело пакета оказалось внутри team_size):
ora2pg -c /tmp/ora2pg.conf -t TABLE -i schema.sql -o TABLE.sql -q --no_header
ora2pg -c /tmp/ora2pg.conf -t FUNCTION -i f1_rownum.sql -o o_f1.sql -q --no_header
ora2pg -c /tmp/ora2pg.conf -t FUNCTION -i f2_connect_by.sql -o o_f2.sql -q --no_header
ora2pg -c /tmp/ora2pg.conf -t PACKAGE -i pkg.sql -o o_pkg.sql -q --no_header
grep -E 'salary|hired' TABLE.sql; grep LIMIT o_f1.sql; sed -n '/WITH RECURSIVE/,/INTO STRICT/p' o_f2.sql; grep current_setting o_pkg.sql
вывод
salary double precision,
hired timestamp(0),
FROM (SELECT salary FROM employees ORDER BY salary DESC) alias1 LIMIT (p_n);
WITH RECURSIVE cte AS (
SELECT COUNT(*)
FROM employees WHERE emp_id = p_root
UNION ALL
SELECT COUNT(*)
FROM employees JOIN cte c ON (c.emp_id = manager_id)
) SELECT * INTO STRICT v_cnt FROM cte;
SELECT COUNT(*) INTO STRICT v_cnt FROM employees WHERE salary < current_setting('pay.g_min_salary')::bigint;
NUMBER(10,2) стал double precision, а дата с временем сохранилась как timestamp(0). ROWNUM <= p_n превратился в LIMIT (p_n) после подзапроса, в CONNECT BY обе ветки рекурсии выбирают COUNT(*), а переменная пакета стала current_setting('pay.g_min_salary'); присваивания := 1000 в выводе нет. Выданный ora2pg код загружен в базу m3 в postgres:18 вместе с шестью INSERT ('' остаётся пустой строкой) и каждая функция вызвана:
Команда вызова: psql -d m3 -v VERBOSITY=terse -Atc 'SELECT top_salary_sum(3)' и так же остальные три.
вывод
> SELECT top_salary_sum(3)
16402
> SELECT team_size(1)
ERROR: column c.emp_id does not exist at character 144
> SELECT pay.without_note()
2
> SELECT pay.below_min()
ERROR: unrecognized configuration parameter "pay.g_min_salary"
Опыт 2: ответ Oracle и отчёт оценки
Тот же скрипт (таблица, шесть INSERT, функции, пакет) выполнен в Oracle Free от пользователя app (CREATE USER app IDENTIFIED BY app QUOTA UNLIMITED ON USERS, права CREATE SESSION, CREATE TABLE, CREATE PROCEDURE). Вывод sqlplus, повторные пустые строки убраны:
вывод (Oracle AI Database 26ai Free 23.26.3.0.0)
TOP3 TEAM1 TEAM2 WITHOUT_NOTE BELOW_MIN
---------- ---------- ---------- ------------ ----------
12000.85 6 3 4 1
NOTE_NULL NOTE_EMPTY
---------- ----------
4 0
Все четыре функции PostgreSQL разошлись с Oracle: 16402 вместо 12000.85, ошибка вместо 6, 2 вместо 4, ошибка вместо 1. Обе пустые строки Oracle сохранил как NULL (note IS NULL — 4 строки, note = '' — 0). Отчёт оценки ora2pg строит по живой базе:
ora2pg -c /tmp/ora2pg.conf -t SHOW_REPORT --estimate_cost -s "dbi:Oracle:host=dba-migr-ora;service_name=FREEPDB1;port=1521" -u app -w app -n APP -q
вывод (выдержка: колонки Comments сокращены, табуляции заменены пробелами)
FUNCTION 2 0 8.00 top_salary_sum: 3. team_size: 3.
PACKAGE BODY 1 0 12.00 pay.below_min: 3. pay.without_note: 3.
Total 5 0 22.00 22.00 cost migration units means approximatively 1 person-day(s). The migration unit was set to 5 minute(s)
Migration level : A-3
Function team_size total estimated cost: 3
TEST => 2
SIZE => 1
Отчёт ставит A-3 («migration that might be run automatically») и 22 единицы, около одного человеко-дня по его же подсчёту. У team_size в разбивке нет строки CONNECT BY, хотя в таблице оценок за неё положено 3 единицы; у top_salary_sum нет ROWNUM (1 единица). Остались только TEST (тестирование) и SIZE (размер кода).
Что показывает код ora2pg
Эти конструкции в оценку не попали, потому что оценка считается по результату конвертации. В lib/Ora2Pg.pm (тег v25.0, строки 4900–4903) в estimate_cost передаётся $sql_f, уже готовый PL/pgSQL:
$sql_output .= $sql_f . "\n\n";
if ($self->{estimate_cost})
{
my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $sql_f, 'FUNCTION');
Счётчики CONNECT BY (3) и ROWNUM (1) заданы в %UNCOVERED_SCORE файла lib/Ora2Pg/PLSQL.pm и ищут эти слова в тексте, где замена их уже убрала. Замена, сработавшая неверно, в оценке выглядит как выполненная.
Неверный ROWNUM объясняется двумя местами в plsql_to_plpgsql (PLSQL.pm, строки 1171–1172 и 1585–1587). Условие WHERE ROWNUM <= n вырезается, число запоминается в $tmp_val, а limit_clause дописывается в конец всего оператора, то есть после SUM:
$statements[$i] .= $class->{limit_clause};
$class->{limit_clause} = '';
...
if ($str =~ s/\s+(WHERE)\s+(?:\(\s*)?ROWNUM\s*<=\s*([^\s\)]+)(\s*\)\s*)?([^;]*)/ $1 $3$4/is) {
$tmp_val = $2;
}
LIMIT 3 применился к одной строке агрегата, и сумма вышла по всем шести сотрудникам: 16401,60, а после приведения к bigint — 16402. Документация Oracle как раз показывает эту форму для top-N: сортировка в подзапросе, ROWNUM снаружи.
Тип колонки выбирает _sql_type (делегирует в lib/Ora2Pg/Oracle.pm, строки 2154–2165); не путай её с _convert_type, которая в v25.0 разбирает SUBTYPE и CREATE TYPE (Ora2Pg.pm, строки 16379–16414):
if ($precision >= $scale)
{
if ($precision <= 6)
{
if ($self->{pg_supports_negative_scale}) {
return "decimal($precision,$scale)";
} else {
return 'real';
}
} elsif ($precision <= 15) {
return 'double precision';
}
При включённом pg_numeric_type (в конфиге из дистрибутива PG_NUMERIC_TYPE 1) NUMBER(10,2) попадает в ветку <= 15 и становится double precision. С -O PG_NUMERIC_TYPE=0 тот же TABLE выдал salary decimal(10,2).
Опыт 3: правки руками и сверка
Функции переписаны так, как их написал бы человек: LIMIT внутри подзапроса, рекурсия по emp_id, nullif(note, '') вместо веры в coalesce, а переменная пакета задана в сеансе через set_config:
CREATE FUNCTION top_salary_sum(p_n bigint) RETURNS numeric AS $body$
SELECT SUM(salary) FROM (SELECT salary FROM employees ORDER BY salary DESC LIMIT p_n) t;
$body$ LANGUAGE sql STABLE;
CREATE OR REPLACE FUNCTION team_size(p_root bigint) RETURNS bigint AS $body$
WITH RECURSIVE t AS (
SELECT emp_id FROM employees WHERE emp_id = p_root
UNION ALL
SELECT e.emp_id FROM employees e JOIN t ON e.manager_id = t.emp_id
) SELECT count(*) FROM t;
$body$ LANGUAGE sql STABLE;
CREATE OR REPLACE FUNCTION pay.without_note() RETURNS bigint AS $body$
SELECT count(*) FROM employees WHERE coalesce(nullif(note, ''), 'x') = 'x';
$body$ LANGUAGE sql STABLE;
вывод: select set_config('pay.g_min_salary','1000',false); select top_salary_sum(3), team_size(1), team_size(2), pay.without_note(), pay.below_min(); и подсчёт note IS NULL и note = ''
1000
12000.85|6|3|4|1
2|2
Пять чисел совпали с Oracle. Совпадение without_note держится на nullif, а не на данных: в PostgreSQL две строки остались '' (note IS NULL — 2, note = '' — 2; в Oracle 4 и 0), и любой другой запрос по note IS NULL разойдётся с Oracle. Решение принимается при загрузке: nullif(note, '') и CHECK (note <> ''), чтобы пустая строка не появилась снова. Переменную пакета ora2pg превращает в параметр сеанса; без set_config функция падает, как в опыте 1.
SQL Server: что ora2pg не ищет
В справке ora2pg есть режим -M (--mssql), в опыте он не запускался. Измерены три отличия поведения: '' против ' ', регистр и порядок NULL. Таблица из трёх строк (NULL, 'ok', ''), запросы SELECT TOP 2 ... ORDER BY note в SQL Server 2022 (sqlcmd -W -h -1) и ORDER BY note LIMIT 2 в PostgreSQL (psql -At):
вывод: SQL Server 2022 16.0.4305.5, затем PostgreSQL 18.6
equal equal
1 NULL
3
different|different
3|
2|ok
В тестовой базе SQL Server '' = ' ' и 'a' = 'A' истинны, в PostgreSQL оба ложны. NULL в SQL Server идёт первым, в PostgreSQL последним (NULL по умолчанию считается больше любого значения), поэтому TOP 2 и LIMIT 2 отобрали разные строки: 1 и 3 против 3 и 2. Приложение, рассчитывавшее на сравнение без учёта регистра, тихо перестанет находить строки.
Российские сборки PostgreSQL
Страницы проверены 28.10.2026, оценок производительности и полноты совместимости здесь нет. Документация Postgres Pro Enterprise 18 содержит главу 16 «Автономные транзакции» с расширениями SQL и PL/pgSQL. В документации Tantor SE 18.6 в оглавлении есть автономные транзакции, orafce, pg_hint_plan, oracle_fdw и tds_fdw. Для Pangolin официальные страницы pangolin.sbertech.ru и sbertech.ru ответили 401 — не проверено. Подходит ли сборка конкретной функции, решает сверка, как в опыте 3, а не список в оглавлении.
Что из этого следует
- Отчёт ora2pg годится для объёма работ, но не для вывода «правок не будет»: он считает уже переписанный код.
- Обязательная фаза — сверка: каждая функция вызывается на одних данных в Oracle и PostgreSQL, числа сравниваются. Здесь пять запросов нашли все четыре ошибки.
- При переезде с SQL Server проверяй регистр и порядок
NULLявно: в PostgreSQL по умолчанию'a' = 'A'ложно, аNULLидёт последним (опыт выше).
Чек-лист
- Запусти
ora2pg -t SHOW_REPORT --estimate_costна копии базы и читай разбивку по функциям, а не только уровень. - Для каждой функции с
ROWNUM,CONNECT BY,NVLи пакетной переменной возьми запрос с известным ответом Oracle и сверь число. - Найди колонки, где
''значит «нет значения», и реши, что делать с ними при загрузке. - Проверь в выводе
TABLEтипы денег:double precisionвместоnumeric— повод поменятьPG_NUMERIC_TYPE. - Переключай приложение только после того, как сверка совпала на всех функциях.
Убрать стенд: docker rm -f -v dba-migr-ora dba-migr-pg dba-migr-mssql && docker network rm dba-migr-net, образ Oracle Free: docker rmi gvenzl/oracle-free:23-slim.