It's All About ORACLE

Oracle - The number one Database Management System. Hope this Blog will teach a lot about oracle.

Showing posts with label Collections. Show all posts
Showing posts with label Collections. Show all posts

Oracle Object Type

Object Types

Oracle implements Objects through the use of TYPEs, defined in a similar way to packages. Unlike packages where the instance of the package is limited to the current session, an instance of an object type can be stored in the database for later use. The definition of the type contains a comma separated list of attributes/properties, defined in the same way as package variables, and member functions/procedures. If the a type contains member functions/procedures, the procedural work for these elements is defined in the TYPE BODY.
  • Define A TYPE
  • Define A TYPE BODY
  • Defining A Table
  • Constructors
  • Data Access
  • Issues


Define A TYPE

First we define a type PersonObj to represent a person. In this example, a person is defined by three attributes (first_name, last_name, date_of_birth) and one member function (getAge).
CREATE OR REPLACE TYPE PersonObj AS OBJECT (
  first_name  VARCHAR2(50),
  last_name   VARCHAR2(50),
  date_of_birth  DATE,
  MEMBER FUNCTION getAge RETURN NUMBER
);
/

Define A TYPE BODY

Next we define a TYPE BODY to add functionality to the getAge member function.
CREATE OR REPLACE TYPE BODY PersonObj AS
  MEMBER FUNCTION getAge RETURN NUMBER AS
  BEGIN
    RETURN Trunc(Months_Between(Sysdate, date_of_birth)/12);
  END getAge;
END;
/

Define A TYPE

First we define a type PersonObj to represent a person. In this example, a person is defined by three attributes (first_name, last_name, date_of_birth) and one member function (getAge).
CREATE OR REPLACE TYPE PersonObj AS OBJECT (
  first_name  VARCHAR2(50),
  last_name   VARCHAR2(50),
  date_of_birth  DATE,
  MEMBER FUNCTION getAge RETURN NUMBER
);
/

Define A TYPE BODY

Next we define a TYPE BODY to add functionality to the getAge member function.
CREATE OR REPLACE TYPE BODY PersonObj AS
MEMBER FUNCTION getAge RETURN NUMBER AS
BEGIN
RETURN Trunc(Months_Between(Sysdate, date_of_birth)/12);
END getAge; END;
/

Defining A Table

Now the object type is defined we can use it as a datatype in a table.
CREATE TABLE people (
  id      NUMBER(10) NOT NULL,
  person  PersonObj
);

Constructors

To insert data into the PEOPLE table we must use the PersonObj() constructor. This can be done as part of a regular DML statement, or using PL/SQL.
INSERT INTO people
VALUES (1, PersonObj('John','Doe',
        TO_DATE('01/01/1999','DD/MM/YYYY')));
COMMIT;

DECLARE
  v_person  PersonObj;
BEGIN
  v_person := PersonObj('Jane','Doe',
              TO_DATE('01/01/1999','DD/MM/YYYY'));
  INSERT INTO people VALUES (2, v_person);
  COMMIT;
END;
/


Data Access

Once the data is loaded it can be accessed using the dot notation.
  • alias.column.attribute
  • alias.column.function()
SELECT p.id,
       p.person.first_name,
       p.person.getAge() age
FROM   people p;

        ID PERSON.FIRST_NAME                  AGE
---------- --------------------------- ----------
         1 John                                 2
         2 Jane                                 2

2 row selected.

SQL>

Issues

  • Once an object is used to define a table only it's BODY cannot be altered. To alter the type all table references to it must be dropped. This means the type definition should be very stable before it is used in a table. Note. Later releases of the database allow Type Evolution.
  • The Export & Import often have difficulties with Object Types.
  • The SQL*Plus COPY command does not work with Object Types.
  • There are a number of issues with database links in conjunction with Object Types. This makes the use of object tables in distributed systems impossible without the use of views to hide the object functionality. This defeats the "object" of Objects somewhat.


LIMIT clause in BULK COLLECT

Best practices for knowing your LIMIT and kicking %NOTFOUND
I have started using BULK COLLECT whenever I need to fetch large volumes of data. This has caused me some trouble with my DBA, however. He is complaining that although my programs might be running much faster, they are also consuming way too much memory. He refuses to approve them for a production rollout. What's a programmer to do?
The most important thing to remember when you learn about and start to take advantage of features such as BULK COLLECT is that there is no free lunch. There is almost always a trade-off to be made somewhere. The tradeoff with BULK COLLECT, like so many other performance-enhancing features, is "run faster but consume more memory."
Specifically, memory for collections is stored in the program global area (PGA), not the system global area (SGA). SGA memory is shared by all sessions connected to Oracle Database, but PGA memory is allocated for each session. Thus, if a program requires 5MB of memory to populate a collection and there are 100 simultaneous connections, that program causes the consumption of 500MB of PGA memory, in addition to the memory allocated to the SGA.
Fortunately, PL/SQL makes it easy for developers to control the amount of memory used in a BULK COLLECT operation by using the LIMIT clause.
Suppose I need to retrieve all the rows from the employees table and then perform some compensation analysis on each row. I can use BULK COLLECT as follows: 
PROCEDURE process_all_rows
IS
   TYPE employees_aat 
   IS TABLE OF employees%ROWTYPE
      INDEX BY PLS_INTEGER;
   l_employees employees_aat;
