AWS Database Blog

Migrate Oracle collections to Amazon Aurora PostgreSQL-Compatible Edition or Amazon RDS for PostgreSQL

Migrating from Oracle to PostgreSQL can involve several steps to ensure a smooth transition. A notable example is an Oracle collection, which is a feature that Oracle database developers commonly use. PostgreSQL does not directly support Oracle collections, and there currently isn’t an extension that adds this capability. There are several decisions you have to make when migrating Oracle collections to PostgreSQL to successfully make the transition.

Oracle database developers use collections in many scenarios, such as the following:

  • Caching static values to reduce the number of database calls, consequently enhancing speed and efficiency
  • Repeatedly accessing data within a block or subprogram, often involving conditional modification and storage for future retrieval within the same transaction
  • Utilizing collections as function or procedure parameters or return types to pass multiple values to other calling subprograms
  • Passing values to an application where the application layer defines a list, array, or map to process values

In this post, we discuss Oracle simple collections, composite collections, and multi-dimensional collections, and how you can migrate these Oracle features to Amazon Aurora PostgreSQL-Compatible Edition or Amazon Relational Database Service (Amazon RDS) for PostgreSQL using PostgreSQL’s native custom types and arrays while following recommended best practices.

Overview of representing Oracle collections in PostgreSQL

PostgreSQL database provides arrays, which you can use as an option for porting Oracle collections. Each array element is a data type similar to that of the collection. You can create an array for any built-in or user-defined data type.

Declaring an array variable in PostgreSQL is done using the [] (array) operator. Here’s how you can declare arrays and use them in PostgreSQL:

  • Simple array creation – You can create a simple array in PostgreSQL using the array [] operator of any predefined data type. For example, you can create an array of varchar(20) and assign values using following SQL statements:
Arr1 varchar(20)[];
Arr1[1] := 'ABC';
Arr1[2] := 'DEFGH';
  • Composite array creation – You can create a composite array in PostgreSQL by first creating a user-defined type and then using the type to create an array. The following statements create a custom type named person_type, define an array variable of the custom type created, and assign values to array elements:
-- Create a custom type
CREATE TYPE person_type AS (
    first_name text,
    last_name  text,
    age int
);

--Define composite array
arr2 person_type[];

--Assign values to composite array
arr2[1] := ROW('John', 'Smith', 30);

You can also use a domain in PostgreSQL for defining array variables. You create a domain when you intend to utilize the same existing structure. For example, you can create a domain for person_type[] and use it as is, without needing to declare arrays with the operator []:

-- Create a array type of person_type
CREATE DOMAIN person_arr AS person_type[];

--Define array variable of domain type
arr3 person_arr;

--Assign values to composite array
arr3[1] := ROW('John', 'Smith', 30);

Now that you have learned about arrays in PostgreSQL, let’s dive deep into the use cases and challenges while migrating collections from an Oracle database to PostgreSQL.

Prerequisites

To get started with the examples explained in this post, you must have the following prerequisites:

  • An active AWS account
  • A source Oracle database (on premises or using Amazon RDS for Oracle)
  • A target RDS for PostgreSQL or Aurora PostgreSQL database
  • A database user with the following privileges on the target database (provide your own schema and user):
GRANT CONNECT ON DATABASE TO YOUR_DB_USER;
GRANT USAGE ON SCHEMA YOUR_SCHEMA_NAME TO YOUR_DB_USER;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA YOUR_SCHEMA_NAME TO YOUR_DB_USER;

Create the following table and insert data using the following script in order to try out the examples described in this post. Run the corresponding statements in Oracle and PostgreSQL databases:

For Oracle, use the following code:

CREATE TABLE Student_Marks(
    StudID         INTEGER,
    Subject        VARCHAR2(20),
    ScoredMarks    INTEGER,
    TotalMarks     INTEGER
);

INSERT ALL
INTO Student_Marks VALUES(10101, 'MATHS', 62, 75)
INTO Student_Marks VALUES(10101, 'ENGLISH', 70, 100)
INTO Student_Marks VALUES(10101, 'ZOOLOGY', 32, 50)
INTO Student_Marks VALUES(10102, 'MATHS', 35, 50)
INTO Student_Marks VALUES(10102, 'COMPUTERS', 43, 50)
INTO Student_Marks VALUES(10102, 'PHYSICS', 69, 100)
INTO Student_Marks VALUES(10102, 'ENGLISH', 81, 100)
SELECT * FROM DUAL;

For PostgreSQL, use the following code:

CREATE TABLE Student_Marks(
    StudID         INTEGER,
    Subject        VARCHAR(20),
    ScoredMarks    INTEGER,
    TotalMarks     INTEGER
);

INSERT INTO Student_Marks VALUES
(10101, 'MATHS', 62, 75),
(10101, 'ENGLISH', 70, 100),
(10101, 'ZOOLOGY', 32, 50),
(10102, 'MATHS', 35, 50),
(10102, 'COMPUTERS', 43, 50),
(10102, 'PHYSICS', 69, 100),
(10102, 'ENGLISH', 81, 100);

Migrate a simple collection using BULK COLLECT to PostgreSQL

Oracle PL/SQL developers use the BULK COLLECT feature in SELECT statements that retrieve multiple rows with a single fetch, improving the speed of data retrieval. BULK COLLECT is currently not available on PostgreSQL databases. If you convert BULK COLLECT with a traditional row-by-row processing approach, then efficiency might become an issue. Therefore, it’s better to use SQL statements rather than looping each row. Moreover, you may learn more about how to migrate BULK COLLECT in PostgreSQL in this post.

