All Products
Search
Document Center

AnalyticDB:Oracle Application Migration to AnalyticDB for PostgreSQL

Last Updated:Mar 30, 2026

AnalyticDB for PostgreSQL is compatible with Oracle syntax. This page covers everything you need to migrate an Oracle application: how to assess migration scope with Ora2Pg, configure compatibility mode, use Oracle-compatible functions via the Orafce extension, and convert PL/SQL constructs to PL/pgSQL.

Key differences at a glance

Review these high-level differences before diving into specific conversion rules. They affect the most common migration issues:

Area Oracle behavior AnalyticDB for PostgreSQL behavior
NULL concatenation 'abc' || NULL returns 'abc' Returns NULL (PostgreSQL default); use Oracle compatibility mode to change
ROWNUM Pseudo-column Use LIMIT (row limit) or row_number() over() (row numbering)
DUAL table Virtual table required by some queries Remove FROM DUAL, or create a dual table
CONNECT BY Native hierarchical query No direct equivalent; rewrite as a PL/pgSQL function
Packages CREATE PACKAGE No package support; convert to schemas + functions
PRAGMA Compilation directives Not supported; delete all PRAGMA statements
Transaction control in functions BEGIN, COMMIT, ROLLBACK in function bodies Not supported; move transaction control outside functions
OUTER JOIN (+) WHERE a.id = b.id(+) Use standard LEFT JOIN / RIGHT JOIN syntax
FOR LOOP REVERSE Counts down from 2nd number to 1st Counts down from 1st number to 2nd; swap boundaries
Functions with RETURN + OUT args Both allowed simultaneously Not supported; convert RETURN to an additional OUT argument
Associative arrays Supported No equivalent; requires redesign
User-defined functions in SELECT Can contain SQL statements Cannot contain SQL statements; rewrite as subqueries

Automate schema conversion with Ora2Pg

Ora2Pg is an open source tool that converts Oracle DDL statements for tables, views, and packages into PostgreSQL-compatible syntax.

Assess migration scope first

Before running a full conversion, generate a migration assessment report to identify which objects cannot be converted automatically:

ora2pg -t SHOW_REPORT

The report lists all database objects, their count, and which ones require manual effort. Use it to estimate scope before starting the migration.

What Ora2Pg handles automatically

  • Table and view DDL

  • Package structure (converted to schemas)

  • Basic PL/SQL-to-PL/pgSQL syntax

What requires manual correction

After running Ora2Pg, review and correct the output for these issues before running it against your instance:

  • Converted scripts may target a PostgreSQL syntax version later than the minor engine version of your instance

  • Conversion rules may be incomplete or incorrect for complex PL/SQL constructs

  • All constructs listed in the PL/SQL conversion section below

Configure compatibility mode

The adb_compatibility_mode parameter controls how AnalyticDB for PostgreSQL handles syntax that behaves differently across databases.

Parameter Valid values Default
adb_compatibility_mode postgres, oracle postgres

Check the current value before making changes:

SHOW adb_compatibility_mode;

To change the parameter at the instance level, Submit a ticket.Submit a ticket

String concatenation with NULL

The expression 'abc' || NULL behaves differently depending on the compatibility mode:

  • PostgreSQL mode (default): returns NULL

  • Oracle mode: returns 'abc' (Oracle treats NULL as an empty string)

Important

Before using string concatenation in Oracle compatibility mode, disable the Laser engine:

SET laser.enable = off;

Constant string type inference

AnalyticDB for PostgreSQL automatically recognizes constant strings as the TEXT type (not UNKNOWN) in statements such as CREATE TABLE AS SELECT. This behavior does not require any compatibility mode setting.

Use the Orafce extension

The Orafce extension provides Oracle-compatible functions that you can use in AnalyticDB for PostgreSQL without modifying or converting them.

Install Orafce once per database:

CREATE EXTENSION orafce;

Orafce also adds support for the Oracle VARCHAR2 data type.

Date and time functions

Function Description Example
add_months(day date, value int) Adds months to a date SELECT add_months(current_date, 2);2019-08-31
last_day(value date) Returns the last day of the month SELECT last_day('2018-06-01');2018-06-30
next_day(value date, weekday text) Returns the next occurrence of a weekday SELECT next_day(current_date, 'FRIDAY');
next_day(value date, weekday integer) Weekday as integer: 1=Sunday, 2=Monday, ..., 7=Saturday SELECT next_day('2019-06-22', 1);2019-06-23
months_between(date1 date, date2 date) Returns the number of months between two dates (positive if date1 > date2) SELECT months_between('2019-01-01', '2018-11-01');2

