Pages

OracleEBSpro is purely for knowledge sharing and learning purpose, with the main focus on Oracle E-Business Suite Product and other related Oracle Technologies.

I'm NOT responsible for any damages in whatever form caused by the usage of the content of this blog.

I share my Oracle knowledge through this blog. All my posts in this blog are based on my experience, reading oracle websites, books, forums and other blogs. I invite people to read and suggest ways to improve this blog.


Showing posts with label 10g. Show all posts
Showing posts with label 10g. Show all posts

Wednesday, March 6, 2013

DML error logging in oracle 10g release 2



This article introduces DML error logging; a major new feature of Oracle 10g Release 2 for bulk SQL operations. DML error logging enables us to trap "bad data" and filter it to a log table without failing our overall DML statement. This has never been possible in SQL before, although we could use complex constraint management and application code to achieve a slightly similar end-result. DML error logging is more similar in concept to the FORALL SAVE EXCEPTIONS construct in PL/SQL (new in Oracle 9i).

overview of dml error logging

With this feature, we can add a clause to our bulk DML statements (INSERT, UPDATE, MERGE and DELETE) to prevent the statement failing on hitting exceptions (i.e. "bad data"). Exceptional rows are added to a specifically-created errors table for investigation and/or intervention. In addition, we can control the number of bad records we will tolerate before failing the entire statement.
There are two components to DML error logging as follows:
  • LOG ERRORS clause to DML statements; and
  • DBMS_ERRLOG package for managing error tables.
We shall examine both of these components in this article, but first we will create some sample tables.

getting started: sample data

We will use two tables in our DML error logging examples, as follows. Note that for the examples, I created a user named EL with just CREATE SESSION, CREATE TABLE and a tablespace quota.
SQL> CREATE TABLE src (x,y,z)
  2  AS
  3     SELECT object_id
  4     ,      object_type
  5     ,      object_name
  6     FROM   all_objects
  7     WHERE  ROWNUM <= 5;

Table created.

SQL> CREATE TABLE tgt
  2  AS
  3     SELECT *
  4     FROM   src
  5     WHERE  ROWNUM <= 3;

Table created.

SQL> ALTER TABLE tgt ADD
  2     CONSTRAINT pk_tgt
  3     PRIMARY KEY (x);

Table altered.
We have a source table (SRC) and a target table (TGT). The data is setup in such a way that a standard INSERT..SELECT from SRC into TGT will fail, as follows.
SQL> INSERT INTO tgt SELECT * FROM src;
INSERT INTO tgt SELECT * FROM src
*
ERROR at line 1:
ORA-00001: unique constraint (EL.PK_TGT) violated
On this basis, we can now introduce the new DML error logging feature. To begin, we will require an exceptions table.

creating the error log table

DML error logging works on the principle of trapping exceptions in bulk SQL statements and re-directing the "bad data" to an error table. The error table is created using an API in the new DBMS_ERRLOG package. The minimum amount of information we need to supply to this is the name of the target table we wish to trap exceptions for. Oracle will by default create an error table named "ERR$_SUBSTR(our_table_name,1,25)". If we so choose, we can optionally control the name, owner and tablespace of the error log table by supplying the relevant parameters.
Given this, we will now create an error log table for TGT and provide a friendly name of our own.
SQL> BEGIN
  2     DBMS_ERRLOG.CREATE_ERROR_LOG(
  3        dml_table_name      => 'TGT',        --<-- required
  4        err_log_table_name  => 'TGT_ERRORS'  --<-- optional
  5        );
  6  END;
  7  /

PL/SQL procedure successfully completed.

SQL> SELECT table_name FROM user_tables;