The PostgreSQL database offers the array function array_agg, which enables you to achieve a processing approach similar to Oracle. To illustrate, consider a basic collection scenario involving BULK COLLECT. We can then adjust the SQL query in PostgreSQL to retrieve all rows within a single fetch operation.

The following PL/SQL program fetches student marks data from the Student_Marks table and stores them in memory as an OUT parameter, which can be passed to other calling routines and applications for further processing. This following Oracle example demonstrates how BULK COLLECT is used to fetch all rows in a single database call.

/* Package spec declaration */
CREATE OR REPLACE PACKAGE stud_marks
IS
    TYPE MarksArray IS TABLE OF INTEGER INDEX BY PLS_INTEGER;
    PROCEDURE get_stud_marks(
        i_StudentID   IN  INTEGER,
        IO_Stud_Marks OUT MarksArray
    );
END stud_marks;
/

/* Package body declaration */
CREATE OR REPLACE PACKAGE BODY stud_marks
IS
    /* Subprogram uses collections as INOUT/OUT parameter */
    PROCEDURE get_stud_marks(
        i_StudentID   IN  INTEGER,
        IO_Stud_Marks OUT MarksArray
    )
    AS
    BEGIN
        SELECT TotalMarks
        BULK COLLECT INTO IO_Stud_Marks
        FROM Student_Marks WHERE StudID = i_StudentID;
    END get_stud_marks;
END stud_marks;
/

Let’s run the package procedure through an anonymous block and check the result:

SET SERVEROUTPUT ON;
DECLARE
    v_Stud_Marks stud_marks.MarksArray;
BEGIN
    stud_marks.get_stud_marks(10101, v_Stud_Marks);   --Invoke procedure
    FOR I IN v_Stud_Marks.FIRST..v_Stud_Marks.LAST
    LOOP
        DBMS_OUTPUT.PUT_LINE(i||':'|| v_Stud_Marks(i));
    END LOOP;
END;
/

We get the following output:

1:75
2:100
3:50

PL/SQL procedure successfully completed.

Let’s now convert this Oracle package function to an equivalent PL/pgSQL code. There are a few things to keep in mind when migrating such code to PostgreSQL:

  • PostgreSQL does not support Oracle’s PACKAGE syntax. For this post, we used PostgreSQL schemas to help convert Oracle packages.
  • All functions or procedures are standalone in nature that you create in a schema in PostgreSQL.
  • It’s not recommended to create types or domains in a procedure or function because they’re created as persistent objects within the database and if you run the PL/pgSQL code a second time, it reports the error message “Object already exists.”
  • You should create types and domains in PostgreSQL for collection types defined in the Oracle package specification or body.
  • You should declare an array variable as a domain and use the array_agg function to retrieve all rows as a single unit to populate the array.

With this understanding, let’s convert the Oracle code to PostgreSQL.

/* Creating a schema for package */
CREATE SCHEMA IF NOT EXISTS stud_marks;

/* Creating a Domain for the collection type in the schema */
CREATE DOMAIN stud_marks.MarkArray AS VARCHAR[];

/* Create function in the schema */
CREATE OR REPLACE FUNCTION stud_marks.get_stud_marks(
    Student_ID         IN  INTEGER,
    IO_MarkGradeArray  OUT stud_marks.MarkArray
)
AS $$
BEGIN
    SELECT array_agg(TotalMarks) /* array_agg as alternate to BULK COLLECT */
    INTO IO_MarkGradeArray
    FROM Student_Marks WHERE StudID = Student_ID;
END $$
LANGUAGE plpgsql;

Let’s run the PostgreSQL function through an anonymous block and check the result:

set client_min_messages = debug;

DO $$
DECLARE
    v_Stud_Marks stud_marks.MarkArray;
BEGIN
    v_Stud_Marks:= stud_marks.get_stud_marks(10101);

    --Accessing the collection type
    FOR i IN array_lower(v_Stud_Marks, 1) .. array_upper(v_Stud_Marks, 1)
    LOOP
        RAISE DEBUG USING message := concat_ws('',i, ':', v_Stud_Marks[i]);
    END LOOP;
    RAISE DEBUG 'Program executed successfully.';
END $$;

We get the following output:

DEBUG: 1:75
DEBUG: 2:100
DEBUG: 3:50

DEBUG: Program executed successfully.

Things to note:

  • We use the array_agg function in PostgreSQL as an alternate to BULK COLLECT in Oracle.
  • You should plan well if you need to create a domain for the type defined in Oracle. If the type is a global object, you must create a domain or type in PostgreSQL.

Let’s now explore another scenario with composite collections, and understand how to handle them in PostgreSQL.

Migrate a composite collection to PostgreSQL

Oracle Database allows collections to be composite, where the elements of the collection can contain more than one column. Because all elements of a collection always have the same data type, this type of collection always refers to a collection created from a user-defined data type.

The following PL/SQL program declares a record type named R_MarkGradeRec, comprised of four attributes. An array named MarkGradeArray of type R_MarkGradeRec is created that can store student marks. The procedure get_grades retrieves and populates an output array (IO_MarkGradeArray) with grade-related information for a specified student ID.

We use the following code in Oracle:

/* Package spec declaration */
CREATE OR REPLACE PACKAGE stud_grades
IS
TYPE R_MarkGradeRec IS RECORD(
    SubjectID   VARCHAR2(10),
    ScoredMarks INTEGER,
    TotalMarks  INTEGER,
    Grade       INTEGER
);

TYPE MarkGradeArray IS TABLE OF R_MarkGradeRec INDEX BY PLS_INTEGER;

Procedure get_grades(
    Student_ID        IN  INTEGER,
    IO_MarkGradeArray OUT MarkGradeArray
);
END stud_grades;
/