BEGIN
   SELECT *
   BULK COLLECT INTO l_employees
      FROM employees;
     
   FOR indx IN 1 .. l_employees.COUNT 
   LOOP
       analyze_compensation 
      (l_employees(indx));
   END LOOP;
END process_all_rows;

Very concise, elegant, and efficient code. If, however, my employees table contains tens of thousands of rows, each of which contains hundreds of columns, this program can cause excessive PGA memory consumption.

Consequently, you should avoid this sort of "unlimited" use of BULK COLLECT. Instead, move the SELECT statement into an explicit cursor declaration and then use a simple loop to fetch many, but not all, rows from the table with each execution of the loop body, as shown in Listing 1.
Code Listing 1: Using BULK COLLECT with LIMIT clause 
PROCEDURE process_all_rows (limit_in IN PLS_INTEGER DEFAULT 100)
IS
    CURSOR employees_cur 
    IS 
        SELECT * FROM employees;

    TYPE employees_aat IS TABLE OF employees_cur%ROWTYPE
        INDEX BY PLS_INTEGER;

    l_employees employees_aat;
BEGIN   
    OPEN employees_cur;
    LOOP
        FETCH employees_cur 
            BULK COLLECT INTO l_employees LIMIT limit_in;

        FOR indx IN 1 .. l_employees.COUNT 
        LOOP
            analyze_compensation (l_employees(indx));
        END LOOP;

        EXIT WHEN l_employees.COUNT < limit_in;

   END LOOP;

   CLOSE employees_cur;
END process_all_rows;

The process_all_rows procedure in Listing 1 requests that up to the value of limit_in rows be fetched at a time. PL/SQL will reuse the same limit_in elements in the collection each time the data is fetched and thus also reuse the same memory. Even if my table grows in size, the PGA consumption will remain stable.

How do you decide what number to use in the LIMIT clause? Theoretically, you will want to figure out how much memory you can afford to consume in the PGA and then adjust the limit to be as close to that amount as possible.
From tests I (and others) have performed, however, it appears that you will see roughly the same performance no matter what value you choose for the limit, as long as it is at least 25. The test_diff_limits.sql script, included with the sample code for this column, demonstrates this behavior, using the ALL_SOURCE data dictionary view on an Oracle Database 11g instance. Here are the results I saw (in hundredths of seconds) when fetching all the rows (a total of 470,000): 
Elapsed CPU time for limit of 1 = 1839
Elapsed CPU time for limit of 5 = 716
Elapsed CPU time for limit of 25 = 539
Elapsed CPU time for limit of 50 = 545
Elapsed CPU time for limit of 75 = 489
Elapsed CPU time for limit of 100 = 490
Elapsed CPU time for limit of 1000 = 501
Elapsed CPU time for limit of 10000 = 478
Elapsed CPU time for limit of 100000 = 527 


Kicking the %NOTFOUND Habit

I was very happy to learn that Oracle Database 10g will automatically optimize my cursor FOR loops to perform at speeds comparable to BULK COLLECT. Unfortunately, my company is still running on Oracle9i Database, so I have started converting my cursor FOR loops to BULK COLLECTs. I have run into a problem: I am using a LIMIT of 100, and my query retrieves a total of 227 rows, but my program processes only 200 of them. [The query is shown in Listing 2.] What am I doing wrong?
Code Listing 2: BULK COLLECT, %NOTFOUND, and missing rows 
PROCEDURE process_all_rows
IS
   CURSOR table_with_227_rows_cur 
   IS 
      SELECT * FROM table_with_227_rows;

   TYPE table_with_227_rows_aat IS 
      TABLE OF table_with_227_rows_cur%ROWTYPE
      INDEX BY PLS_INTEGER;

   l_table_with_227_rows table_with_227_rows_aat;
BEGIN   
   OPEN table_with_227_rows_cur;
   LOOP
      FETCH table_with_227_rows_cur 
         BULK COLLECT INTO l_table_with_227_rows LIMIT 100;

         EXIT WHEN table_with_227_rows_cur%NOTFOUND;     /* cause of missing rows */

      FOR indx IN 1 .. l_table_with_227_rows.COUNT 
      LOOP
         analyze_compensation (l_table_with_227_rows(indx));
      END LOOP;
   END LOOP;

   CLOSE table_with_227_rows_cur;
