Пришла задача перевода одного из наших приложений с базой данных Oracle в Postgres. Приложение написано на php использует базу данных только как хранилище данных, т.е. только таблицы, ключи, индексы, последовательности. Хранимых процедур нет. Вся бизнес логика, формирование запросов – внутри кода php. ORM в приложении не использовалось, запросы SQL формируются в коде.
И вроде бы задача не сложная, описана многими авторами, но все-таки как подойти с наименьшими трудозатратами разработчиков и тестировщиков? А если еще и доработки не прерывать, обеспечить работу приложения в обеих базах? Как найти и поправить все запросы, подлежащие исправлению, если их много и еще больше вариаций в зависимости от роли пользователя.
Мы предлагаем следующее решение
Выбрать все запросы из библиотечного кэша базы данных Oracle работающего приложения
Создать таблицы в базе данных postgres с помощью утилиты ora2pg или иным способом
Загрузить выбранные запросы в таблицу Postgres и прогнать их на пустых таблицах (можно и с данными), разработав для этого несложную процедуру в pgsql
Провести анализ ошибок, создать аналоги функций Oracle в Postgres (либо установить расширение orafce), поправить запросы, возможно создать функции в oracle для обратной совместимости приложения. Исправить конструкции, неработающие в Postgres.
Выборку запросов можно обогатить, прогнав php-код в нейрочатах с адекватным промптом. Так можно найти редкоиспользуемые sql.
-
Данные выгрузить из Oracle с помощью утилиты ora2pg и загрузить в Postgres с помощью psql.
Теперь рассмотрим каждый шаг подробнее.
1. Выборка всех запросов приложения из библиотечного кэша базы данных Oracle.
Выводим в пул SQL для дальнейшей вставки в тестовую таблицу migration.test_sql_list. Условия в where запроса нужно скорректировать исходя из Вашей базы данных. Нам оказалось достаточно указания имени схемы.
DECLARE
l_sql VARCHAR2(32000);
BEGIN
dbms_output.enable(1000000);
FOR r1 IN (
SELECT a.SQL_FULLTEXT
FROM v$sqlarea a
WHERE a.PARSING_SCHEMA_NAME NOT IN ('SYS','DBSNMP','DBSNMP','INSTRUCTION_TEST','ASSESSMENT_USER','AUDSYS','ORACLE_OCM','DIADMIN') –исключаем ненужное
AND upper(a.SQL_TEXT) LIKE '%XXX%' –условие для выбора только нужных запросов
ORDER BY a.SQL_TEXT
) LOOP
l_sql := 'INSERT INTO migration.test_sql_list(sql_text) VALUES ('''||replace(r1.sql_fulltext,'''','''''')||''');';
dbms_output.put_line(l_sql);
END LOOP;
END;
Выполнять скрипт желательно в часы наибольшей нагрузки. Еще лучше создать джоб для выборки уникальных SQL на разумное время с периодичностью 10 минут.
2. Создаем таблицы в Postgres.
Можно установить утилиту ora2pg и использовать ее для выгрузки скриптов таблиц (мы использовали v25 утилиты):
>ora2pg -t TABLE -b /папка_для_миграции -c config/ora2pg.conf -o table.sql
>ora2pg -t SEQUENCE -b /папка_для_миграции -c config/ora2pg.conf -o sequence.sql
В ora2pg.conf предварительно нужно прописать параметры для соединения с базой данных Oracle. Созданные скрипты выполняются в базе данных Postgres через psql или иным способом.
3. Прогон запросов приложения в Postgres.
Для целей миграции создаем в базе Postgres схему migration, таблицу и 2 функции:
CREATE TABLE migration.test_sql_list (
id serial4 NOT NULL,
sql_text text NOT NULL,
created_date timestamp NULL DEFAULT clock_timestamp(),
processed_date timestamp NULL,
errc varchar(1) NULL,
errm text NULL,
CONSTRAINT test_sql_list_pkey PRIMARY KEY (id));
CREATE OR REPLACE FUNCTION migration.test_query(p_sql text)
RETURNS text
LANGUAGE plpgsql;
AS $function$
DECLARE
-- matches TEXT[]; -- Массив для хранения найденных параметров
v_param TEXT;
v_sql text;
v_result TEXT;
v_cnt integer;
v_code text;
v_dummy integer;
BEGIN
v_sql := p_sql;
-- Извлечение всех параметров из строки запроса, перебор их и замена нв null
FOR v_param IN
SELECT par
FROM (
SELECT unnest(regexp_matches(p_sql, ':\w+', 'g')) as par
) a
ORDER BY length(par) desc
LOOP
v_sql := REPLACE(v_sql, v_param, 'NULL');
v_sql := REPLACE(v_sql, v_param, 'NULL'); --делаем еще 1 про
END LOOP;
if upper(substr(trim(v_sql),1,6)) = 'SELECT' then
v_sql = 'select count(*) from ('||v_sql||') b';
EXECUTE v_sql
into v_cnt;
else
EXECUTE v_sql;
GET DIAGNOSTICS v_cnt = ROW_COUNT;
v_result := 'ok';
v_dummy := 1/0 ; --откатим изменение и перехватим исключение
end if;
v_result := 'ok';
RETURN v_result;
EXCEPTION
when division_by_zero then
RETURN v_result;
WHEN OTHERS THEN
-- Получаем код состояния SQL и сообщение об ошибке
GET STACKED DIAGNOSTICS
v_code = PG_EXCEPTION_CONTEXT;
END;
$function$
;
CREATE OR REPLACE FUNCTION migration.process_sql_statements()
RETURNS void
LANGUAGE plpgsql
AS $function$
DECLARE
v_rec RECORD;
v_result TEXT;
BEGIN
-- Перебор всех SQL выражений из таблицы
FOR v_rec IN SELECT l.sql_text, l.id FROM migration.test_sql_list l LOOP
-- Вызов функции и получение результата
v_result := migration.test_query(v_rec.sql_text);
-- Вывод результата
-- RAISE NOTICE 'SQL: %, Result: %', v_result, v_rec.sql_text;
update migration.test_sql_list l
set processed_date = clock_timestamp()
,errc = case when upper(v_result) = 'OK' then 'S' else 'E' end
,errm = v_result
where l.id = v_rec.id;
END LOOP;
END;
$function$
;
Первая функция принимает текст SQL запроса, заменяет переменные подстановки (bind variables) на NULL и выполняет сам запрос. Вторая – прогоняет все запросы из таблицы migration.test_sql_list и сохраняет результат.
Теперь загружаем, полученные в п.1 запросы, в таблицу migration.test_sql_list и выполняем ф-цию
>select migration.process_sql_statements();
В таблице мы увидим результат