/* Package body declaration */
CREATE OR REPLACE PACKAGE BODY stud_grades
IS
/* Subprogram uses collections as INOUT/OUT parameter */
    Procedure get_grades(
     Student_ID        IN  INTEGER,
     IO_MarkGradeArray OUT MarkGradeArray
    )
    AS
        C_getMarks      SYS_REFCURSOR;
        l_sql           VARCHAR2(1000);
        l_subjID        VARCHAR2(10);
        l_ScoredMarks   INTEGER;
        l_TotalMarks    INTEGER;
        l_counter       INTEGER := 1;
    BEGIN
        l_sql := 'SELECT Subject, ScoredMarks, TotalMarks FROM Student_Marks WHERE StudID = '||Student_ID;

        OPEN C_getMarks FOR l_sql;
        LOOP
            FETCH C_getMarks INTO l_subjID, l_ScoredMarks, l_TotalMarks;
            EXIT WHEN C_getMarks%NOTFOUND;
            IO_MarkGradeArray(l_counter).SubjectID := l_subjID;
            IO_MarkGradeArray(l_counter).ScoredMarks := l_ScoredMarks;
            IO_MarkGradeArray(l_counter).TotalMarks := l_TotalMarks;
            IO_MarkGradeArray(l_counter).Grade := TO_CHAR(l_ScoredMarks/l_TotalMarks * 100, '99');
            l_counter := l_counter +1;
        END LOOP;
        CLOSE C_getMarks;
    END get_grades;
END stud_grades;
/

Let’s run the script that retrieves and displays grade information for a specific student (identified by ID 10102) using the custom array type MarkGradeArray. It prints the SubjectID and TotalMarks for each grade entry.

SET SERVEROUTPUT ON;
DECLARE
    v_MarkGradeArray stud_grades.MarkGradeArray;
BEGIN
    stud_grades.get_grades(10102, v_MarkGradeArray);
    FOR i IN v_MarkGradeArray.FIRST.. v_MarkGradeArray.LAST
    LOOP
        DBMS_OUTPUT.PUT_LINE(v_MarkGradeArray(i).SubjectID|| ':'|| v_MarkGradeArray(i).TotalMarks);
    END LOOP;
END;
/

We get the following output:

MATHS:50
COMPUTERS:50
PHYSICS:100
ENGLISH:100

PL/SQL procedure successfully completed.

You can migrate the aforementioned Oracle code to PostgreSQL by completing the following steps:

  1. Create a user-defined type with the same structure as the Oracle user-defined type.
  2. Create a domain in PostgreSQL using the created type.
  3. Declare an array variable and modify the cursor SELECT statement using array_agg and the row operator.

We use the following code in PostgreSQL:

CREATE SCHEMA IF NOT EXISTS stud_grades;

CREATE TYPE stud_grades.R_MarkGradeArray AS(
    SubjectID     VARCHAR(10),
    ScoredMarks   INTEGER,
    TotalMarks    INTEGER,
    Grade         INTEGER
);

CREATE DOMAIN stud_grades.MarkGradeArray AS stud_grades.R_MarkGradeArray[];

CREATE OR REPLACE FUNCTION stud_grades.get_grades(
    Student_ID IN INTEGER,
    IO_MarkGradeArray OUT stud_grades.MarkGradeArray
)
AS $$
DECLARE
    c_getMarks REFCURSOR;
    l_sql VARCHAR(1000);
    l_subjID VARCHAR(10);
    l_ScoredMarks INTEGER;
    l_TotalMarks INTEGER;
    l_counter INTEGER := 1;
BEGIN
    l_sql := 'SELECT Subject, ScoredMarks, TotalMarks FROM Student_Marks WHERE StudID = $1';
    OPEN C_getMarks FOR EXECUTE l_sql USING Student_ID;
    LOOP
        FETCH C_getMarks INTO l_subjID, l_ScoredMarks, l_TotalMarks;
        EXIT WHEN NOT FOUND;
        IO_MarkGradeArray[l_counter] := row(l_subjID, l_ScoredMarks, l_TotalMarks, (l_ScoredMarks/l_TotalMarks)::INTEGER * 100);
        l_counter := l_counter + 1;
    END LOOP;
END $$
LANGUAGE plpgsql;

Let’s run the script:

set client_min_messages = debug;

DO $$
DECLARE
    V_MarkGradeArray stud_grades.MarkGradeArray ;
BEGIN
    V_MarkGradeArray:= stud_grades.get_grades(10102);

--Accessing the collection type
    FOR i IN array_lower(V_MarkGradeArray, 1) .. array_upper(V_MarkGradeArray, 1)
    LOOP
        RAISE DEBUG USING message := concat_ws('',V_MarkGradeArray[i].SubjectID, ':', V_MarkGradeArray[i].TotalMarks);
    END LOOP;
    RAISE DEBUG 'Program executed successfully.';
END $$;

We get the following output:

DEBUG: MATHS:50
DEBUG: COMPUTERS:50
DEBUG: PHYSICS:100
DEBUG: ENGLISH:100

DEBUG: Program executed successfully.

Migrate a nested composite collection to PostgreSQL

Both the Oracle and PostgreSQL database support nested types, but let’s see the operational differences with respect to collections or arrays in both databases.

You can include a record as a field within another record. This is called a nested record. The record that contains the nested record as a field is called the enclosing record. The following code illustrates how you can port nested composite collections to PostgreSQL and carry out the following actions:

  • Declare two custom types, school_info and student_info:
    • school_info is a record type that represents an address and all attributes are of built-in types.
    • student_info is a nested record type because it contains one attribute of the school_info record type for student’s school details.
  • The students_address_info procedure populates the IO_student_info_array with student information, including IDs, names, and school details.