TABLE_NAME
------------------------------
TGT
SRC
TGT_ERRORS
The error log table has a number of metadata columns (describing the nature of the exceptional data) and also a VARCHAR2 representation of the base-table columns themselves. The VARCHAR2 columns enable us to see erroneous data that perhaps did not satisfy its base-table datatype. Needless to say, there is a limitation on the datatypes that can be converted to VARCHAR2. For example, ANYDATA, user-defined types and LOBs cannot be captured in the error log table.
The structure of the TGT_ERRORS table as follows.
SQL> desc tgt_errors;
 Name                                      Null?    Type
 ----------------------------------------- -------- ----------------------------
 ORA_ERR_NUMBER$                                    NUMBER
 ORA_ERR_MESG$                                      VARCHAR2(2000)
 ORA_ERR_ROWID$                                     ROWID
 ORA_ERR_OPTYP$                                     VARCHAR2(2)
 ORA_ERR_TAG$                                       VARCHAR2(2000)
 X                                                  VARCHAR2(4000)
 Y                                                  VARCHAR2(4000)
 Z                                                  VARCHAR2(4000)

invoking dml error logging

Now we have some sample data and an error log table in place, we are ready to see DML error logging in action. We'll begin by replaying the failed INSERT..SELECT from earlier and then describe the new syntax elements.
SQL> INSERT INTO tgt
  2  SELECT * FROM src
  3  LOG ERRORS INTO tgt_errors ('INSERT..SELECT..RL=UNLIMITED')
  4  REJECT LIMIT UNLIMITED;

2 rows created.
Skipping over the fact that our DML statament succeeded for a moment, this is new and extended syntax we haven't seen before. In particular, note the following.
  • Line 3: the LOG ERRORS clause is how we invoke DML error logging. We are telling Oracle that we wish our DML statement to succeed in the event that we encounter exceptions below a specified threshold;
  • Line 3: the INTO tgt_errors extension to the LOG ERRORS clause is only necessary when using a non-default error log table name, as we are in this article;
  • Line 3: the optional literal in brackets enables us to "tag" any bad data that is written to our error table. This helps us to differentiate exceptional data over time. Note this tag can (and should) be a bind variable in "real" applications;
  • Line 4: users of external tables will recognise the REJECT LIMIT clause. This is how we specify our threshold for errors (i.e. the number of exceptions we will allow before Oracle fails the entire DML statement).

error log data