END process_all_rows;


You came so close to a completely correct conversion from your cursor FOR loop to BULK COLLECT! Your only mistake was that you didn't give up the habit of using the %NOTFOUND cursor attribute in your EXIT WHEN clause.
The statement 
EXIT WHEN 
table_with_227_rows_cur%NOTFOUND;

makes perfect sense when you are fetching your data one row at a time. With BULK COLLECT, however, that line of code can result in incomplete data processing, precisely as you described.
Let's examine what is happening when you run your program and why those last 27 rows are left out. After opening the cursor and entering the loop, here is what occurs:
1. The fetch statement retrieves rows 1 through 100. 
2. table_with_227_rows_cur%NOTFOUND evaluates to FALSE, and the rows are processed. 
3. The fetch statement retrieves rows 101 through 200. 
4. table_with_227_rows_cur%NOTFOUND evaluates to FALSE, and the rows are processed. 
5. The fetch statement retrieves rows 201 through 227. 
6. table_with_227_rows_cur%NOTFOUND evaluates to TRUE , and the loop is terminated—with 27 rows left to process!
When you are using BULK COLLECT and collections to fetch data from your cursor, you should never rely on the cursor attributes to decide whether to terminate your loop and data processing.
So, to make sure that your query processes all 227 rows, replace this statement: 
EXIT WHEN 
table_with_227_rows_cur%NOTFOUND; 

with

EXIT WHEN 
l_table_with_227_rows.COUNT = 0; 

Generally, you should keep all of the following in mind when working with BULK COLLECT: 
  • The collection is always filled sequentially, starting from index value 1.
  • It is always safe (that is, you will never raise a NO_DATA_FOUND exception) to iterate through a collection from 1 to collection .COUNT when it has been filled with BULK COLLECT.
  • The collection is empty when no rows are fetched.
  • Always check the contents of the collection (with the COUNT method) to see if there are more rows to process.
  • Ignore the values returned by the cursor attributes, especially %NOTFOUND.

NOCOPY Hint to Improve Performance of OUT and IN OUT Parameters in PL/SQL Code

Background

Oracle has two methods of passing passing OUT and IN OUT parameters in PL/SQL code:
  • Pass By Value : The default action is to create a temporary buffer (formal parameter), copy the data from the parameter variable (actual parameter) to that buffer and work on the temporary buffer during the lifetime of the procedure. On successful completion of the procedure, the contents of the temporary buffer are copied back into the parameter variable. In the event of an exception occurring, the copy back operation does not happen.
  • Pass By Reference : Using the NOCOPY hint tells the compiler to use pass by reference, so no temporary buffer is needed and no copy forward and copy back operations happen. Instead, any modification to the parameter values are written directly to the parameter variable (actual parameter).
Under normal circumstances you probably wouldn't notice the difference between the two methods, but once you start to pass large or complex data types (LOBs, XMLTYPEs, collections etc.) the difference between the two methods can be come quite considerable. The presence of the temporary buffer means pass by value requires twice the memory for every OUT and IN OUT parameter, which can be a problem when using large parameters. In addition, the time it takes to copy the data to the temporary buffer and back to the parameter variable can be quite considerable.
The following tests compare the elapsed time and memory consumption of a single call to test procedures passing a large collection as OUT and IN OUT parameters.

Test Code

The test user will need the following privileges to compile the test code.
CONN / AS SYSDBA

GRANT SELECT ON v_$statname TO test;
GRANT SELECT ON v_$mystat TO test;
GRANT CREATE PROCEDURE TO test;
With the privileges in place, compile the test code displayed below.
CONN test/test

CREATE OR REPLACE PACKAGE test_nocopy AS

PROCEDURE in_out_time;
PROCEDURE in_out_memory;
PROCEDURE in_out_nocopy_time;
PROCEDURE in_out_nocopy_memory;

END;
/

CREATE OR REPLACE PACKAGE BODY test_nocopy AS

TYPE     t_tab IS TABLE OF VARCHAR2(32767);
g_tab    t_tab := t_tab();
g_start  NUMBER;

FUNCTION get_stat (p_stat IN VARCHAR2) RETURN NUMBER;
PROCEDURE in_out (p_tab  IN OUT  t_tab);
PROCEDURE in_out_nocopy (p_tab  IN OUT NOCOPY  t_tab);

-- Function to return the specified statistics value.
FUNCTION get_stat (p_stat IN VARCHAR2) RETURN NUMBER AS
  l_return  NUMBER;
BEGIN
  SELECT ms.value
  INTO   l_return
  FROM   v$mystat ms,
         v$statname sn
  WHERE  ms.statistic# = sn.statistic#
  AND    sn.name = p_stat;
  RETURN l_return;