We use the following code in Oracle:

/* Package spec declaration */
CREATE OR REPLACE PACKAGE stud_info
IS
    TYPE school_info IS RECORD (
        house_addr VARCHAR2(50),
        street     VARCHAR2(50),
        city       VARCHAR2(50)
    );

    TYPE student_info IS RECORD (
        id         INTEGER,
        name       VARCHAR2(50),
        school_det school_info
    );

    TYPE student_info_tbl IS TABLE OF student_info INDEX BY BINARY_INTEGER;

    Procedure students_address_info(
        IO_student_info_array OUT student_info_tbl
    );
END stud_info;
/

/* Package body declaration */
CREATE OR REPLACE PACKAGE BODY stud_info
IS
/* Subprogram uses collections as INOUT/OUT parameter */
Procedure students_address_info(
    IO_student_info_array OUT student_info_tbl
)
AS
    i INTEGER := 1;
BEGIN
    IO_student_info_array(1).id := 101010 ;
    IO_student_info_array(1).name := 'Peter';
    IO_student_info_array(1).school_det.house_addr := 'F 30, ABC Apartments';
    IO_student_info_array(1).school_det.street := 'XYZ Street';
    IO_student_info_array(1).school_det.city := 'HYD CITY, DC';
    IO_student_info_array(2).id := 202020;
    IO_student_info_array(2).name := 'Willard';
    IO_student_info_array(2).school_det.house_addr := 'Lane 4, ABC Center';
    IO_student_info_array(2).school_det.street := NULL;
    IO_student_info_array(2).school_det.city := 'XYZ City, Washington';
    IO_student_info_array(3).id := 303030;
    IO_student_info_array(3).name := 'Andrew';
    IO_student_info_array(3).school_det:= NULL;
END;
end stud_info;
/

Let’s run the code:

SET SERVEROUTPUT ON;

DECLARE
    IO_STUDENT_INFO_ARRAY STUD_INFO.STUDENT_INFO_TBL;
    i INTEGER := 1;
BEGIN
    STUD_INFO.STUDENTS_ADDRESS_INFO(IO_STUDENT_INFO_ARRAY);
    FOR rec IN IO_STUDENT_INFO_ARRAY.FIRST .. IO_STUDENT_INFO_ARRAY.LAST
    LOOP
        DBMS_OUTPUT.PUT_LINE('Element- '||i||': ID='||IO_STUDENT_INFO_ARRAY(i).id||', Name='||IO_STUDENT_INFO_ARRAY(i).name||' is staying at '||IO_STUDENT_INFO_ARRAY(i).school_det.house_addr||','||IO_STUDENT_INFO_ARRAY(i).school_det.street||','||IO_STUDENT_INFO_ARRAY(i).school_det.city);
i:=i+1;
    END LOOP;
    DBMS_OUTPUT.PUT_LINE('Program executed successfully.');
END;
/

We get the following output:

Element- 1: ID=101010, Name=Peter is staying at F 30, ABC Apartments,XYZ Street,HYD CITY, DC
Element- 2: ID=202020, Name=Willard is staying at Lane 4, ABC Center,,XYZ City, Washington
Element- 3: ID=303030, Name=Andrew is staying at ,,
Program executed successfully.

The PL/SQL procedure successfully completed.

The following PostgreSQL code snippet is the equivalent code for the preceding nested composite collection:

CREATE SCHEMA IF NOT EXISTS stud_info;

CREATE TYPE stud_info.school_info AS (
    house_addr VARCHAR(50),
    street     VARCHAR(50),
    city       VARCHAR(50)
);

CREATE TYPE stud_info.student_info AS(
    id         INTEGER,
    name       VARCHAR(50),
    school_det stud_info.school_info
);

/* Create Domain from above type */
CREATE DOMAIN stud_info.student_info_tbl AS stud_info.student_info[];

CREATE OR REPLACE FUNCTION stud_info.students_address_info(
    IO_student_info_tbl OUT stud_info.student_info_tbl
)
AS $$
DECLARE
    i INTEGER := 1;
    v_rec stud_info.student_info;
BEGIN
    v_rec := IO_student_info_tbl[1];
    v_rec.id := 101010;
    v_rec.name := 'Peter';
    v_rec.school_det:= ROW('F 30, ABC Apartments', 'XYZ Street', 'HYD CITY, DC');
    IO_student_info_tbl[1] := v_rec;

    /* ROW constructor is optional. PostgreSQL treats values enclosed in parenthesis as RECORD type and can implicitly type cast to proper type. */
    v_rec := IO_student_info_tbl[2];
    v_rec.id := 202020;
    v_rec.name := 'Willard';
    v_rec.school_det:= ('Lane 4, ABC Center', NULL, 'XYZ City, Washington');
    IO_student_info_tbl[2] := v_rec;
    v_rec := IO_student_info_tbl[3];
    v_rec.id := 303030;
    v_rec.name := 'Andrew';
    v_rec.school_det:= NULL;
    IO_student_info_tbl[3] := v_rec;
END $$
LANGUAGE plpgsql;

Let’s run the code:

set client_min_messages = debug;

DO $$
DECLARE
    i integer=1;
    V_student_info_Array stud_info.student_info_tbl ;