Re-visiting our example, therefore, we can see that with DML error logging our insert succeeded but only for 2 rows. We know this is fewer than the number of records in our staging table, so we should check the error log table, as follows (using Tom Kyte's print_table procedure for convenience). Note in particular our tags, which can help us find the bad data quickly on a busy system and also the error message assigned to each failed row (we just happen to have the same exception for each due to the setup of the sample data). In addition, we can see the actual data that we were trying to insert.
SQL> exec print_table( 'SELECT * FROM tgt_errors' );
-----------------
ORA_ERR_NUMBER$               : 1
ORA_ERR_MESG$                 : ORA-00001: unique constraint (EL.PK_TGT) violated
ORA_ERR_ROWID$                :
ORA_ERR_OPTYP$                : I
ORA_ERR_TAG$                  : INSERT..SELECT..RL=UNLIMITED
X                             : 258
Y                             : TABLE
Z                             : DUAL
-----------------
ORA_ERR_NUMBER$               : 1
ORA_ERR_MESG$                 : ORA-00001: unique constraint (EL.PK_TGT) violated
ORA_ERR_ROWID$                :
ORA_ERR_OPTYP$                : I
ORA_ERR_TAG$                  : INSERT..SELECT..RL=UNLIMITED
X                             : 259
Y                             : SYNONYM
Z                             : DUAL
-----------------
ORA_ERR_NUMBER$               : 1
ORA_ERR_MESG$                 : ORA-00001: unique constraint (EL.PK_TGT) violated
ORA_ERR_ROWID$                :
ORA_ERR_OPTYP$                : I
ORA_ERR_TAG$                  : INSERT..SELECT..RL=UNLIMITED
X                             : 311
Y                             : TABLE
Z                             : SYSTEM_PRIVILEGE_MAP

PL/SQL procedure successfully completed.
The logged data is not part of the same transaction, which we can demonstrate with a simple rollback. We can see that the error log data is still in the log table. On repeated re-runs and failures, therefore, it will be necessary to tag each statement in such a way as to make then easily identifiable. The tags we've used so far in this article would obviously be useless under such a scenario.
Note that there is a ROWID column in the logging table. This is used when an UPDATE (or update part of a MERGE) fails and provides the ROWID of the target row that was being updated. As we saw with the INSERT example, the "bad data" that caused the exception is recorded in the logging table.
SQL> ROLLBACK;

Rollback complete.

SQL> SELECT COUNT(*) FROM tgt;

  COUNT(*)
----------
         3

SQL> exec print_table( 'SELECT * FROM tgt_errors' );
-----------------
ORA_ERR_NUMBER$               : 1
ORA_ERR_MESG$                 : ORA-00001: unique constraint (EL.PK_TGT) violated
ORA_ERR_ROWID$                :
ORA_ERR_OPTYP$                : I
ORA_ERR_TAG$                  : INSERT..SELECT..RL=UNLIMITED
X                             : 258
Y                             : TABLE
Z                             : DUAL
-----------------
ORA_ERR_NUMBER$               : 1
ORA_ERR_MESG$                 : ORA-00001: unique constraint (EL.PK_TGT) violated
ORA_ERR_ROWID$                :
ORA_ERR_OPTYP$                : I
ORA_ERR_TAG$                  : INSERT..SELECT..RL=UNLIMITED
X                             : 259
Y                             : SYNONYM
Z                             : DUAL
-----------------
ORA_ERR_NUMBER$               : 1
ORA_ERR_MESG$                 : ORA-00001: unique constraint (EL.PK_TGT) violated
ORA_ERR_ROWID$                :
ORA_ERR_OPTYP$                : I
ORA_ERR_TAG$                  : INSERT..SELECT..RL=UNLIMITED
X                             : 311
Y                             : TABLE
Z                             : SYSTEM_PRIVILEGE_MAP

PL/SQL procedure successfully completed.

reject limit

The default reject limit is 0 (i.e. if this part of the LOG ERRORS clause is omitted). In our first DML error logging example, we used an unlimited reject limit. With this option, a DML statement will succeed even if none of its target operations succeed (i.e. all data is "bad"). If we set an explicit reject limit and exceed it, the entire statement fails but n+1 errors are still logged (where n is the reject limit). We can see this as follows by setting a reject limit of 1. Note that we have changed our tag accordingly to assist with the lookup against the error log.
SQL> INSERT INTO tgt
  2  SELECT * FROM src
  3  LOG ERRORS INTO tgt_errors ('INSERT..SELECT..RL=1')
  4  REJECT LIMIT 1;
INSERT INTO tgt
*
ERROR at line 1:
ORA-00001: unique constraint (EL.PK_TGT) violated

SQL> exec print_table( 'SELECT * FROM tgt_errors WHERE ora_err_tag$ LIKE ''%RL=1%''' );
-----------------
ORA_ERR_NUMBER$               : 1
ORA_ERR_MESG$                 : ORA-00001: unique constraint (EL.PK_TGT) violated
ORA_ERR_ROWID$                :
ORA_ERR_OPTYP$                : I
ORA_ERR_TAG$                  : INSERT..SELECT..RL=1
X                             : 258
Y                             : TABLE
Z                             : DUAL
-----------------
ORA_ERR_NUMBER$               : 1
ORA_ERR_MESG$                 : ORA-00001: unique constraint (EL.PK_TGT) violated
ORA_ERR_ROWID$                :
ORA_ERR_OPTYP$                : I
ORA_ERR_TAG$                  : INSERT..SELECT..RL=1
X                             : 259
Y                             : SYNONYM
Z                             : DUAL

PL/SQL procedure successfully completed.

restrictions

Error logging supports all DML operations, including INSERT FIRST|ALL and MERGE. There are some restrictions, however, according to the documentation, that will cause the DML to fail and not invoke error logging at all. These are:
  • violated deferred constraints;
  • any direct-path INSERT or MERGE operation that raises a unique constraint or index violation; or
  • any update operation (UPDATE or MERGE) that raises a unique constraint or index violation.
The second and third of these restrictions are slightly baffling. The second because often in batch environments we are likely to want to combine error logging with direct path loading. The third because it seems to be a pretty standard error in some environments with natural keys (despite all best practice rules about updating PK/UK columns).
We can demonstrate the second restriction quite easily as follows. Note that DML error logging is not invoked at all, despite us adding the LOG ERRORS clause with an unlimited reject limit.
SQL> INSERT /*+ APPEND */ INTO tgt
  2  SELECT * FROM src
  3  LOG ERRORS INTO tgt_errors ('INSERT..SELECT..DIRECT..ORA-00001')
  4  REJECT LIMIT UNLIMITED;
INSERT /*+ APPEND */ INTO tgt
*
ERROR at line 1:
ORA-00001: unique constraint (EL.PK_TGT) violated

SQL> exec print_table( 'SELECT * FROM tgt_errors WHERE ora_err_tag$ LIKE ''001%''' );

PL/SQL procedure successfully completed.
Continuing with the same direct-path restriction, we'll remove the primary key and force a different error to show that it will log exceptions other than unique violations. We will try to add too many characters to our Z columns for just one of the rows.
SQL> ALTER TABLE tgt DROP PRIMARY KEY;

Table altered.

SQL> INSERT /*+ APPEND */ INTO tgt
  2  SELECT x
  3  ,      y
  4  ,      DECODE(ROWNUM,1,RPAD(z,31,'@'),z)  --<-- 31 characters for row 1
  5  FROM   src
  6  LOG ERRORS INTO tgt_errors ('INSERT..SELECT..DIRECT..ORA-12899')
  7  REJECT LIMIT UNLIMITED;

4 rows created.

SQL> exec print_table( 'SELECT * FROM tgt_errors WHERE ora_err_tag$ LIKE ''%12899''' );
-----------------
ORA_ERR_NUMBER$               : 12899
ORA_ERR_MESG$                 : ORA-12899: value too large for column "EL"."TGT"."Z" (actual: 31, maximum: 30)
ORA_ERR_ROWID$                :
ORA_ERR_OPTYP$                : I
ORA_ERR_TAG$                  : INSERT..SELECT..DIRECT..ORA-12899
X                             : 258
Y                             : TABLE
Z                             : DUAL@@@@@@@@@@@@@@@@@@@@@@@@@@@

PL/SQL procedure successfully completed.

dml error logging in pl/sql

We can see that DML error logging is fully supported in PL/SQL. The SQL%ROWCOUNT attribute will report the successful rowcount only. Unfortunately, there doesn't appear to be an attribute or exception to indicate that errors were logged, so the only option is to examine the error log table itself. In the following example, we will reset our sample data and table and embed our SQL inside a PL/SQL block. We will also use a bind variable for the logging tag.
SQL> ROLLBACK;

Rollback complete.

SQL> ALTER TABLE tgt ADD
  2     CONSTRAINT pk_tgt
  3     PRIMARY KEY (x);

Table altered.

SQL> DECLARE
  2  
  3     v_unique_tag VARCHAR2(64) := 'INSERT..SELECT..PL/SQL';
  4  
  5  BEGIN
  6  
  7     INSERT INTO tgt
  8     SELECT * FROM src
  9     LOG ERRORS INTO tgt_errors (v_unique_tag)
 10     REJECT LIMIT 10;
 11  
 12     DBMS_OUTPUT.PUT_LINE( SQL%ROWCOUNT || ' rows successfully inserted.' );
 13  
 14     FOR r IN ( SELECT RTRIM(ora_err_mesg$,CHR(10)) AS err
 15                FROM   tgt_errors
 16                WHERE  ora_err_tag$ = v_unique_tag )
 17     LOOP
 18        DBMS_OUTPUT.PUT_LINE( r.err );
 19     END LOOP;
 20  
 21  END;
 22  /
2 rows successfully inserted.
ORA-00001: unique constraint (EL.PK_TGT) violated
ORA-00001: unique constraint (EL.PK_TGT) violated
ORA-00001: unique constraint (EL.PK_TGT) violated

PL/SQL procedure successfully completed.

dropping the error log table

To remove the error log table, we have to manually drop it. Unusually, the DBMS_ERRLOG package does not supply an API for this, but as it is simply a table without any other objects attached to it, we can simply drop it ourselves.
SQL> DROP TABLE tgt PURGE;

Table dropped.

SQL> SELECT table_name FROM user_tables;

TABLE_NAME
------------------------------
SRC
TGT_ERRORS

SQL> DROP TABLE tgt_errors PURGE;

Table dropped.

further reading

To read more on the DML error logging clause, including more information on its restrictions, see the Administrator's Guide. To see the performance characteristics of DML error logging and a comparison with the FORALL SAVE EXCEPTIONS clause, read this oracle-developer.net article. The DBMS_ERRLOG package overview can be found in the Supplied Packages and Types Reference.

Credit goes to the below website(s):

the collect function in 10g


Oracle 10g has introduced an extremely useful new group function, COLLECT. This function enables us to aggregate data into a collection, retaining multiple records of data within a single row (like a nested table). One of the main benefits of this function is that it makes "string aggregation" (one of the web's most-requested Oracle technique) very simple. This article will introduce the COLLECT function and then demonstrate how it can be used to aggregate multiple records into a single value (a technique known as "string aggregation").

an overview of the collect function

We'll start by demonstrating the COLLECT function. We'll run a simple query against the ubiquitous EMP table to collect the names of all employees by department.
SQL> SELECT deptno
  2  ,      COLLECT(ename) AS emps
  3  FROM   emp
  4  GROUP  BY
  5         deptno;

    DEPTNO EMPS
---------- ------------------------------------------------------------------------------------
        10 SYSTPXeCjDqbWSqWrshgYrRPR4Q==('CLARK', 'KING')
        20 SYSTPXeCjDqbWSqWrshgYrRPR4Q==('SMITH', 'JONES', 'SCOTT', 'ADAMS', 'FORD')
        30 SYSTPXeCjDqbWSqWrshgYrRPR4Q==('ALLEN', 'WARD', 'MARTIN', 'BLAKE', 'TURNER', 'JAMES')
        40 SYSTPXeCjDqbWSqWrshgYrRPR4Q==('MILLER')

4 rows selected.
Something looks a little unusual here, but ignoring the strange identifier for a moment, we can see that the COLLECT function has aggregated the employee names per department as requested.

system-generated types

Moving on to the strange identifier in the example output, we can see that Oracle has created a collection type to support the COLLECT function. The behaviour is different between 10g releases 1 and 2, so we'll investigate each separately.

10g release 1

For our example EMP query above (executed in a 10.1 database), Oracle has created a supporting type named "SYSTPXeCjDqbWSqWrshgYrRPR4Q==". We can find this in the dictionary as follows.
SQL> SELECT owner
  2  ,      typecode
  3  FROM   all_types
  4  WHERE  type_name = 'SYSTPXeCjDqbWSqWrshgYrRPR4Q==';

OWNER                          TYPECODE
------------------------------ ------------------------------
SYS                            COLLECTION

1 row selected.
It appears as though Oracle generates a supporting collection type every time it hard-parses a SQL statement that uses the COLLECT function. In Oracle 10.1, the type is created in the SYS schema. If we try to use this new type as follows, we'll find that we cannot.
SQL> SELECT *
  2  FROM   TABLE(
  3            sys."SYSTPXeCjDqbWSqWrshgYrRPR4Q=="('A','B','C') );
          sys."SYSTPXeCjDqbWSqWrshgYrRPR4Q=="('A','B','C') )
          *
ERROR at line 3:
ORA-00904: "SYS"."SYSTPXeCjDqbWSqWrshgYrRPR4Q==": invalid identifier
In addition to the burden this must place on the parsing process, we might also be concerned about the number of system-generated types that might start appearing in our database (for reasons unknown, some DBAs and developers are worried about this sort of thing). If we flush the shared pool or even bounce the database, the type persists, even though it no longer supports a cached SQL statement. Oracle Support's official line on this (which used to be "bounce the database to remove the type") is that SMON cleans up unused types "after a period". Recent experience suggests that this period can be anything up to 24 hours after the bounce, so in an online database with a large shared pool, these types could stick around for some considerable period. Whether we choose to worry about this or not is another matter entirely!

10g release 2

In Oracle 10g Release 2, the type is created in the schema that parses the SQL statement, as we can see in the following example (we must first repeat the original EMP query to generate a type).
SQL> SELECT deptno
  2  ,      COLLECT(ename) AS emps
  3  FROM   emp
  4  GROUP  BY
  5         deptno;

    DEPTNO EMPS
---------- ------------------------------------------------------------------------------------
        10 SYSTPo3itZvoiRAyeH+f5LKv6+Q==('CLARK', 'KING')
        20 SYSTPo3itZvoiRAyeH+f5LKv6+Q==('SMITH', 'JONES', 'SCOTT', 'ADAMS', 'FORD')
        30 SYSTPo3itZvoiRAyeH+f5LKv6+Q==('ALLEN', 'WARD', 'MARTIN', 'BLAKE', 'TURNER', 'JAMES')
        40 SYSTPo3itZvoiRAyeH+f5LKv6+Q==('MILLER')

4 rows selected.

SQL> SELECT owner
  2  ,      typecode
  3  FROM   all_types
  4  WHERE  type_name = 'SYSTPo3itZvoiRAyeH+f5LKv6+Q==';

OWNER                          TYPECODE
------------------------------ ------------------------------
SCOTT                          COLLECTION

1 row selected.
The fact that the parsing schema owns the collection type in Oracle 10.2 means that we can use these types if we wish, as follows.
SQL> SELECT *
  2  FROM   TABLE(
  3            "SYSTPo3itZvoiRAyeH+f5LKv6+Q=="('A','B','C') );

COLUMN_VAL
----------
A
B
C

3 rows selected.
Furthermore, removing this system-generated type is much more simple in 10.2 than in 10.1. Firstly, because we own the type, we can simply drop it as follows (assuming that the SQL statement that generated it is no longer required).
SQL> DROP TYPE "SYSTPo3itZvoiRAyeH+f5LKv6+Q==";

Type dropped.
Bear in mind, however, that this type is supporting a SQL cursor. Therefore, if we do decide to drop the system-generated type as above, the underlying SQL cursor will be removed from the shared pool. Therefore, a re-run of the original SQL statement will need to be hard-parsed and a new type will be created accordingly.
Alternatively, a bounce of the database will drop the type immediately (rather than at some point during the next 24 hours, which is the 10.1 behaviour). Despite this, we are probably best to leave the SQL cursor to age out of the shared pool naturally, leaving SMON to clean up at a later stage.

using our own collection types

It is possible to use our own collection types with COLLECT. The CAST function (available at least as far back as Oracle 8.0 and possibly further) can be used to turn the results of the COLLECT into a type of our choosing. Note that this doesn't stop Oracle creating system-generated types to support the SQL statement, but it does make the results easier to work with.
In the following example, we'll create a standard VARCHAR2 collection type and CAST the results of our collected employee names.
SQL> CREATE OR REPLACE TYPE varchar2_ntt AS TABLE OF VARCHAR2(4000);
  2  /

Type created.
SQL> SELECT deptno
  2  ,      CAST(COLLECT(ename) AS varchar2_ntt) AS emps
  3  FROM   emp
  4  GROUP  BY
  5         deptno;

    DEPTNO EMPS
---------- ---------------------------------------------------------------------
        10 VARCHAR2_NTT('CLARK', 'KING')
        20 VARCHAR2_NTT('SMITH', 'JONES', 'SCOTT', 'ADAMS', 'FORD')
        30 VARCHAR2_NTT('ALLEN', 'WARD', 'MARTIN', 'BLAKE', 'TURNER', 'JAMES')
        40 VARCHAR2_NTT('MILLER')

4 rows selected.
Note that if you are casting collections of numbers, Oracle can be particularly fussy about precisions and scales, as the following example demonstrates. We'll create a general collection of number and then attempt to cast a collection of employee salaries (the EMP.SAL column is defined as NUMBER(7,2)).
SQL> CREATE OR REPLACE TYPE number_ntt AS TABLE OF NUMBER;
  2  /

Type created.
SQL> SELECT deptno
  2  ,      CAST(COLLECT(sal) AS number_ntt) AS sals
  3  FROM   emp
  4  GROUP  BY
  5         deptno;
,      CAST(COLLECT(sal) AS number_ntt) AS sals
            *
ERROR at line 2:
ORA-22814: attribute or element value is larger than specified in type
This is rather a confusing problem, as the unconstrained NUMBER type should easily incorporate a NUMBER(7,2). To wrap this up, however, there are two simple solutions. We can either make the collected column fit the type or the type fit the column, as shown below. First we'll make the column fit the type.
SQL> SELECT deptno
  2  ,      CAST(COLLECT(CAST(sal AS NUMBER)) AS number_ntt) AS sals
  3  FROM   emp
  4  GROUP  BY
  5         deptno;

    DEPTNO SALS
---------- -------------------------------------------------------------------------
        10 NUMBER_NTT(2450, 5000)
        20 NUMBER_NTT(800, 2975, 3000, 1100, 3000)
        30 NUMBER_NTT(1600, 1250, 1250, 2850, 1500, 950)
        40 NUMBER_NTT(1300)

4 rows selected.
Secondly we'll make the type fit the column.
SQL> CREATE TYPE number_7_2_ntt AS TABLE OF NUMBER(7,2);
  2  /

Type created.

SQL> SELECT deptno
  2  ,      CAST(COLLECT(sal) AS number_7_2_ntt) AS sals
  3  FROM   emp
  4  GROUP  BY
  5         deptno;

    DEPTNO SALS
---------- -------------------------------------------------------------------------
        10 NUMBER_7_2_NTT(2450, 5000)
        20 NUMBER_7_2_NTT(800, 2975, 3000, 1100, 3000)
        30 NUMBER_7_2_NTT(1600, 1250, 1250, 2850, 1500, 950)
        40 NUMBER_7_2_NTT(1300)

4 rows selected.

string aggregation using collect

We'll now see a practical demonstration of what the COLLECT function can be used for. One of the FAQs of Oracle developer forums is how to aggregate multiple strings into a single value. From releases of Oracle 8.0 onwards, there have been numerous methods for doing this. The most well-known method undoubtedly utilises Tom Kyte's "STRAGG" user-defined aggregate function. The STRAGG function is popular because it is extremely easy to use and faster than any pre-9i method. The COLLECT function, when combined with a function to turn the elements of a collection into a string, is faster still. We'll look at this below.
We'll continue with our standard VARCHAR2_NTT collection type, but we'll also require a "collection-to-string" function as follows.
SQL> CREATE FUNCTION to_string (
  2                  nt_in        IN varchar2_ntt,
  3                  delimiter_in IN VARCHAR2 DEFAULT ','
  4                  ) RETURN VARCHAR2 IS
  5
  6     v_idx PLS_INTEGER;
  7     v_str VARCHAR2(32767);
  8     v_dlm VARCHAR2(10);
  9
 10  BEGIN
 11
 12     v_idx := nt_in.FIRST;
 13     WHILE v_idx IS NOT NULL LOOP
 14        v_str := v_str || v_dlm || nt_in(v_idx);
 15        v_dlm := delimiter_in;
 16        v_idx := nt_in.NEXT(v_idx);
 17     END LOOP;
 18
 19     RETURN v_str;
 20
 21  END to_string;
 22  /

Function created.
Now we are ready to demonstrate string aggregation using the COLLECT function. We'll again collect the employee names per department, but this time we will display them in a comma-delimited string.
SQL> SELECT deptno
  2  ,      TO_STRING(CAST(COLLECT(ename) AS varchar2_ntt)) AS emps
  3  FROM   emp
  4  GROUP  BY
  5         deptno;

    DEPTNO EMPS
---------- --------------------------------------------------
        10 CLARK,KING,MILLER
        20 SMITH,JONES,SCOTT,ADAMS,FORD
        30 ALLEN,WARD,MARTIN,BLAKE,TURNER,JAMES
Now we can compare the performance of this method to the STRAGG implementation. We'll start by building a larger dataset to work with. We'll create a table with four sets of DBA_OBJECTS data.
SQL> CREATE TABLE t
  2  AS
  3     SELECT MOD(ROWNUM,100)          AS id
  4     ,      CAST('A' AS VARCHAR2(1)) AS val
  5     FROM   dba_objects
  6     ,      TABLE(varchar2_ntt('A','B','C','D'));

Table created.

SQL> SELECT COUNT(*) FROM t;

  COUNT(*)
----------
    193900
    
1 row selected.

SQL> exec DBMS_STATS.GATHER_TABLE_STATS(USER,'T');

PL/SQL procedure successfully completed.
Now we have almost 200K rows to work with, we'll aggregate the VAL column into a delimited string, using STRAGG. We'll use a TIMER package for wall-clock timings and autotrace for statistics.
SQL> set autotrace traceonly statistics

SQL> exec timer.snap();

PL/SQL procedure successfully completed.

SQL> SELECT id
  2  ,      STRAGG(val) AS vals
  3  FROM   t
  4  GROUP  BY
  5         id;

100 rows selected.


Statistics
----------------------------------------------------------
        221  recursive calls
          9  db block gets
        551  consistent gets
        395  physical reads
          0  redo size
       5213  bytes sent via SQL*Net to client
        388  bytes received via SQL*Net from client
          8  SQL*Net roundtrips to/from client
          3  sorts (memory)
          1  sorts (disk)
        100  rows processed

SQL> exec timer.show('STRAGG');
[STRAGG] 7.20 seconds

PL/SQL procedure successfully completed.
Now we have a rough timing for STRAGG, we can move onto the COLLECT function. The syntax is not quite as simple as STRAGG, as we've seen, but the time-savings are significant.
SQL> exec timer.snap();

PL/SQL procedure successfully completed.

SQL> SELECT id
  2  ,      TO_STRING(CAST(COLLECT(val) AS varchar2_ntt)) AS vals
  3  FROM   t
  4  GROUP  BY
  5         id;

100 rows selected.


Statistics
----------------------------------------------------------
       4441  recursive calls
        111  db block gets
       2519  consistent gets
        109  physical reads
      22040  redo size
       5213  bytes sent via SQL*Net to client
        388  bytes received via SQL*Net from client
          8  SQL*Net roundtrips to/from client
        104  sorts (memory)
          0  sorts (disk)
        100  rows processed

SQL> exec timer.show('COLLECT');
[COLLECT] 1.21 seconds

PL/SQL procedure successfully completed.
We can see that this is significantly faster. Yet we might also notice that many of the statistics are showing considerably more work being performed by Oracle in support of COLLECT. Most interestingly the number of recursive calls and sorts are much higher than for the STRAGG method. How can the COLLECT function be faster? The answer is not displayed by autotrace; it is context-switching. In the STRAGG implementation, there's a context-switch for every value being aggregated (in our example, roughly 193,000). Yet in the COLLECT example, we are only context-switching 100 times (once for every call to TO_STRING). As we know from the 8i days when BULK COLLECT was making headway, context-switching penalties can be high and we can see this once again.

further reading

For a good summary of common string-aggregation techniques, see this article by Tim Hall and this article by William Robertson.
For a copy of STRAGG, see this thread on Ask Tom. This thread also contains a CONCAT_ALL function by James Padfield which is essentially a re-factored STRAGG but allowing slightly more flexibility with delimiters. For a copy of the TIMER function used in the examples in this article, see the Utilities page on this site.

The Credit goes to the below website(s):