END get_stat;


-- Basic test procedures.
PROCEDURE in_out (p_tab  IN OUT  t_tab) IS
  l_count NUMBER;
BEGIN
  l_count := p_tab.count;
END in_out;

PROCEDURE in_out_nocopy (p_tab  IN OUT NOCOPY  t_tab) IS
  l_count NUMBER;
BEGIN
  l_count := p_tab.count;
END in_out_nocopy;


-- Time a single call using IN OUT.
PROCEDURE in_out_time IS
BEGIN
   g_start := DBMS_UTILITY.get_time;

   in_out(g_tab);

   DBMS_OUTPUT.put_line('IN OUT Time         : ' || 
                        (DBMS_UTILITY.get_time - g_start) || ' hsecs');
END in_out_time;


-- Check the memory used by a single call using IN OUT.
PROCEDURE in_out_memory IS
BEGIN
   g_start := get_stat('session pga memory');

   in_out(g_tab);

   DBMS_OUTPUT.put_line('IN OUT Memory       : ' || 
                        (get_stat('session pga memory') - g_start) || ' bytes');
END in_out_memory;


-- Time a single call using IN OUT NOCOPY.
PROCEDURE in_out_nocopy_time IS
BEGIN
   g_start := DBMS_UTILITY.get_time;

   in_out_nocopy(g_tab);

   DBMS_OUTPUT.put_line('IN OUT NOCOPY Time  : ' || 
                        (DBMS_UTILITY.get_time - g_start) || ' hsecs');
END in_out_nocopy_time;


-- Check the memory used by a single call using IN OUT NOCOPY.
PROCEDURE in_out_nocopy_memory IS
BEGIN
   g_start := get_stat('session pga memory');

   in_out_nocopy(g_tab);

   DBMS_OUTPUT.put_line('IN OUT NOCOPY Memory: ' || 
                        (get_stat('session pga memory') - g_start) || ' bytes');
END in_out_nocopy_memory;


-- Initialization block to populate test collection.
BEGIN
  g_tab.extend;
  g_tab(1) := '1234567890123456789012345678901234567890';
  g_tab.extend(999999, 1);  -- Copy element 1 into 2..1000000
END;
/
The test code includes the following basic elements:
  • g_tab : A global collection populated with a million rows in the initialization block of the package.
  • get_stat : A function to return the specified statistics value. In this case we will use this to return the "session pga memory" value before and after the test and calculate the delta value, representing the memory allocated during the test. Remember, PGA memory is allocated in chunks, so the value will not represent exactly what is required for the temporary buffer space.
  • in_out[_nocopy] : Two test procedures that accept a collection as a parameter and check the number of rows in the collection. The definitions are the same, except for the inclusion of the NOCOPYhint in one of them.
  • in_out[_nocopy]_time : Two procedures that measure the elapsed time associated with calling their respective test procedures.
  • in_out[_nocopy]_memory : Two procedures that measure the memory allocated when calling their respective test procedures.

Run The Tests

When running the test procedures, it makes sense to reconnect every time to make sure you get a new session with a clean PGA allocation.
CONN test/test

SET SERVEROUTPUT ON
EXEC test_nocopy.in_out_time;

CONN test/test

SET SERVEROUTPUT ON
EXEC test_nocopy.in_out_nocopy_time;

CONN test/test

SET SERVEROUTPUT ON
EXEC test_nocopy.in_out_memory;

CONN test/test

SET SERVEROUTPUT ON
EXEC test_nocopy.in_out_nocopy_memory;
When we run these tests, the output looks something like this.
Connected.
IN OUT Time         : 126 hsecs

PL/SQL procedure successfully completed.

Connected.
IN OUT NOCOPY Time  : 0 hsecs

PL/SQL procedure successfully completed.

Connected.
IN OUT Memory       : 99549184 bytes

PL/SQL procedure successfully completed.

Connected.
IN OUT NOCOPY Memory: 0 bytes

PL/SQL procedure successfully completed.

SQL>
From this we can make the following conclusions:
  • Elapsed Time: When we use an IN OUT parameter to pass this large collection, it takes over a second to perform the memory allocation, copy forward and copy back for a single call. In comparison, the time taken to make the call using the IN OUT NOCOPY parameter is not measurable in hundredths of a second, because there is no management of a temporary buffer. So using pass by reference for large parameters gives us a considerable performance boost.
  • Memory Usage: As expected, when passing a large IN OUT parameter by value, the session requires extra memory for the temporary buffer. When the parameter is defined as IN OUT NOCOPY, no extra memory is required as there is no temporary buffer. So using pass by reference for large parameters reduces the memory required by the session.

Issues