Timestamp truncation and rounding

Function Description Example
trunc(value timestamp with time zone, fmt text) Truncates to year (Y), quarter (Q), month, day, week, hour, minute, or second SELECT TRUNC(current_date, 'Q');2019-04-01
trunc(value timestamp with time zone) Truncates hours, minutes, and seconds SELECT TRUNC('2019-12-11'::timestamp);2019-12-11 00:00:00+08
trunc(value date) Truncates a date SELECT TRUNC('2019-12-11'::timestamp, 'Y');2019-01-01 00:00:00+08
round(value timestamp with time zone, fmt text) Rounds to the nearest value by unit (week, day, etc.) SELECT round('2018-10-06 13:11:11'::timestamp, 'YEAR');2019-01-01 00:00:00+08
round(value timestamp with time zone) Rounds to the nearest day SELECT round('2018-10-06 13:11:11'::timestamp);2018-10-07 00:00:00+08
round(value date, fmt text) Returns a rounded date SELECT round(TO_DATE('27-OCT-00','DD-MON-YY'), 'YEAR');2001-01-01
round(value date) Returns a rounded date SELECT round(TO_DATE('27-FEB-00','DD-MON-YY'));2000-02-27

NULL-handling functions

Function Description Example
nvl(anyelement, anyelement) Returns the second argument if the first is null; otherwise returns the first. Both arguments must be the same type. SELECT nvl(null, 1);1
nvl2(anyelement, anyelement, anyelement) Returns the third argument if the first is null; otherwise returns the second SELECT nvl2(null, 1, 2);2
lnnvl(bool) Returns true if the argument is null or false; returns false if the argument is true SELECT lnnvl(null);t
nanvl(float4, float4) / nanvl(numeric, numeric) Returns the second argument if the first is NaN; otherwise returns the first SELECT nanvl('NaN', 1.1);1.1

String functions

Function Description
instr(str text, patt text, start int, nth int) Returns the position of the nth occurrence of patt in str starting from start; returns 0 if not found
instr(str text, patt text, start int) Returns the position of the first occurrence from start
instr(str text, patt text) Searches from the beginning of the string
substr(str text, start int) Returns the substring starting at start
substr(str text, start int, len int) Returns a substring; len must be >= start and <= the string length
pg_catalog.substrb(varchar2, integer, integer) Returns a substring from a VARCHAR2 string by start and end position
pg_catalog.substrb(varchar2, integer) Returns a substring from a VARCHAR2 string from a position to the end
pg_catalog.lengthb(varchar2) Returns the byte count of a VARCHAR2 string; returns null for null input, 0 for empty string
lpad(string char, length int, fill char) Left-pads a string to the specified length.
Note

PostgreSQL removes trailing spaces from CHAR values; Oracle does not.

lpad(string char, length int) Left-pads with spaces
concat(text, text) Joins two strings
concat(text/anyarray, text/anyarray) Joins values of the same or different types
nlssort(text, text) Sorts data in a locale-specific order (e.g., 'en_US.UTF-8', 'C')
plvstr.rvrs(str text, start int, end int) Reverses characters from start to end
plvstr.rvrs(str text, start int) Reverses from start to the end of the string
plvstr.rvrs(str text) Reverses the entire string

Aggregation functions

Function Description Example
listagg(text) Concatenates values into a single string SELECT listagg(t) FROM (VALUES('abc'), ('def')) as l(t);abcdef
listagg(text, text) Concatenates with a separator SELECT listagg(t, '.') FROM (VALUES('abc'), ('def')) as l(t);abc.def
bitand(bigint, bigint) Bitwise AND on two integer values SELECT bitand(2, 6);2

Diagnostic functions

Function Description Example
dump("any") Returns type code, byte length, and internal representation SELECT dump('adb4pg');Typ=705 Len=7: 97,100,98,52,112,103,0
dump("any", integer) Specifies output format: 10 for decimal, 16 for hexadecimal. Passing 2 causes an error. SELECT dump('adb4pg', 16);

Regular expression functions

All regex functions below accept a flags argument with these values: 'i' (case-insensitive), 'c' (case-sensitive), 'n' (. matches newline), 'm' (multiline), 'x' (ignore whitespace).

