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 treatsNULLas an empty string)
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 |
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 plpgsqlclause -
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..3counts down from 3 to 1 -
PL/pgSQL:
FOR i IN REVERSE 1..3counts 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
COMMITorROLLBACKboundary into separate functions.
EXECUTE (dynamic SQL)
AnalyticDB for PostgreSQL supports dynamic SQL, but with these differences from Oracle:
-
The
USINGsyntax is not supported. Concatenate parameters directly into SQL strings. -
Wrap database identifiers with
quote_identand string values withquote_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
RAISEstatement 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 |