BEGIN
    V_student_info_Array:= stud_info.students_address_info();
    /* Accessing the collection type */
    FOR rec IN array_lower(V_student_info_Array, 1) .. array_upper(V_student_info_Array, 1)
    LOOP
        RAISE DEBUG USING message := concat_ws('', 'Element- ',i,': ID=',V_student_info_Array[i].id,', Name=',V_student_info_Array[i].name,' is staying at ',V_student_info_Array[i].school_det.house_addr,',',V_student_info_Array[i].school_det.street,',',V_student_info_Array[i].school_det.city);
        i:=i+1;
    END LOOP;
    RAISE DEBUG 'Program executed successfully.';
END $$;

We get the following output:

DEBUG: Element- 1: ID=101010, Name=Peter is staying at F 30, ABC Apartments,XYZ Street,HYD CITY, DC
DEBUG: Element- 2: ID=202020, Name=Willard is staying at Lane 4, ABC Center,,XYZ City, Washington
DEBUG: Element- 3: ID=303030, Name=Andrew is staying at ,,

DEBUG: Program executed successfully.

Things to note:

  • When values are enclosed in parentheses, PostgreSQL understands them as a single unit and treats them as a record type and implicitly type casts to the proper type. In this case, the ROW constructor is optional.
  • The current version of PostgreSQL doesn’t allow multi-level reference even with the record type, which means v_rec.school_det.city := 'HYD CITY, DC' is not allowed. Therefore, the correct method is modifying the record type, for example v_rec.school_det := ROW('F 30, ABC Apartments', 'XYZ Street', 'HYD CITY, DC');.

Migrate a multi-dimensional collection

You can create a collection whose elements are also collections. For example, you can create a varray of varrays, a nested table of varrays, and so on. PostgreSQL also allows you to create arrays of arrays. This is referred as a multi-dimensional collection or multi-dimensional array.

Let’s take an example of a two-dimensional array, which is the simplest form of a multi-dimensional array. The following anonymous block declares a two-dimensional array CLASS_STUD_JOINDT_ARRAY, populates the arrays, and provides the array access and modification mechanism.

We use the following code in Oracle:

SET SERVEROUTPUT ON;

DECLARE
    TYPE STUDENT_JOINDT_ARRAY IS TABLE OF DATE INDEX BY PLS_INTEGER;
    TYPE CLASS_STUD_JOINDT_ARRAY IS TABLE OF STUDENT_JOINDT_ARRAY INDEX BY PLS_INTEGER;
    V_CLASS_STUD_JOINDT_ARRAY  CLASS_STUD_JOINDT_ARRAY;
    i INTEGER := 1;
BEGIN
    /* Populate the collection. */
    FOR i IN 1..5 LOOP
        FOR j IN 1..2 LOOP
            V_CLASS_STUD_JOINDT_ARRAY(i)(j) := sysdate-i-j;
        END LOOP;
    END LOOP;

    /* Accessing the collection type - Before Modify */
    FOR i IN V_CLASS_STUD_JOINDT_ARRAY.FIRST .. V_CLASS_STUD_JOINDT_ARRAY.LAST
    LOOP
        FOR j IN V_CLASS_STUD_JOINDT_ARRAY(i).FIRST .. V_CLASS_STUD_JOINDT_ARRAY(i).LAST
        LOOP
            DBMS_OUTPUT.PUT_LINE('Before modify- Element- ('||i||')('||j||'): ID='|| V_CLASS_STUD_JOINDT_ARRAY(i)(j));
        END LOOP;
    END LOOP;

    /* Modifying collection element values */
    V_CLASS_STUD_JOINDT_ARRAY(3)(2) := '22-JAN-2022';

    /* Accessing the collection type – After Modify */
    FOR i IN V_CLASS_STUD_JOINDT_ARRAY.FIRST .. V_CLASS_STUD_JOINDT_ARRAY.LAST
    LOOP
        FOR j IN V_CLASS_STUD_JOINDT_ARRAY(i).FIRST .. V_CLASS_STUD_JOINDT_ARRAY(i).LAST
        LOOP
            DBMS_OUTPUT.PUT_LINE('After modify- Element- ('||i||')('||j||'): ID='|| V_CLASS_STUD_JOINDT_ARRAY(i)(j));
        END LOOP;
    END LOOP;
    DBMS_OUTPUT.PUT_LINE('Program executed successfully.');
END;
/

We get the following output:

Before modify- Element- (1)(1): ID=17-10-23
Before modify- Element- (1)(2): ID=16-10-23
Before modify- Element- (2)(1): ID=16-10-23
Before modify- Element- (2)(2): ID=15-10-23
Before modify- Element- (3)(1): ID=15-10-23
Before modify- Element- (3)(2): ID=14-10-23
Before modify- Element- (4)(1): ID=14-10-23
Before modify- Element- (4)(2): ID=13-10-23
Before modify- Element- (5)(1): ID=13-10-23
Before modify- Element- (5)(2): ID=12-10-23
After modify- Element- (1)(1): ID=17-10-23
After modify- Element- (1)(2): ID=16-10-23
After modify- Element- (2)(1): ID=16-10-23
After modify- Element- (2)(2): ID=15-10-23
After modify- Element- (3)(1): ID=15-10-23
After modify- Element- (3)(2): ID=22-01-22
After modify- Element- (4)(1): ID=14-10-23
After modify- Element- (4)(2): ID=13-10-23
After modify- Element- (5)(1): ID=13-10-23
After modify- Element- (5)(2): ID=12-10-23
Program executed successfully.

PL/SQL procedure successfully completed.

To migrate the Oracle multi-dimensional collection code to PostgreSQL’s PL/pgSQL, we create equivalent arrays as domain types. PostgreSQL uses a similar loop structure, but the syntax for accessing and modifying arrays is slightly different.

We use the following code in PostgreSQL:

/* Create a DATE array */
CREATE DOMAIN STUNDENT_JOINDT_ARRAY AS DATE[];