There are a number of issues associated with using the NOCOPY hint that you should be aware of before adding it to all your OUT and IN OUT parameters.

  • NOCOPY is a hint. There are a number of circumstances where the compiler can ignore the hint, as described here.
  • If you are testing the contents of the parameter as a measure of successful completion of a procedure, adding NOCOPY may give unexpected results. For example, suppose I pass the value of NULL and assume if the parameter returns with a NOT NULL value the procedure has worked. This will work without NOCOPY, since the copy back operation will not happen in the event of an exception being raised. If I add NOCOPY, all changes are instantly written to the actual parameter, so exceptions will not prevent a NOT NULL value being returned. This may seem like a problem, but in my opinion if this affects you it is an indication of bad coding practice on your part. Failure should be indicated by raising an exception, or at worst using a status flag, rather than testing for values.
  • Parameter Aliasing. If you use a single variable as an actual parameter for multiple OUT and/or IN OUT parameters in a procedure, using a mix of pass by value and pass by reference, you may get unexpected results. This is because the final copy back from the pass by value parameters will wipe out any changes to the pass by reference parameters. This situation can be compounded further if the actual parameter is a global variable that can be referenced directly from within the procedure. Although the manual describes possible issues, once again it is an indication that you are writing terrible code, rather than a limitation of pass by reference. You can read more about parameter aliasing here.

Using Collections Methods

PL/SQL provides the built-in collection methods that make collections easier to use. The following table lists the methods and their purpose:
S.N.Method Name & Purpose
1EXISTS(n)
Returns TRUE if the nth element in a collection exists; otherwise returns FALSE.
2COUNT
Returns the number of elements that a collection currently contains.
3LIMIT
Checks the Maximum Size of a Collection.
4FIRST
Returns the first (smallest) index numbers in a collection that uses integer subscripts.
5LAST
Returns the last (largest) index numbers in a collection that uses integer subscripts.
6PRIOR(n)
Returns the index number that precedes index n in a collection.
7NEXT(n)
Returns the index number that succeeds index n.
8EXTEND
Appends one null element to a collection.
9EXTEND(n)
Appends n null elements to a collection.
10EXTEND(n,i)
Appends n copies of the ith element to a collection.
11TRIM
Removes one element from the end of a collection.
12TRIM(n)
Removes n elements from the end of a collection.
13DELETE
Removes all elements from a collection, setting COUNT to 0.
14DELETE(n)
Removes the nth element from an associative array with a numeric key or a nested table. If the associative array has a string key, the element corresponding to the key value is deleted. If n is null, DELETE(n) does nothing.
15DELETE(m,n)
Removes all elements in the range m..n from an associative array or nested table. If m is larger than n or if m or n is null, DELETE(m,n) does nothing.


A collection method is a built-in function or procedure that operates on collections and is called using dot notation.

Collection methods cannot be called from SQL statements.

EXTEND and TRIM cannot be used with associative arrays.

EXISTS, COUNT, LIMIT, FIRST, LAST, PRIOR, and NEXT are functions; EXTEND,
TRIM, and DELETE are procedures.

EXISTS, PRIOR, NEXT, TRIM, EXTEND, and DELETE take parameters corresponding to
collection subscripts, which are usually integers but can also be strings for associative arrays.

Only EXISTS can be applied to atomically null collections. If you apply another method to such collections, PL/SQL raises COLLECTION_IS_NULL.

Collection Exceptions

The following table provides the collection exceptions and when they are raised:
Collection ExceptionRaised in Situations
COLLECTION_IS_NULLYou try to operate on an atomically null collection.
NO_DATA_FOUNDA subscript designates an element that was deleted, or a nonexistent element of an associative array.
SUBSCRIPT_BEYOND_COUNTA subscript exceeds the number of elements in a collection.
SUBSCRIPT_OUTSIDE_LIMITA subscript is outside the allowed range.
VALUE_ERRORA subscript is null or not convertible to the key type. This exception might occur if the key is defined as a PLS_INTEGER range, and the subscript is outside this range.

Checking if collection Element Exists ( EXISTS Method)

EXISTS(n) returns TRUE if the nth element in a collection exists. Otherwise, EXISTS(n) returns FALSE. By combining EXISTS with DELETE, you can work with sparse nested tables. You can also use EXISTS to avoid referencing a nonexistent element, which raises an exception. When passed an out-of-range subscript, EXISTS returns FALSE instead of raising SUBSCRIPT_OUTSIDE_LIMIT Exception.

DECLARE
TYPE NumList IS TABLE OF INTEGER;
n NumList := NumList(1,3,5,7);
BEGIN
n.DELETE(2); -- Delete the second element
IF n.EXISTS(1) THEN
dbms_output.put_line('OK, element #1 exists.');
END IF;
IF n.EXISTS(2) = FALSE THEN
dbms_output.put_line('OK, element #2 has been deleted.');
END IF;
IF n.EXISTS(99) = FALSE THEN
dbms_output.put_line('OK, element #99 does not exist at all.');
END IF;
END;
/

