Справочник DBA: базы данных изнутри читается по порядку

Статья 14.6 · раздел 14 «Консалтинг: аудит, оптимизация, миграции» · 107 из 109

Миграция с Oracle на PostgreSQL: ora2pg оценил схему как A-3, а 4 функции из 4 дали другой ответ

опубликовано
читать
≈ 9 мин
автор
среда и версия
PostgreSQL 18.6 (postgres:18), ora2pg 25.0 (тег v25.0, из исходников) и Oracle AI Database 26ai Free 23.26.3.0.0 (gvenzl/oracle-free:23-slim, --memory 3g; на 2g первый старт закончился сообщением Killed) в контейнерах; SQL Server 2022 16.0.4305.5 (--memory 2g); учебная схема из одной таблицы на 6 строк и четыре функции; опыт выполнен 28 октября 2026
содержание решать задачи SQL на собеседовании

Отчёт оценки миграции говорит «уровень A, ручной правки нет», а функции после конвертации возвращают другое число или падают. Причина в устройстве ora2pg: он переписывает PL/SQL набором регулярных замен и считает стоимость уже по переписанному коду, а семантику Oracle (пустая строка равна NULL, ROWNUM нумерует строки до сортировки) не знает. Соответствия понятий двух баз лежат в статье про Oracle и SQL Server для DBA из PostgreSQL и здесь не повторяются. Ниже — учебная схема на 6 строк, которую прогнали через ora2pg 25.0 и через живой Oracle Free: из четырёх функций ни одна не вернула ответ Oracle, пока их не поправили руками.

DBA / 01

Фазы миграции: что показал опыт на каждой

Шесть фаз миграции с Oracle и результат опыта на каждой Оценка: ora2pg поставил уровень A-3 и 22 единицы, считая уже переписанный код. Схема: NUMBER(10,2) стал double precision. Код: функции вернули 16402, ошибку, 2 и ошибку вместо 12000.85, 6, 4 и 1. Данные: пустая строка осталась пустой, в Oracle NULL: 4 строки против 2. Двойная запись: после правок пять чисел совпали с Oracle. Переключение: решает сверка, а не оценка. фаза что показал опыт итог оценка ora2pg: уровень A-3, 22 единицы считает уже переписанный код схема NUMBER(10,2) → double precision деньги в float по умолчанию код 0 из 4 функций дали ответ Oracle 16402, ошибка, 2, ошибка данные note IS NULL: Oracle 4, PostgreSQL 2 пустая строка осталась '' двойная запись сверка: после правок 5 чисел = Oracle 12000.85 · 6 · 3 · 4 · 1 переключение после совпадения сверки оценка этого не решает
Отчёт оценки стоит в начале и выглядит самым надёжным, но проверяемых чисел даёт меньше всего. Решение о переключении держится на сверке результатов Oracle и PostgreSQL, и расхождения нашлись только на ней.

Стенд

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 идёт последним (опыт выше).

Чек-лист

  1. Запусти ora2pg -t SHOW_REPORT --estimate_cost на копии базы и читай разбивку по функциям, а не только уровень.
  2. Для каждой функции с ROWNUM, CONNECT BY, NVL и пакетной переменной возьми запрос с известным ответом Oracle и сверь число.
  3. Найди колонки, где '' значит «нет значения», и реши, что делать с ними при загрузке.
  4. Проверь в выводе TABLE типы денег: double precision вместо numeric — повод поменять PG_NUMERIC_TYPE.
  5. Переключай приложение только после того, как сверка совпала на всех функциях.

Убрать стенд: 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.

Источники

Выжимки статей в Telegram: одна ловушка, код с выводом и ссылка на полный разбор. t.me/koddo_ru