/* Create 2D array */
CREATE DOMAIN CLASS_STUD_JOINDT_ARRAY AS STUNDENT_JOINDT_ARRAY[];

set client_min_messages = debug;

DO $$
DECLARE
    V_STUDENT_JOINDT_ARRAY STUNDENT_JOINDT_ARRAY;
    V_CLASS_STUD_JOINDT_ARRAY CLASS_STUD_JOINDT_ARRAY;
    i INTEGER := 1;
BEGIN
    /* Populate the collection */
    FOR I IN 1..5 LOOP
        FOR J IN 1..2 LOOP
            V_STUDENT_JOINDT_ARRAY[J] := CURRENT_DATE - I - J;
        END LOOP;
        V_CLASS_STUD_JOINDT_ARRAY[I] := V_STUDENT_JOINDT_ARRAY;
    END LOOP;

    /* Accessing the collection type - before modify */
    FOR I IN ARRAY_LOWER(V_CLASS_STUD_JOINDT_ARRAY, 1).. ARRAY_UPPER(V_CLASS_STUD_JOINDT_ARRAY, 1)
    LOOP
        FOR J IN ARRAY_LOWER(V_CLASS_STUD_JOINDT_ARRAY[I], 1) .. ARRAY_UPPER(V_CLASS_STUD_JOINDT_ARRAY[I], 1)
        LOOP
            RAISE DEBUG USING MESSAGE := CONCAT_WS('', 'Before modify- Element- (', I, ')(', J, '): ID=', (V_CLASS_STUD_JOINDT_ARRAY[I])[J]);
        END LOOP;
    END LOOP;

    /* Modifying collection element values */
    V_STUDENT_JOINDT_ARRAY := V_CLASS_STUD_JOINDT_ARRAY[3];
    V_STUDENT_JOINDT_ARRAY[2] := '2022-01-22';
    V_CLASS_STUD_JOINDT_ARRAY[3] := V_STUDENT_JOINDT_ARRAY;

    /* Accessing the collection type – after modify */
    FOR I IN ARRAY_LOWER(V_CLASS_STUD_JOINDT_ARRAY, 1).. ARRAY_UPPER(V_CLASS_STUD_JOINDT_ARRAY, 1)
    LOOP
        FOR J IN ARRAY_LOWER(V_CLASS_STUD_JOINDT_ARRAY[I], 1) .. ARRAY_UPPER(V_CLASS_STUD_JOINDT_ARRAY[I], 1)
        LOOP
            RAISE DEBUG USING MESSAGE := CONCAT_WS('', 'After modify- Element- (', I, ')(', J, '): ID=', (V_CLASS_STUD_JOINDT_ARRAY[I])[J]);
        END LOOP;
    END LOOP;
    RAISE DEBUG 'Program executed successfully.';
END $$;

We get the following output:

DEBUG: Before modify- Element- (1)(1): ID=2023-10-17
DEBUG: Before modify- Element- (1)(2): ID=2023-10-16
DEBUG: Before modify- Element- (2)(1): ID=2023-10-16
DEBUG: Before modify- Element- (2)(2): ID=2023-10-15
DEBUG: Before modify- Element- (3)(1): ID=2023-10-15
DEBUG: Before modify- Element- (3)(2): ID=2023-10-14
DEBUG: Before modify- Element- (4)(1): ID=2023-10-14
DEBUG: Before modify- Element- (4)(2): ID=2023-10-13
DEBUG: Before modify- Element- (5)(1): ID=2023-10-13
DEBUG: Before modify- Element- (5)(2): ID=2023-10-12
DEBUG: After modify- Element- (1)(1): ID=2023-10-17
DEBUG: After modify- Element- (1)(2): ID=2023-10-16
DEBUG: After modify- Element- (2)(1): ID=2023-10-16
DEBUG: After modify- Element- (2)(2): ID=2023-10-15
DEBUG: After modify- Element- (3)(1): ID=2023-10-15
DEBUG: After modify- Element- (3)(2): ID=2022-01-22
DEBUG: After modify- Element- (4)(1): ID=2023-10-14
DEBUG: After modify- Element- (4)(2): ID=2023-10-13
DEBUG: After modify- Element- (5)(1): ID=2023-10-13
DEBUG: After modify- Element- (5)(2): ID=2023-10-12

DEBUG: Program executed successfully.

Things to note:

  • You can fetch multi-dimensional element values in PostgreSQL similar to Oracle. For example, V_CLASS_STUD_JOINDT_ARRAY(i)(j) can be converted to V_CLASS_STUD_JOINDT_ARRAY[i][j] in PostgreSQL when fetching or accessing values.
  • However, you can’t assign values directly to elements of a multi-dimensional array in PostgreSQL as you do in Oracle. In PostgreSQL, you need another temporary record variable to hold values and assign back the array element.

The following SQL for multi-dimensional collection assignment works fine in Oracle PL/SQL:

V_CLASS_STUD_JOINDT_ARRAY(i)(j) := sysdate-i-j;

The following multi-dimensional array assignment statement throws an error in PL/pgSQL because it’s not possible to write to a multi-dimensional array element directly in PostgreSQL:

V_CLASS_STUD_JOINDT_ARRAY[i][j] := current_date-i-j; --Incorrect

In PostgreSQL, you have to break the multi-dimensional array to a single dimensional array and assign values:

--Correct
V_STUDENT_JOINDT_ARRAY := V_CLASS_STUD_JOINDT_ARRAY[i];
V_STUDENT_JOINDT_ARRAY[j] := current_date-i-j;
V_CLASS_STUD_JOINDT_ARRAY[i] := V_STUDENT_JOINDT_ARRAY;