Counting the elements in a collection ( COUNT method)

COUNT returns the number of elements that a collection currently contains:

DECLARE
TYPE NumList IS TABLE OF NUMBER;
n NumList := NumList(2,4,6,8); -- Collection starts with 4 elements.
BEGIN
dbms_output.put_line('There are ' || n.COUNT || ' elements in N.');
n.EXTEND(3); -- Add 3 new elements at the end.
dbms_output.put_line('Now there are ' || n.COUNT || ' elements in N.');
n := NumList(86,99); -- Assign a completely new value with 2 elements.
dbms_output.put_line('Now there are ' || n.COUNT || ' elements in N.');
n.TRIM(2); -- Remove the last 2 elements, leaving none.
dbms_output.put_line('Now there are ' || n.COUNT || ' elements in N.');
END;
/

 COUNT is useful because the current size of a collection is not always known. For example, you can fetch a column of Oracle data into a nested table, where the number of elements depends on the size of the result set.

 For varrays, COUNT always equals LAST. You can increase or decrease the size of a varray using the EXTEND and TRIM methods, so the value of COUNT can change, up to the value of the LIMIT method.

 For nested tables, COUNT normally equals LAST. But, if you delete elements from the middle of a nested table, COUNT becomes smaller than LAST. When tallying elements, COUNT ignores deleted elements.

Checking the maximum size of  a Collection ( LIMIT method)


For nested tables and associative arrays, which have no maximum size, LIMIT returns NULL. For varrays, LIMIT returns the maximum number of elements that a varray can contain. You specify this limit in the type definition, and can change it later with the TRIM and EXTEND methods. For instance, if the maximum size of varray PROJECTS is 25 elements, the following IF condition is true:

DECLARE
TYPE Colors IS VARRAY(7) OF VARCHAR2(64);
c Colors := Colors('Gold','Silver');
BEGIN
dbms_output.put_line('C has ' || c.COUNT || ' elements now.');
dbms_output.put_line('C''s type can hold a maximum of ' || c.LIMIT || '
elements.');
dbms_output.put_line('The maximum number you can use with C.EXTEND() is ' ||
(c.LIMIT - c.COUNT));
END;
/

Finding the First or Last collection Element ( FIRST and LAST Methods)

FIRST and LAST return the first and last (smallest and largest) index numbers in a collection that uses integer subscripts.
For an associative array with VARCHAR2 key values, the lowest and highest key values are returned. By default, the order is based on the binary values of the characters in the string. If the NLS_COMP initialization parameter is set to ANSI, the order is based on the locale-specific sort order specified by the NLS_SORT initialization parameter. 

If the collection is empty, FIRST and LAST return NULL.
If the collection contains only one element, FIRST and LAST return the same index value.

The following example shows how to use FIRST and LAST to iterate through the elements in a collection that has consecutive subscripts:

DECLARE
TYPE NumList IS TABLE OF NUMBER;
n NumList := NumList(1,3,5,7);
counter INTEGER;
BEGIN
dbms_output.put_line('N''s first subscript is ' || n.FIRST);
dbms_output.put_line('N''s last subscript is ' || n.LAST);
-- When the subscripts are consecutive starting at 1, it's simple to loop through
them.
FOR i IN n.FIRST .. n.LAST
LOOP
dbms_output.put_line('Element #' || i || ' = ' || n(i));
END LOOP;
n.DELETE(2); -- Delete second element.
-- When the subscripts have gaps or the collection might be uninitialized,
-- the loop logic is more extensive. We start at the first element, and
-- keep looking for the next element until there are no more.
IF n IS NOT NULL THEN
counter := n.FIRST;
WHILE counter IS NOT NULL
LOOP
dbms_output.put_line('Element #' || counter || ' = ' || n(counter));
counter := n.NEXT(counter);
END LOOP;
ELSE
dbms_output.put_line('N is null, nothing to do.');
END IF;
END;
/

For varrays, FIRST always returns 1 and LAST always equals COUNT. 
For nested tables, normally FIRST returns 1 and LAST equals COUNT. But if you delete elements from the beginning of a nested table, FIRST returns a number larger than 1.
If you delete elements from the middle of a nested table, LAST becomes larger than COUNT.
When scanning elements, FIRST and LAST ignore deleted elements.

Looping Through Collection Elements (PRIOR and NEXT Methods)

PRIOR(n) returns the index number that precedes index n in a collection. NEXT(n) returns the index number that succeeds index n. If n has no predecessor, PRIOR(n) returns NULL. If n has no successor, NEXT(n) returns NULL.

For associative arrays with VARCHAR2 keys, these methods return the appropriate key value; ordering is based on the binary values of the characters in the string, unless the NLS_COMP initialization parameter is set to ANSI, in which case the ordering is based on the locale-specific sort order specified by the NLS_SORT initialization parameter.

These methods are more reliable than looping through a fixed set of subscript values, because elements might be inserted or deleted from the collection during the loop.
This is especially true for associative arrays, where the subscripts might not be in consecutive order and so the sequence of subscripts might be (1,2,4,8,16) or ('A','E','I','O','U').

DECLARE
TYPE NumList IS TABLE OF NUMBER;
n NumList := NumList(1966,1971,1984,1989,1999);
BEGIN
dbms_output.put_line('The element after #2 is #' || n.NEXT(2));
dbms_output.put_line('The element before #2 is #' || n.PRIOR(2));
n.DELETE(3); -- Delete an element to show how NEXT can handle gaps.
dbms_output.put_line('Now the element after #2 is #' || n.NEXT(2));
IF n.PRIOR(n.FIRST) IS NULL THEN
dbms_output.put_line('Can''t get PRIOR of the first element or NEXT of the
last.');
END IF;
END;

/

o/p:

The element after #2 is #3
The element before #2 is #1
Now the element after #2 is #4
Can't get PRIOR of the first element or NEXT of the last.


PL/SQL procedure successfully completed.

You can use PRIOR or NEXT to traverse collections indexed by any series of subscripts. The following example uses NEXT to traverse a nested table from which some elements have been deleted:

DECLARE
TYPE NumList IS TABLE OF NUMBER;
n NumList := NumList(1,3,5,7);
counter INTEGER;
BEGIN
n.DELETE(2); -- Delete second element.
-- When the subscripts have gaps, the loop logic is more extensive. We start at
the
-- first element, and keep looking for the next element until there are no more.

counter := n.FIRST;
WHILE counter IS NOT NULL
LOOP
dbms_output.put_line('Counting up: Element #' || counter || ' = ' ||
n(counter));
counter := n.NEXT(counter);
END LOOP;
-- Run the same loop in reverse order.
counter := n.LAST;
WHILE counter IS NOT NULL
LOOP
dbms_output.put_line('Counting down: Element #' || counter || ' = ' ||
n(counter));
counter := n.PRIOR(counter);
END LOOP;
END;
/

When traversing elements, PRIOR and NEXT skip over deleted elements.

Increasing the Size of a Collection (EXTEND Method)

To increase the size of a nested table or varray, use EXTEND.

You cannot use EXTEND with index-by tables.
This procedure has three forms:
■ EXTEND appends one null element to a collection.
■ EXTEND(n) appends n null elements to a collection.
■ EXTEND(n,i) appends n copies of the ith element to a collection.

You cannot use EXTEND to add elements to an uninitialized.
If you impose the NOT NULL constraint on a TABLE or VARRAY type, you cannot apply the first two forms of EXTEND to collections of that type.

EXTEND operates on the internal size of a collection, which includes any deleted elements. If EXTEND encounters deleted elements, it includes them in its tally. PL/SQL keeps placeholders for deleted elements so that you can re-create them by assigning new values.

DECLARE
TYPE NumList IS TABLE OF INTEGER;
n NumList := NumList(2,4,6,8);
x NumList := NumList(1,3);
PROCEDURE print_numlist(the_list NumList) IS
output VARCHAR2(128);
BEGIN
FOR i IN the_list.FIRST .. the_list.LAST
LOOP
output := output || NVL(TO_CHAR(the_list(i)),'NULL') || ' ';
END LOOP;
dbms_output.put_line(output);
END;
BEGIN
dbms_output.put_line('At first, N has ' || n.COUNT || ' elements.');
n.EXTEND(5); -- Add 5 elements at the end.
dbms_output.put_line('Now N has ' || n.COUNT || ' elements.');
-- Elements 5, 6, 7, 8, and 9 are all NULL.
print_numlist(n);
dbms_output.put_line('At first, X has ' || x.COUNT || ' elements.');
x.EXTEND(4,2); -- Add 4 elements at the end.
dbms_output.put_line('Now X has ' || x.COUNT || ' elements.');
-- Elements 3, 4, 5, and 6 are copies of element #2.
print_numlist(x);
END;
/

When it includes deleted elements, the internal size of a nested table differs from the values returned by COUNT and LAST. For instance, if you initialize a nested table with five elements, then delete elements 2 and 5, the internal size is 5, COUNT returns 3, and LAST returns 4. All deleted elements, regardless of position, are treated alike.

Decreasing the Size of a Collection (TRIM Method)

This procedure has two forms:
■ TRIM removes one element from the end of a collection.
■ TRIM(n) removes n elements from the end of a collection.

DECLARE
TYPE NumList IS TABLE OF NUMBER;
n NumList := NumList(1,2,3,5,7,11);
PROCEDURE print_numlist(the_list NumList) IS
output VARCHAR2(128);
BEGIN
IF n.COUNT = 0 THEN
dbms_output.put_line('No elements in collection.');
ELSE
FOR i IN the_list.FIRST .. the_list.LAST
LOOP
output := output || NVL(TO_CHAR(the_list(i)),'NULL') || ' ';
END LOOP;
dbms_output.put_line(output);
END IF;
END;
BEGIN
print_numlist(n);
n.TRIM(2); -- Remove last 2 elements.
print_numlist(n);
n.TRIM; -- Remove last element.
print_numlist(n);
n.TRIM(n.COUNT); -- Remove all remaining elements.
print_numlist(n);
-- If too many elements are specified, TRIM raises the exception SUBSCRIPT_BEYOND_
COUNT.
BEGIN
n := NumList(1,2,3);
n.TRIM(100);
EXCEPTION
WHEN SUBSCRIPT_BEYOND_COUNT THEN
dbms_output.put_line('I guess there weren''t 100 elements that could be trimmed.');
END;
-- When elements are removed by DELETE, placeholders are left behind. TRIM counts
these
-- placeholders as it removes elements from the end.
n := NumList(1,2,3,4);
n.DELETE(3); -- delete element 3
-- At this point, n contains elements (1,2,4).
-- TRIMming the last 2 elements removes the 4 and the placeholder, not 4 and 2.
n.TRIM(2);
print_numlist(n);
END;
/
END;
/

If n is too large, TRIM(n) raises SUBSCRIPT_BEYOND_COUNT.
TRIM operates on the internal size of a collection. If TRIM encounters deleted elements, it includes them in its tally. Consider the following example:

DECLARE
TYPE CourseList IS TABLE OF VARCHAR2(10);
courses CourseList;
BEGIN
courses := CourseList('Biol 4412', 'Psyc 3112', 'Anth 3001');
courses.DELETE(courses.LAST); -- delete element 3
/* At this point, COUNT equals 2, the number of valid
elements remaining. So, you might expect the next
statement to empty the nested table by trimming
elements 1 and 2. Instead, it trims valid element 2
and deleted element 3 because TRIM includes deleted
elements in its tally. */
courses.TRIM(courses.COUNT);
dbms_output.put_line(courses(1)); -- prints 'Biol 4412'
END;
/
In general, do not depend on the interaction between TRIM and DELETE. It is better to treat nested tables like fixed-size arrays and use only DELETE, or to treat them like stacks and use only TRIM and EXTEND.
Because PL/SQL does not keep placeholders for trimmed elements, you cannot replace a trimmed element simply by assigning it a new value.

Deleting Collection Elements (DELETE Method)

This procedure has various forms:
■ DELETE removes all elements from a collection.
■ DELETE(n) removes the nth element from an associative array with a numeric key or a nested table. If the associative array has a string key, the element corresponding to the key value is deleted. If n is null, DELETE(n) does nothing.
■ DELETE(m,n) removes all elements in the range m..n from an associative array or nested table. If m is larger than n or if m or n is null, DELETE(m,n) does nothing.

For example:
DECLARE
TYPE NumList IS TABLE OF NUMBER;
n NumList := NumList(10,20,30,40,50,60,70,80,90,100);
TYPE NickList IS TABLE OF VARCHAR2(64) INDEX BY VARCHAR2(32);
nicknames NickList;
BEGIN
n.DELETE(2); -- deletes element 2
n.DELETE(3,6); -- deletes elements 3 through 6
n.DELETE(7,7); -- deletes element 7
n.DELETE(6,3); -- does nothing since 6 > 3
n.DELETE; -- deletes all elements
nicknames('Bob') := 'Robert';
nicknames('Buffy') := 'Esmerelda';
nicknames('Chip') := 'Charles';
nicknames('Dan') := 'Daniel';
nicknames('Fluffy') := 'Ernestina';
nicknames('Rob') := 'Robert';
nicknames.DELETE('Chip'); -- deletes element denoted by this key
nicknames.DELETE('Buffy','Fluffy'); -- deletes elements with keys in this
alphabetic range
END;
/

Varrays always have consecutive subscripts, so you cannot delete individual elements except from the end (by using the TRIM method).
If an element to be deleted does not exist, DELETE simply skips it; no exception is raised. PL/SQL keeps placeholders for deleted elements, so you can replace a deleted element by assigning it a new value.
DELETE lets you maintain sparse nested tables. You can store sparse nested tables in the database, just like any other nested tables.
The amount of memory allocated to a nested table can increase or decrease dynamically. As you delete elements, memory is freed page by page. If you delete the entire table, all the memory is freed.

You Might Also Like

Related Posts with Thumbnails

Pages