Function Description
regexp_count(string, pattern) Returns the number of times pattern occurs; returns 0 if no match
regexp_count(string, pattern, startPos) Counts occurrences from startPos
regexp_count(string, pattern, startPos, flags) Counts with custom matching flags
regexp_instr(string, pattern) Returns the start position of the pattern; returns 0 if not found
regexp_instr(string, pattern, startPos, occurrence, return_opt, flags, group) Full signature: return_opt=0 returns the start position of the match, return_opt=1 returns the position after the match; group specifies the capturing group (0 = entire match)
regexp_like(string, pattern) Returns true if any substring matches the pattern
regexp_like(string, pattern, flags) Returns true with custom matching flags
regexp_substr(string, pattern) Returns the first matching substring
regexp_substr(string, pattern, startPos, occurrence, flags) Returns the matching substring for the specified occurrence from startPos

Functions supported without Orafce

These Oracle-compatible functions are available natively in AnalyticDB for PostgreSQL without installing Orafce:

Function Description Example
sinh(float) Hyperbolic sine SELECT sinh(0.1);0.100166750019844
tanh(float) Hyperbolic tangent SELECT tanh(3);0.99505475368673
cosh(float) Hyperbolic cosine SELECT cosh(0.2);1.02006675561908
decode(expression, value, return [,value,return]... [,default]) Returns the matching value, or the default if no match See example below
-- Create sample table
CREATE TABLE t1(id int, name varchar(20));
INSERT INTO t1 values(1,'alibaba');
INSERT INTO t1 values(2,'adb4pg');

-- decode: returns 'alibaba' for id=1, 'adb4pg' for id=2
SELECT decode(id, 1, 'alibaba', 2, 'adb4pg', 'not found') FROM t1;

Data type mappings

Oracle type AnalyticDB for PostgreSQL type
VARCHAR2 varchar or text
DATE timestamp
LONG text
LONG RAW bytea
CLOB text
NCLOB text
BLOB bytea
RAW bytea
ROWID oid
FLOAT double precision
DEC decimal
DECIMAL decimal
DOUBLE PRECISION double precision
INT int
INTEGER integer
REAL real
SMALLINT smallint
NUMBER numeric
BINARY_FLOAT double precision
BINARY_DOUBLE double precision
TIMESTAMP timestamp
XMLTYPE xml
BINARY_INTEGER integer
PLS_INTEGER integer
TIMESTAMP WITH TIME ZONE timestamp with time zone
TIMESTAMP WITH LOCAL TIME ZONE timestamp with time zone

Function mappings

Oracle function AnalyticDB for PostgreSQL equivalent
sysdate current_timestamp
trunc trunc or date_trunc
dbms_output.put_line RAISE statement
decode CASE WHEN or decode
NVL coalesce

Conversion of data in PL/SQL

PL/SQL (Procedural Language/SQL) maps to PL/pgSQL in AnalyticDB for PostgreSQL. The sections below cover the conversion rules for each PL/SQL construct.

Packages

PL/pgSQL does not support packages. Convert each package to a schema, and convert all procedures and functions within the package to standalone functions in that schema.

Oracle:

CREATE OR REPLACE PACKAGE pkg IS
    ...
END;

AnalyticDB for PostgreSQL:

-- Package becomes a schema
CREATE SCHEMA pkg;

Conversion rules for package contents:

  • Local variables in procedures and functions: no change required

  • Global variables: store in a temporary table (see Global variables below)

  • Package initialization blocks: remove them. If the logic cannot be removed, encapsulate it in a function and call the function explicitly when needed.

  • Procedures and functions: convert to functions in the corresponding schema. Each function must be prefixed with the schema name.

Example — function inside a package:

Oracle:

FUNCTION test_func (args int) RETURN int is
var number := 10;
BEGIN
    ...
END;

AnalyticDB for PostgreSQL:

-- RETURN -> RETURNS; IS/AS -> AS $$...$$; add LANGUAGE clause
CREATE OR REPLACE FUNCTION pkg.test_func(args int) RETURNS int AS
$$
    ...
$$
LANGUAGE plpgsql;

Procedures and functions

Convert Oracle procedures and functions to PL/pgSQL functions. The key changes are:

  • RETURN (in the function signature) -> RETURNS

  • Wrap the function body in $$...$$

  • Add a LANGUAGE plpgsql clause

  • Convert subprocedures to standalone functions

Example:

Oracle:

CREATE OR REPLACE FUNCTION test_func (v_name varchar2, v_version varchar2)
RETURN varchar2 IS
    ret varchar(32);
BEGIN
    IF v_version IS NULL THEN
        ret := v_name;
    ELSE
        ret := v_name || '/' || v_version;
    END IF;
    RETURN ret;
END;

AnalyticDB for PostgreSQL:

-- Changes: varchar2 -> varchar; RETURN -> RETURNS; IS -> AS; body wrapped in $$...$$
CREATE OR REPLACE FUNCTION test_func (v_name varchar, v_version varchar)
RETURNS varchar AS
$$
DECLARE
    ret varchar(32);
BEGIN
    IF v_version IS NULL THEN
        ret := v_name;
    ELSE
        ret := v_name || '/' || v_version;
    END IF;
    RETURN ret;
END;
$$
LANGUAGE plpgsql;

PL statements

FOR LOOP with REVERSE

PL/SQL and PL/pgSQL handle the REVERSE keyword differently:

  • PL/SQL: FOR i IN REVERSE 1..3 counts down from 3 to 1

  • PL/pgSQL: FOR i IN REVERSE 1..3 counts down from 1 to 3 (the opposite direction)

Swap the loop boundaries when converting:

Oracle:

FOR i IN REVERSE 1..3 LOOP
    DBMS_OUTPUT.PUT_LINE(TO_CHAR(i));
END LOOP;

AnalyticDB for PostgreSQL:

-- Boundaries swapped: 1..3 -> 3..1; DBMS_OUTPUT.PUT_LINE -> RAISE
FOR i IN REVERSE 3..1 LOOP
    RAISE '%', i;
END LOOP;

PRAGMA statements

PRAGMA statements are not supported. Delete all PRAGMA statements.

Transaction control

Functions in AnalyticDB for PostgreSQL do not support BEGIN, COMMIT, or ROLLBACK inside function bodies. Apply one of these strategies:

  • Delete the transaction control statements from the function body and include them outside the function call.

  • Split the function at each COMMIT or ROLLBACK boundary into separate functions.

EXECUTE (dynamic SQL)

AnalyticDB for PostgreSQL supports dynamic SQL, but with these differences from Oracle:

  • The USING syntax is not supported. Concatenate parameters directly into SQL strings.

  • Wrap database identifiers with quote_ident and string values with quote_literal.

Oracle:

EXECUTE 'UPDATE employees_temp SET commission_pct = :x' USING a_null;

AnalyticDB for PostgreSQL:

-- USING replaced with string concatenation using quote_literal
EXECUTE 'UPDATE employees_temp SET commission_pct = ' || quote_literal(a_null);

PIPE ROW functions

Replace PIPE ROW functions with table functions using RETURNS SETOF <type> and RETURN NEXT.

Oracle:

TYPE pair IS RECORD(a int, b int);
TYPE numset_t IS TABLE OF pair;

FUNCTION f1(x int) RETURN numset_t PIPELINED IS
DECLARE
    v_p pair;
BEGIN
    FOR i IN 1..x LOOP
        v_p.a := i;
        v_p.b := i+10;
        PIPE ROW(v_p);
    END LOOP;
    RETURN;
END;

SELECT * FROM f1(10);

AnalyticDB for PostgreSQL:

-- RECORD type -> CREATE TYPE; PIPE ROW -> RETURN NEXT; PIPELINED -> RETURNS SETOF
CREATE TYPE pair AS (a int, b int);

CREATE OR REPLACE FUNCTION f1(x int) RETURNS SETOF pair AS
$$
DECLARE
    rec pair;
BEGIN
    FOR i IN 1..x LOOP
        rec := row(i, i+10);
        RETURN NEXT rec;
    END LOOP;
    RETURN;
END
$$
LANGUAGE plpgsql;

SELECT * FROM f1(10);

Exception handling

  • Use the RAISE statement to throw exceptions.

  • After an exception is caught, the transaction cannot be rolled back inside the function. Rollback is only allowed outside user-defined functions.

For supported error codes, see the PostgreSQL error codes reference.

Functions with RETURN and OUT arguments

A function cannot use both a RETURN argument and an OUT argument simultaneously. Convert the RETURN argument to an additional OUT argument, then retrieve the return values using SELECT * FROM test_func(...) INTO rec.

Oracle:

CREATE OR REPLACE FUNCTION test_func(id int, name varchar(10), out_id out int) RETURNS varchar(10)
AS $body$
BEGIN
    out_id := id + 1;
    RETURN name;
END
$body$
LANGUAGE PLPGSQL;

AnalyticDB for PostgreSQL:

-- RETURN value converted to an OUT argument (out_name)
CREATE OR REPLACE FUNCTION test_func(id int, name varchar(10), out_id out int, out_name out varchar(10))
AS $body$
BEGIN
    out_id := id + 1;
    out_name := name;
END
$body$
LANGUAGE PLPGSQL;

-- Retrieve both output values
SELECT * FROM test_func(1, '1') INTO rec;

Single quotes in dynamic SQL string concatenation

When a string variable contains single quotes (e.g., adb'-'pg), direct concatenation causes a parse error because hyphens are interpreted as operators. Use quote_literal to safely embed the value.

Oracle:

sql_str := 'SELECT * FROM test1 WHERE col1 = ' || param1 || ' AND col2 = ''' || param2 || ''' AND col3 = 3';

AnalyticDB for PostgreSQL:

-- quote_literal wraps param2 safely, handling embedded quotes
sql_str := 'SELECT * FROM test1 WHERE col1 = ' || param1 || ' AND col2 = ' || quote_literal(param2) || ' AND col3 = 3';

Days between two timestamps

Oracle:

SELECT to_date('2019-06-30 16:16:16') - to_date('2019-06-29 15:15:15') + 1 INTO v_days FROM dual;

AnalyticDB for PostgreSQL:

-- Use extract() to get integer day count from an interval
SELECT extract('days' FROM '2019-06-30 16:16:16'::timestamp - '2019-06-29 15:15:15'::timestamp + '1 days'::interval)::int INTO v_days;

PL data types

RECORD type

Convert Oracle RECORD types to composite types:

Oracle:

TYPE rec IS RECORD (a int, b int);

AnalyticDB for PostgreSQL:

CREATE TYPE rec AS (a int, b int);

Nested tables and variable-size arrays

As PL variables, both NESTED TABLE and VARRAY map to the PostgreSQL ARRAY type.

Oracle:

DECLARE
    TYPE Roster IS TABLE OF VARCHAR2(15);
    names Roster := Roster('D Caruso', 'J Hamil', 'D Piro', 'R Singh');
BEGIN
    FOR i IN names.FIRST .. names.LAST LOOP
        IF names(i) = 'J Hamil' THEN
            DBMS_OUTPUT.PUT_LINE(names(i));
        END IF;
    END LOOP;
END;

AnalyticDB for PostgreSQL:

-- TABLE OF -> varchar[]; array indexing: names(i) -> names[i]; FIRST..LAST -> 1..array_length
CREATE OR REPLACE FUNCTION f1() RETURNS VOID AS
$$
DECLARE
    names varchar(15)[] := '{"D Caruso", "J Hamil", "D Piro", "R Singh"}';
    len int := array_length(names, 1);
BEGIN
    FOR i IN 1..len LOOP
        IF names[i] = 'J Hamil' THEN
            RAISE NOTICE '%', names[i];
        END IF;
    END LOOP;
    RETURN;
END
$$
LANGUAGE plpgsql;

SELECT f1();

If the nested table is used as a function return value, use a table function (RETURNS SETOF <type>) instead.

Associative arrays

There is no replacement for Oracle associative arrays in AnalyticDB for PostgreSQL. This construct requires redesign.

Global variables

AnalyticDB for PostgreSQL does not support global variables. Store package-level global variables in a temporary table and define accessor functions.

-- Store global variables in a temporary table
-- Note: The id column is the distribution key and cannot be modified
CREATE TEMPORARY TABLE global_variables (
    id int,
    g_count int,
    g_set_id varchar(50),
    g_err_code varchar(100)
);

INSERT INTO global_variables VALUES(0, 1, null, null);

-- Getter function
CREATE OR REPLACE FUNCTION get_variable() RETURNS SETOF global_variables AS
$$
DECLARE
    rec global_variables%rowtype;
BEGIN
    EXECUTE 'SELECT * FROM global_variables' INTO rec;
    RETURN NEXT rec;
END;
$$
LANGUAGE plpgsql;

-- Setter function
CREATE OR REPLACE FUNCTION set_variable(IN param varchar(50), IN value anyelement) RETURNS void AS
$$
BEGIN
    EXECUTE 'UPDATE global_variables SET ' || quote_ident(param) || ' = ' || quote_literal(value);
END;
$$
LANGUAGE plpgsql;

To modify a global variable:

-- Declare tmp_rec record; in the calling function
SELECT * FROM set_variable('g_err_code', 'error'::varchar) INTO tmp_rec;

To read a global variable:

SELECT * FROM get_variable() INTO tmp_rec;
error_code := tmp_rec.g_err_code;

SQL conversions

CONNECT BY

The CONNECT BY clause for hierarchical queries has no direct SQL equivalent in AnalyticDB for PostgreSQL. Rewrite it as a PL/pgSQL function that traverses the hierarchy iteratively.

Oracle:

SELECT emp_id, lead_id, emp_name, PRIOR emp_name AS lead_name, salary
FROM employee
START WITH lead_id = 0
CONNECT BY PRIOR emp_id = lead_id;

AnalyticDB for PostgreSQL (iterative traversal function):

CREATE OR REPLACE FUNCTION f1(tablename text, lead_id int, nocycle boolean) RETURNS SETOF employee AS
$$
DECLARE
    idx int := 0;
    res_tbl varchar(265) := 'result_table';
    prev_tbl varchar(265) := 'tmp_prev';
    curr_tbl varchar(256) := 'tmp_curr';
    current_result_sql varchar(4000);
    tbl_count int;
    rec record;
BEGIN
    EXECUTE 'TRUNCATE ' || prev_tbl;
    EXECUTE 'TRUNCATE ' || curr_tbl;
    EXECUTE 'TRUNCATE ' || res_tbl;
    LOOP
        -- Query the current hierarchy level and insert into tmp_curr
        current_result_sql := 'INSERT INTO ' || curr_tbl || ' SELECT t1.* FROM ' || tablename || ' t1';

        IF idx > 0 THEN
            current_result_sql := current_result_sql || ', ' || prev_tbl || ' t2 WHERE t1.lead_id = t2.emp_id';
        ELSE
            current_result_sql := current_result_sql || ' WHERE t1.lead_id = ' || lead_id;
        END IF;
        EXECUTE current_result_sql;

        -- If nocycle is false, remove already-traversed rows
        IF nocycle IS FALSE THEN
            EXECUTE 'DELETE FROM ' || curr_tbl || ' WHERE (lead_id, emp_id) IN (SELECT lead_id, emp_id FROM ' || res_tbl || ')';
        END IF;

        -- Exit when no more rows
        EXECUTE 'SELECT count(*) FROM ' || curr_tbl INTO tbl_count;
        EXIT WHEN tbl_count = 0;

        -- Promote current results to result and prev tables
        EXECUTE 'INSERT INTO ' || res_tbl || ' SELECT * FROM ' || curr_tbl;
        EXECUTE 'TRUNCATE ' || prev_tbl;
        EXECUTE 'INSERT INTO ' || prev_tbl || ' SELECT * FROM ' || curr_tbl;
        EXECUTE 'TRUNCATE ' || curr_tbl;
        idx := idx + 1;
    END LOOP;

    FOR rec IN EXECUTE 'SELECT * FROM ' || res_tbl LOOP
        RETURN NEXT rec;
    END LOOP;
    RETURN;
END
$$
LANGUAGE plpgsql;

ROWNUM

Limit result size:

Oracle:

SELECT * FROM t WHERE rownum < 10;

AnalyticDB for PostgreSQL:

SELECT * FROM t LIMIT 10;

Generate row numbers:

Oracle:

SELECT rownum, * FROM t;

AnalyticDB for PostgreSQL:

SELECT row_number() OVER() AS rownum, * FROM t;

DUAL table

Option 1 — Remove `FROM DUAL`:

Oracle:

SELECT sysdate FROM dual;

AnalyticDB for PostgreSQL:

SELECT current_timestamp;

Option 2 — Create a `dual` table:

CREATE TABLE dual (dummy varchar(1));
INSERT INTO dual VALUES ('X');

User-defined functions in SELECT statements

AnalyticDB for PostgreSQL does not allow user-defined functions that contain SQL statements to be called inside SELECT. Doing so causes a segment error:

ERROR: function cannot execute on segment because it accesses relation "public.t2"

Convert such functions to SQL expressions or subqueries.

Oracle:

CREATE OR REPLACE FUNCTION f1(arg int) RETURN int IS
    v int;
BEGIN
    SELECT b INTO v FROM t2 WHERE a = arg;
    RETURN v;
END;

SELECT a, f1(b) FROM t1;

AnalyticDB for PostgreSQL:

-- Inline the function logic as a join
SELECT t1.a, t2.b FROM t1, t2 WHERE t1.b = t2.a;

OUTER JOIN (+)

Two-table outer join:

Oracle:

SELECT * FROM a, b WHERE a.id = b.id(+);

AnalyticDB for PostgreSQL:

SELECT * FROM a LEFT JOIN b ON a.id = b.id;

Three-table outer join:

Use a Common Table Expression (CTE) to join two tables first, then apply RIGHT OUTER JOIN with coalesce.

Oracle:

SELECT * FROM test1 t1, test2 t2, test3 t3
WHERE t1.col1(+) BETWEEN NVL(t2.col1, t3.col1) AND NVL(t3.col1, t2.col1);

AnalyticDB for PostgreSQL:

WITH cte AS (
    SELECT t2.col1 AS low, t2.col2, t3.col1 AS high, t3.col2 AS c2
    FROM t2, t3
)
SELECT * FROM t1
RIGHT OUTER JOIN cte ON t1.col1 BETWEEN coalesce(cte.low, cte.high) AND coalesce(cte.high, cte.low);

MERGE INTO

In most cases, replace MERGE INTO with INSERT ON CONFLICT. For cases that INSERT ON CONFLICT cannot cover, use stored procedures.

Sequences

Oracle:

CREATE SEQUENCE seq1;
SELECT seq1.nextval FROM dual;

AnalyticDB for PostgreSQL:

CREATE SEQUENCE seq1;
SELECT nextval('seq1');

Cursors

Basic cursor traversal is supported. Use the OPEN/FETCH/CLOSE pattern:

Oracle:

FUNCTION test_func() IS
    CURSOR data_cursor IS SELECT * FROM test1;
BEGIN
    FOR i IN data_cursor LOOP
        -- process i
    END LOOP;
END;

AnalyticDB for PostgreSQL:

CREATE OR REPLACE FUNCTION test_func()
AS $body$
DECLARE
    data_cursor CURSOR FOR SELECT * FROM test1;
    i record;
BEGIN
    OPEN data_cursor;
    LOOP
        FETCH data_cursor INTO i;
        IF NOT FOUND THEN
            EXIT;
        END IF;
        -- process i
    END LOOP;
    CLOSE data_cursor;
END;
$body$
LANGUAGE PLPGSQL;

Cursors with the same name in recursive functions are not supported. Replace with FOR i IN <query>:

Oracle:

FUNCTION test_func(level IN number) IS
    CURSOR data_cursor IS SELECT * FROM test1;
BEGIN
    IF level > 5 THEN RETURN; END IF;
    FOR i IN data_cursor LOOP
        -- process i
        test_func(level + 1);
    END LOOP;
END;

AnalyticDB for PostgreSQL:

-- Named cursor removed; inline query used instead to support recursion
CREATE OR REPLACE FUNCTION test_func(level int) RETURNS void
AS $body$
DECLARE
    i record;
BEGIN
    IF level > 5 THEN
        RETURN;
    END IF;
    FOR i IN SELECT * FROM test1 LOOP
        -- process i
        PERFORM test_func(level + 1);
    END LOOP;
END;
$body$
LANGUAGE PLPGSQL;

Common pitfalls

These runtime behavior differences are the most frequent source of bugs after migration. Review them before testing your converted code.

Pitfall Oracle behavior AnalyticDB for PostgreSQL behavior Action
NULL in string concatenation 'abc' || NULL -> 'abc' Returns NULL Switch to Oracle compatibility mode, or rewrite using coalesce
FOR LOOP REVERSE direction Counts down from 2nd to 1st number Counts down from 1st to 2nd Swap loop boundaries
Transaction control in functions COMMIT/ROLLBACK allowed in function bodies Not supported Move outside function or split function
Dynamic SQL with USING EXECUTE '...' USING var USING not supported Concatenate with quote_literal / quote_ident
Functions with RETURN + OUT Both allowed Not supported Convert RETURN to an OUT argument
User-defined functions in SELECT Can query tables Cannot query tables Rewrite as a subquery or join
Associative arrays Supported No equivalent Requires redesign
Same-name cursors in recursion Supported Not supported Use FOR i IN <query> instead

What's next