Migrate a composite multi-dimensional collection

Multi-dimensional structures can also be extended when table/type columns become arrays. In this case, each record can have multiple values of each with different data types, which are grouped as a single unit and make the multi-dimensional elements.

Developers often come across such scenarios when migrating Oracle PL/SQL code. Operating with a correct reasonable data structure in PostgreSQL becomes troublesome because the syntax, Oracle functions, and operators don’t work similarly in PostgreSQL.

The following code is an extension of our previous example. The collection discussed in this example is multi-dimensional as well as composite consisting of more than one attribute.

We use the following code in Oracle:

SET SERVEROUTPUT ON;

DECLARE
    TYPE SCHOOL_INFO IS RECORD (
        ADDR_TYPE  VARCHAR2(100),
        ADDR_VAL   VARCHAR2(100),
        CITY       VARCHAR2(50)
    );

    TYPE SCHOOL_INFO_ARRAY IS TABLE OF SCHOOL_INFO INDEX BY BINARY_INTEGER;
    TYPE STUDENT_SCHOOL_ADDR IS RECORD(
        ID         NUMBER,
        NAME       VARCHAR2(50),
        SCHOOL_DET SCHOOL_INFO_ARRAY
    );

    TYPE STUDENT_SCHOOL_ADDR_ARRAY IS TABLE OF STUDENT_SCHOOL_ADDR INDEX BY BINARY_INTEGER;
    V_STUDENT_SCHOOL_ADDR_ARRAY STUDENT_SCHOOL_ADDR_ARRAY;
BEGIN
    /* Populate the collection */
    V_STUDENT_SCHOOL_ADDR_ARRAY(1).ID := 10101;
    V_STUDENT_SCHOOL_ADDR_ARRAY(1).NAME := 'WILLIAMS';
    V_STUDENT_SCHOOL_ADDR_ARRAY(1).SCHOOL_DET(1).ADDR_TYPE := 'PERMANENT';
    V_STUDENT_SCHOOL_ADDR_ARRAY(1).SCHOOL_DET(1).ADDR_VAL := '3044 ABC LANE';
    V_STUDENT_SCHOOL_ADDR_ARRAY(1).SCHOOL_DET(1).CITY := 'NEW CITY';
    V_STUDENT_SCHOOL_ADDR_ARRAY(1).SCHOOL_DET(2).ADDR_TYPE := 'CORRESPONDENCE';
    V_STUDENT_SCHOOL_ADDR_ARRAY(1).SCHOOL_DET(2).ADDR_VAL := '4390 XYZ LANE';
    V_STUDENT_SCHOOL_ADDR_ARRAY(1).SCHOOL_DET(2).CITY := 'LOS ANGELES';
    V_STUDENT_SCHOOL_ADDR_ARRAY(1).SCHOOL_DET(3).ADDR_TYPE := 'OFFICE';
    V_STUDENT_SCHOOL_ADDR_ARRAY(1).SCHOOL_DET(3).ADDR_VAL := '2970 SEH ROAD';
    V_STUDENT_SCHOOL_ADDR_ARRAY(1).SCHOOL_DET(3).CITY := 'ILLINOIS';
    V_STUDENT_SCHOOL_ADDR_ARRAY(2).ID := 20202;
V_STUDENT_SCHOOL_ADDR_ARRAY(2).NAME := 'JACKSON';

    /* Accessing the collection type - before modify */
    FOR J IN 1..3 LOOP
        DBMS_OUTPUT.PUT_LINE('ELEMENT- '|| V_STUDENT_SCHOOL_ADDR_ARRAY(1).ID|| ', NAME:'|| V_STUDENT_SCHOOL_ADDR_ARRAY(1).NAME||', COMPLETE ADDRESS: '|| V_STUDENT_SCHOOL_ADDR_ARRAY(1).SCHOOL_DET(J).ADDR_TYPE||'- '|| V_STUDENT_SCHOOL_ADDR_ARRAY(1).SCHOOL_DET(J).ADDR_VAL||', '|| V_STUDENT_SCHOOL_ADDR_ARRAY(1).SCHOOL_DET(J).CITY);
    END LOOP;
    DBMS_OUTPUT.PUT_LINE('PROGRAM EXECUTED SUCCESSFULLY.');
END;
/

We get the following output:

ELEMENT- 10101, NAME:WILLIAMS, COMPLETE ADDRESS: PERMANENT- 3044 ABC LANE, NEW CITY
ELEMENT- 10101, NAME:WILLIAMS, COMPLETE ADDRESS: CORRESPONDENCE- 4390 XYZ LANE, LOS ANGELES
ELEMENT- 10101, NAME:WILLIAMS, COMPLETE ADDRESS: OFFICE- 2970 SEH ROAD, ILLINOIS
PROGRAM EXECUTED SUCCESSFULLY.

PL/SQL procedure successfully completed.

The following code block in PostgreSQL creates the types and domains required to implement the multi-dimensional composite array structure:

CREATE TYPE SCHOOL_INFO AS (
    ADDR_TYPE VARCHAR(100),
    ADDR_VAL  VARCHAR(100),
    CITY      VARCHAR(50)
);

CREATE DOMAIN SCHOOL_INFO_ARRAY AS SCHOOL_INFO[];

CREATE TYPE STUDENT_SCHOOL_ADDR AS(
    ID         INTEGER,
    NAME       VARCHAR(50),
    SCHOOL_DET SCHOOL_INFO_ARRAY
);

CREATE DOMAIN STUDENT_SCHOOL_ADDR_ARRAY AS STUDENT_SCHOOL_ADDR[];