В колонке errm отразится ошибка или успех работы каждого SQL в Postgres.
4. Анализ полученных результатов.
В таблицу собраны примеры конструкций, которые не будут работать в Postgres и способы решения.
Функции | |
Функция SYSDATE |
аналог clock_timestamp(). Создать фунцию в Postgres c имененем SYSDATE(). Без скобок вариантов нет. Требуется переделка кода |
NVL(p1,p2) |
COALESCE(p1,p2). Создать фунцию в Postgres имененем NVL |
NVL2(p1,p2,p3) |
Исправить в коде на |
to_number() |
Создать фунцию в Postgres c имененем to_number() |
TO_DATE() |
переделать на TO_TIMESTAMP() в приложении (TO_TIMESTAMP('2025-02-23 23:01.02','yyyy‑mm‑dd hh24:mi:ss')). Не хотелось бы подменять стандартную ф‑цию postgres to_date, которая возвращает тип DATE (без времени) |
INSTR() |
Создать фунцию в Postgres c имененем INSTR() |
Последовательности | |
получение слеющено значения: имя_последовательности.nextval |
Замена на функцию nextval('имя_последовательности') |
Конструкции | |
ROWNUM (в where запросов) |
аналог LIMIT n (ROWNUM=1 → LIMIT 1; ROWNUM<=10 → LIMIT 10) Нужно переделывать запросы |
ROWNUM (в select запросов) |
select ROW_NUMBER() OVER (ORDER BY 1) AS ROW_NUM from table — работает и в postgres, и в oracle. Переделка запросов в бэке |
update table1 alias set alias.c1= |
алиасы в полях не поддерживаются, нужно исправлять на update table1 alias set c1=, то есть писать без алиасов. |
dual |
создание view dual |
псевдостолбец ROWID |
псевдостолбец ctid, либо переделать на использование первичных ключей |
Полученные таким образом ошибки мы исправляем созданием функций аналогов Oracle в базе Postgres и переделками в коде. Несложно добиться работы кода в обеих базах. Можно установить расширение orafce, но в нашем случае это оказалось избыточным.
Интересен вариант исправления работающего приложения в Oracle и итерационной выборки запросов. Так переход получится более плавным.
5. Обогащаем SQL с помощью нейрочатов
Полученный в п.1 список sql может оказаться недостаточно полным в силу того, что какие-то отчеты могут запускаться раз в месяц, квартал. Здесь можно призвать на помощь нейрочаты, попросив сформировать запросы из программного кода.
Нам больше понравились результаты Gemini с моделями google/gemini-2.5-flash и google/gemini-2.5-pro. Использовали такой промпт + php-код:
“Сформируй все возможные sql запросы, который может создать приведенный ниже php код. Полученные sql запросы должны быть вставлены в таблицу test_sql_list (sql_text text NOT NULL) в поле sql_text. Вместо переменной SCHEMA подставь instruction. Запросы должны начинаться с select, update, delete, insert, alter. Переменные подстановки должны начинаться с “:”. Запросы должны быть готовы к исполнения в oracle.
<Ваш php код>
В итоге получали готовый для вставки в таблицу SQL. Результат был отличный.
INSERT INTO test_sql_list (sql_text) VALUES
(
'SELECT filials.FILIAL, departs.FULL_OEBS_PATH, CASE WHEN INSTR(departs.FULL_OEBS_PATH, '' |'') > 0 THEN SUBSTR(departs.FULL_OEBS_PATH, 1, INSTR(departs.FULL_OEBS_PATH, '' |'') - 1) ELSE departs.FULL_OEBS_PATH END AS DEPARTS_OEBS FROM instruction.departs LEFT JOIN instruction.filials ON departs.FILIAL = filials.ID_ WHERE (departs.FULL_OEBS_PATH LIKE :path OR departs.FULL_OEBS_PATH LIKE ''%'' || REPLACE(REPLACE(:path, ''«'', '''' ), ''»'', '''' )) and ROWNUM = 1 '
),
......
6. Загрузка данных таблиц.
Остался последний шаг – выгрузка данных из базы Oracle и загрузка в Postgres.
Данные выгружаем из Oracle утилитой ora2pg:
>ora2pg -t INSERT -o data.sql -b /mig/ora2pg -c config/ora2pg.conf --no_start_scn
Утилита создает скрипты (с командами insert) для загрузки. Для каждой таблицы создается отдельный файл. Загрузка выполняется из стандартной psql. Загрузку можно выполнить в несколько потоков., распределив файлы. Также стоит разбить на разные транзакции. Перед загрузкой лучше удалить внешние ключи и индексы.
Выводы
В статье описана методика для перевода приложения из базы данных Oracle в Postgres. Методика проста в использовании и опробована в деле.
vshmdt
спасибо за статью. По части работы с нативным SQL понятно. Могли бы подсказать:
после замены драйвер оракла на постгрес что-то отвалилось в самом приложении?
индексы и перформанс после переезда нужно тюнить?