The following code block is equivalent to an Oracle multi-dimensional composite collection and describes how to operate on a multi-dimensional composite array in a PostgreSQL database:

set client_min_messages = debug;

DO $$
DECLARE
    V_STUDENT_SCHOOL_ADDR STUDENT_SCHOOL_ADDR;
    V_STUDENT_SCHOOL_ADDR_ARRAY STUDENT_SCHOOL_ADDR_ARRAY;
    V_STUDENT_ADDR_ARRAY SCHOOL_INFO_ARRAY;
    V_STUDENT_ADDR SCHOOL_INFO;
BEGIN
    /* Populate the collection */
    V_STUDENT_SCHOOL_ADDR := V_STUDENT_SCHOOL_ADDR_ARRAY[1];
    V_STUDENT_SCHOOL_ADDR.ID := 10101;
    V_STUDENT_SCHOOL_ADDR.NAME := 'WILLIAMS';
    V_STUDENT_SCHOOL_ADDR.SCHOOL_DET[1] := ROW('PERMANENT', '3044 ABC LANE', 'NEW CITY');
    V_STUDENT_SCHOOL_ADDR.SCHOOL_DET[2] := ROW('CORRESPONDENCE', '4390 XYZ LANE', 'LOS ANGELES');
    V_STUDENT_SCHOOL_ADDR.SCHOOL_DET[3] := ROW(' OFFICE', '2970 SEH ROAD', 'ILLINOIS');
    V_STUDENT_SCHOOL_ADDR_ARRAY[1] := V_STUDENT_SCHOOL_ADDR;
    V_STUDENT_SCHOOL_ADDR := V_STUDENT_SCHOOL_ADDR_ARRAY[2];
    V_STUDENT_SCHOOL_ADDR.ID := 20202;
    V_STUDENT_SCHOOL_ADDR.NAME := 'JACKSON';
    V_STUDENT_SCHOOL_ADDR_ARRAY[2] := V_STUDENT_SCHOOL_ADDR;

    /* Accessing the collection type */
    FOR J IN 1..3 LOOP
        RAISE DEBUG USING MESSAGE := CONCAT_WS('', 'ELEMENT- ', V_STUDENT_SCHOOL_ADDR_ARRAY[1].ID, ', NAME:', V_STUDENT_SCHOOL_ADDR_ARRAY[1].NAME, ', COMPLETE ADDRESS: ', V_STUDENT_SCHOOL_ADDR_ARRAY[1].SCHOOL_DET[J].ADDR_TYPE, '- ', V_STUDENT_SCHOOL_ADDR_ARRAY[1].SCHOOL_DET[J].ADDR_VAL, ', ', V_STUDENT_SCHOOL_ADDR_ARRAY[1].SCHOOL_DET[J].CITY);
    END LOOP;
    RAISE DEBUG 'PROGRAM EXECUTED SUCCESSFULLY.';
END $$;

We get the following output:

DEBUG: ELEMENT- 10101, NAME:WILLIAMS, COMPLETE ADDRESS: PERMANENT- 3044 ABC LANE, NEW CITY
DEBUG: ELEMENT- 10101, NAME:WILLIAMS, COMPLETE ADDRESS: CORRESPONDENCE- 4390 XYZ LANE, LOS ANGELES
DEBUG: ELEMENT- 10101, NAME:WILLIAMS, COMPLETE ADDRESS: OFFICE- 2970 SEH ROAD, ILLINOIS
DEBUG: PROGRAM EXECUTED SUCCESSFULLY.

Clean up

When you’re finished experimenting with the scenarios described in this post, you can clean up your PostgreSQL database by using following code to delete all the objects created in this example.

DROP SCHEMA stud_marks CASCADE;
DROP SCHEMA stud_grades CASCADE;
DROP SCHEMA stud_info CASCADE;
DROP DOMAIN CLASS_STUD_JOINDT_ARRAY;
DROP DOMAIN STUNDENT_JOINDT_ARRAY;
DROP DOMAIN STUDENT_SCHOOL_ADDR_ARRAY;
DROP TYPE STUDENT_SCHOOL_ADDR;
DROP DOMAIN SCHOOL_INFO_ARRAY;
DROP TYPE SCHOOL_INFO;
DROP TABLE Student_Marks;

Conclusion

In this post, we showed how to use PostgreSQL-native features to efficiently migrate Oracle simple collections, composite collections, and multi-dimensional collections to PostgreSQL. It’s important to note that the specifics of the migration process will depend on the complexity of your Oracle collections, the size of your dataset, and the intricacies of your application. We shared instructions for migrating Oracle collections to Amazon Aurora PostgreSQL or Amazon RDS for PostgreSQL and also provided code examples, SQL scripts, and best practices that you can use while migrating Oracle collections.

If you have any feedback, comments, or questions, share them in the comments section.


About the Authors

Sashikanta Pattanayak works as a Lead Consultant with the Professional Services team at AWS, specializing in database modernization, optimization, and migration strategies. His role involves collaborating with clients to architect scalable, resilient, and secure solutions within the AWS cloud environment. He specializes in homogeneous and heterogeneous database migrations.

Vinay Paladi is a Lead Database Consultant with the Professional Services team at Amazon Web Services. He works as a database migration specialist, helping and enabling customers to build highly available, cost-effective database solutions and migrate their commercial engines to the AWS Cloud. He is passionate about building innovative solutions to accelerate the database journey to cloud.

Anuradha Chintha is a Lead Consultant with Amazon Web Services. She works with customers to build scalable, highly available, and secure solutions in the AWS Cloud. Her focus area is homogeneous and heterogeneous database migrations.