Visit the Quest Software Home Page. Pipelines Home

PL/SQL Pipeline  Oracle PL/SQL Tips

As of January 2008, the Pipelines will no longer publish monthly tips. However, we welcome and will post user-submitted tips on this page. Please send your tips to webmaster@quest-pipelines.com.

Our archive of monthly tips will be maintained, so feel free to browse them below.


Previous Tips of the Month

2007 Tips:
January%NOTFOUND Versus NO_DATA_FOUND
February Concurrent Cursors and Batch Systems
March Using the ANYDATA Object
May Variable Binding in PL/SQL
June Design by Contract (DBC) and PL/SQL
July Concatenating Multiple Rows Into A Single String
August Oracle 11g Sequence Enhancement
September Dynamic Ref Cursor with Dynamic Fetch - An 11g Version
October OR(DBMS) or R(DBMS), That is the Question
November Best Practices to Improve SQL Performance
December Mixed Parameter Placement in Oracle 11g Functions

2006 Tips 2005 Tips 2004 Tips
2003 Tips 2002 Tips 2001 Tips
2000 Tips 1999 Tips 1998 Tips
1997 Tips


January's Tip of the Month

%NOTFOUND Versus NO_DATA_FOUND
By S. Inderjeet Singh, Reprinted from OracleBrains.com
BOOKMARK: http://www.quest-pipelines.com/tiptrack.asp?id=20701

Theoretically, %NOTFOUND is a cursor attribute that returns TRUE if the last FETCH statement retrieved no record and FALSE if the last FETCH statement retrieved a record, while NO_DATA_FOUND is a predefined exception (an error condition), which is raised when a SELECT … INTO … statement fetches no record.

The difference between the two is that if a FETCH statement doesn’t retrieve any record, NO_DATA_FOUND is not raised. In other words, when using cursors, the NO_DATA_FOUND exception is not raised, rather the %NOTFOUND attribute value is set to TRUE. However, in the case of a SELECT … INTO … statement, the NO_DATA_FOUND exception is raised and the %NOTFOUND attribute of the implicit cursor is also set to TRUE.

Here is an example:

SQL> DECLARE l_ename VARCHAR2(100);
BEGIN
  SELECT ename INTO l_ename FROM emp
  WHERE empno = ‘1515′;
  DBMS_OUTPUT.PUT_LINE(l_ename);
  EXCEPTION
  WHEN NO_DATA_FOUND THEN
  IF ( SQL%NOTFOUND ) THEN
    DBMS_OUTPUT.PUT_LINE(’NOTFOUND’);
  ELSE
    DBMS_OUTPUT.PUT_LINE(’FOUND’);
  END IF;
END;
/

NOTFOUND

SQL> DECLARE CURSOR c1 IS SELECT ename FROM emp WHERE empno = 1515;
l_ename VARCHAR2(100);
BEGIN
  OPEN c1;
  LOOP
    FETCH c1 INTO l_ename;
    IF ( c1%NOTFOUND ) THEN
      DBMS_OUTPUT.PUT_LINE(’NOTFOUND’);
    ELSE
      DBMS_OUTPUT.PUT_LINE(l_ename);
    END IF;
    EXIT WHEN c1%NOTFOUND;
  END LOOP;
  CLOSE c1;
  EXCEPTION
  WHEN NO_DATA_FOUND THEN
  DBMS_OUTPUT.PUT_LINE(’NO_DATA_FOUND’);
END;
/

NOTFOUND

Hence the two are different while sometimes taken to be the same.


February's Tip of the Month

Concurrent Cursors and Batch Systems
Reprinted from Oracle FAQ
BOOKMARK: http://www.quest-pipelines.com/tiptrack.asp?id=20702

Almost every data retrieval requirement for a batch process can be satisfied either by merging a lookup into the main cursor, or by using associative arrays. In a very small number of cases, there is a need to process rows from two or more very large tables which have N:M join-cardinality.

For example: consider a billing run where an invoice must be generated for each Customer, but in order to generate that invoice the program needs to process all of the Customer's Services, plus all of the Customer's Billing Instructions. If there was only one Billing Instruction per customer (say, the latest one), then the main cursor could join the Customer table to Customer Service (return a row per Service per Customer) and join in the latest Billing Instruction to each row. However, when multiple Billing Instructions are required, such a join would destroy the cardinality of the cursor. One Customer joined to 5 Services and 4 Billing Instructions would explode to 5*4=20 rows.

The most efficient solution to such a problem is to use Concurrent Cursors. For this example, use 3 explicit cursors: one that retrieves all of the customers ordered by the customer number, a second that retrieves all of the Customer Services ordered by customer number, and a third that retrieves all of the Customer Billing Instructions ordered by customer number. Open all of the cursors once only at the beginning of the program, and then fetch the rows customer by customer.

To demonstrate the concept more clearly, the sample code below does not use BULK COLLECT, although it could be adapted to do so.

-- Declare 3 cursors
CURSOR cust_cur IS
SELECT *
FROM   customer
ORDER BY cust_num;

CURSOR custserv_cur IS
SELECT *
FROM   customer_service
ORDER BY cust_num;

CURSOR custinst_cur IS
SELECT *
FROM   customer_billing_instruction
ORDER BY cust_num

-- Declare a record type to FETCH each cursor, and collections to load
-- the services and billing instructions for each customer as they are
-- processed.
curr_cust_rec     CUST_CUR%ROWTYPE;
curr_custserv_rec CUSTSERV_CUR%ROWTYPE;
curr_custinst_rec CUSTINST_CUR%ROWTYPE;

TYPE custserv_tab_type IS TABLE OF CUSTSERV_CUR%ROWTYPE;
TYPE custinst_tab_type IS TABLE OF CUSTINST_CUR%ROWTYPE;

custserv_tab CUSTSERV_TAB_TYPE;
custinst_tab CUSTINST_TAB_TYPE;

BEGIN
    -- Open all the cursors and pre-fetch the first row.
    OPEN cust_cur;
    OPEN custserv_cur;
    OPEN custinst_cur;
    
    FETCH cust_cur INTO curr_cust_rec;
    FETCH custserv_cur INTO curr_custserv_rec;
    FETCH custinst_cur INTO curr_custinst_rec;

    WHILE cust_cur%FOUND LOOP

        -- Load up the services for the customer
        custserv_tab.DELETE;
        WHILE custserv_cur%FOUND 
        AND curr_custserv_rec.cust_num <= curr_cust_rec.cust_num LOOP
            IF curr_custserv_rec.cust_num = curr_cust_rec.cust_num THEN
                custserv_tab.EXTEND;
                custserv_rec.LAST := curr_custserv_rec;
            END IF;
        END LOOP;

        -- Load up the billing instructions for the customer
        custinst_tab.DELETE;
        WHILE custinst_cur%FOUND 
        AND curr_custinst_rec.cust_num <= curr_cust_rec.cust_num LOOP
            IF curr_custinst_rec.cust_num = curr_cust_rec.cust_num THEN
                custinst_tab.EXTEND;
                custinst_rec.LAST := curr_custinst_rec;
            END IF;
        END LOOP;

        <... Process the invoice using services in the custserv_tab
             collection and billing instructions in the custinst_tab
             collection 
         ...>

        FETCH cust_cur INTO curr_cust_rec;
    END LOOP;
END;

Even though the above example – not using BULK COLLECT – does not significantly reduce context switching to any significant degree, it still has a profound improvement in performance: usually several orders of magnitude. The reason for this is that the Customer Service and Customer Billing Instruction tables are processed in a single fast Full Table Scan, rather than the death-of-a-thousand-cuts method of reading via the index in a separate cursor per customer.


March's Tip of the Month

Using the ANYDATA Object
Reprinted from OracleBrains.com
BOOKMARK: http://www.quest-pipelines.com/tiptrack.asp?id=20703

First introduced in Oracle 9i, ANYDATA is a "self describing data instance type" which means it not only holds the value, but it also holds its own data type within each instance of it. An ANYDATA can be persistently stored in the database. It also contains many methods that allow us to perform various functions with it.

Now how and where could we use this? Imagine a situation where we need to design a table with a column which should store any type of data, and we only know what kind of data when the application is actually run. For each row there can be any type of data type.

In the past, what we would normally do is declare it as VARCHAR2. While inserting we would convert each value to VARCHAR2. But the problem with this approach is that we don't know what kind of data type is stored and even if we know we need to write a conversion for it to convert it into the original value. Now ANYDATA type comes to the rescue.

Such fields in Oracle 9i and above can be declared as SYS.ANYDATA. After that we can use whatever type we want and it will stay that type. For example, if we use date, it will stays as date and we will be able to perform any date operation on it without any need for conversion.

Examples:

SQL> CREATE TABLE tab1( col1 SYS.ANYDATA );

Table created.

SQL> INSERT INTO tab1 VALUES(SYS.ANYDATA.convertVarchar2(’Rajender Singh’));

SQL> INSERT INTO tab1 VALUES(SYS.ANYDATA.convertDate(SYSDATE));

SQL> INSERT INTO tab1 VALUES(SYS.ANYDATA.convertNumber(1972));

Now we know how to create a table with it and we know how to put in values.

The next question is how to get back a value. The bad news is that there is no straight-forward method to do so. The good news is that the ANYDATA type provides many methods that help us perform this and many others functions.

For example, by using the following methods exposed in the ANYDATA type we can get the desire data in the original data type:

gettypeName()
getNumber()
getDate()
getVarchar2()

…

DECLARE
l_flag NUMBER;
l_v VARCHAR2(100);
l_n NUMBER;
l_d DATE;
l_error VARCHAR2(40);
BEGIN
CASE anydata_type_variable.gettypeName
WHEN ‘SYS.NUMBER’ THEN
l_flag :=anydata_type_variable.getNumber(l_n);
WHEN ‘SYS.DATE’ THEN
l_flag :=anydata_type_variable.getDate(l_d);
WHEN ‘SYS.VARCHAR2′ THEN
l_flag :=anydata_type_variable.getVarchar2(l_v);
ELSE
l_error := ‘** unknown **’;
END CASE;
….
…


May's Tip of the Month

Variable Binding in PL/SQL
By Rolland Sovarszki, Reprinted from http://www.dba-village.com
BOOKMARK: http://www.quest-pipelines.com/tiptrack.asp?id=20705

Binding standard selections has helped me a lot in improving PL/SQL procedure performance.

Say you have a SELECT statement (it could be a regullar select into or a FOR loop), and you have a column A which can have either value 1 or value 0.

The selection would look like this:

SELECT * 
FROM <my tab> 
WHERE column_A = 1 

<.....> 

SELECT * 
FROM <my tab> 
WHERE column_A = 0

In this case the Optimizer would create 2 definitions for basically the same SQL statement.

Instead you could use binding variables:

DECLARE 

val_1 CONSTANT PLS_INTEGER := 0; 
val_2 CONSTANT PLS_INTEGER := 1; 

BEGIN 

SELECT * 
FROM <my tab> 
WHERE column_A = val_2; 

<.....> 

SELECT * 
FROM <my tab> 
WHERE column_A = val_1; 

END;

In this case the Optimizer would create only 1 definition:

SELECT * 
FROM <my tab> 
WHERE column_A = :bind;

:bind then would take either value 1, or value 0, depending on the variable.

In this case you saved the Optimizer from doing 2 execution plans for the same selection.


June's Tip of the Month

Design by Contract (DBC) and PL/SQL
By Zlatko Sirotic, Istra Informaticki Inzenjering, Croatia
BOOKMARK: http://www.quest-pipelines.com/tiptrack.asp?id=20706

Design by contract (DBC) is a method whose author is Bertrand Mayer, also the maker of the OOPL language Eiffel (Eiffel was designed in 1985, commercialy released in 1986, ISO-standardized in 2006). Simplified, DBC is based on the principle that in each routine (procedure or function) with standard code, two additional parts – PRECONDITION and POSTCONDITION - need to be asserted. An additional assertion in this class is called INVARIANT. Contract is based on the routine's putting up an obligation to the caller (some other routine) to satisfy conditions of precondition and conditions of invariant, and the called routine's obligation to satisfy conditions of postcondition and conditions of invariant.

The object oriented Eiffel programming language was created to implement DBC. For now, other OO (object-oriented) languages don’t directly support the ideas behind DBC. However, precondition and postcondition are applicable to many programming languages, both OO and not OO. Invariants are applicable only in OOPL.

This is my attempt to use DBC methodology (including invariants) in Oracle PL/SQL.

This is an Eiffel class interface (not like a Java interface, but more like PL/SQL package specification) from Bertrand Meyer's book "Object Oriented Software Construction", second edition (OOSC2), 1997, page 390-391:

class interface STACK [G]

creation make

feature -- Initialization
  make (n: INTEGER) is -- Alocate stack for a maximum of n elements
    require
      non_negative_capacity: n >= 0
    ensure
      capacity_set: capacity = n
    end

feature -- Access
  capacity: INTEGER -- Maximum number of stack elements

  count: INTEGER -- Number of stack elements

  item: G is -– Top element
    require
      not_empty: not empty
    end

feature -- Status report
  empty: BOOLEAN is -- Is stack empty?
    ensure
      empty_definition: Result = (count = 0)
    end

  full: BOOLEAN is -- Is stack full?
    ensure
      full_definition: Result = (count = capacity)
    end

feature -- Element change
  put (x: G) is -- Add x on top
    require
      not_full: not full
    ensure
      not_empty: not empty
      added_to_top: item = x
      one_more_item: count = old count + 1
    end

  remove is -– Remove top element
    require
      not_empty: not empty
    ensure
      not_full: not full
      one_fewer: count = old count - 1
    end

invariant
  count_non_negative: 0 <= count
  count_bounded: count <= capacity
  empty_if_no_elements: empty = (count = 0)

end -– class interface STACK
 


PL/SQL "equivalent":

-- Stack implementation - TABLE of INTEGER.
-- Eiffel has generic classes (like C++ templates and better than Java generics).
-- PL/SQL (now) has not generic classes or generic packages
-- (PL/SQL was modeled after ADA 83, and ADA 83 has generic packages).
--
CREATE OR REPLACE TYPE array_t AS TABLE OF INTEGER
/


-- utility package:
CREATE OR REPLACE PACKAGE dbc AS
  -- 0 = no check
  -- 1 = check preconditions
  -- 2 = check preconditions + postconditions
  -- 3 = check preconditions + postconditions + invariants
  c_no_check                  CONSTANT INTEGER := 0;
  c_check_preconditions       CONSTANT INTEGER := 1;
  c_check_pre_postconditions  CONSTANT INTEGER := 2;
  c_check_pre_post_invariants CONSTANT INTEGER := 3;

  FUNCTION check_preconditions       RETURN BOOLEAN;
  FUNCTION check_pre_postconditions  RETURN BOOLEAN;
  FUNCTION check_pre_post_invariants RETURN BOOLEAN;

  PROCEDURE set_level (p_level INTEGER);
  PROCEDURE display_error (p_error VARCHAR2);
END;
/

CREATE OR REPLACE PACKAGE BODY dbc AS
  m_level INTEGER := c_no_check;

  FUNCTION check_preconditions RETURN BOOLEAN IS
  BEGIN
    IF m_level >= c_check_preconditions THEN
      RETURN TRUE;
    ELSE
      RETURN FALSE;
    END IF;   
  END;

  FUNCTION check_pre_postconditions RETURN BOOLEAN IS
  BEGIN
    IF m_level >= c_check_pre_postconditions THEN
      RETURN TRUE;
    ELSE
      RETURN FALSE;
    END IF;   
  END;

  FUNCTION check_pre_post_invariants RETURN BOOLEAN IS
  BEGIN
    IF m_level >= c_check_pre_post_invariants THEN
      RETURN TRUE;
    ELSE
      RETURN FALSE;
    END IF;   
  END;

  PROCEDURE set_level (p_level INTEGER) IS
  BEGIN
    IF p_level NOT IN
      (c_no_check, c_check_preconditions, c_check_pre_postconditions, 
        c_check_pre_post_invariants)
    THEN
      RAISE_APPLICATION_ERROR (-20000, 'Wrong checking level');
    END IF;

    m_level := p_level;
  END;

  PROCEDURE display_error (p_error VARCHAR2) IS
  BEGIN
    RAISE_APPLICATION_ERROR (-20000, 'ERROR in method ' || p_error);
  END;
END;
/


CREATE OR REPLACE TYPE stack AS OBJECT (
  -- Maximum number of stack elements
  capacity INTEGER,

  -- Number of stack elements
  el_count INTEGER,

  -- Stack implementation
  stack_implementation array_t,

  -- Alocate stack for a maximum of n elements
  CONSTRUCTOR FUNCTION stack (n INTEGER) RETURN SELF AS RESULT,

  -- Top element
  MEMBER FUNCTION item (SELF IN OUT stack) RETURN INTEGER,

  -- Is stack empty?
  MEMBER FUNCTION empty RETURN BOOLEAN,

  -- Is stack full?
  MEMBER FUNCTION full RETURN BOOLEAN,

  -- Add x on top
  MEMBER PROCEDURE put (x INTEGER),

  -- Remove top element
  MEMBER PROCEDURE remove,

  -- INVARIANTS
  -- Note:
  -- If subprogram is declared in an object type body (in PL/SQL 8i/9i/10g)
  -- it must be defined in the object type specification too.
  MEMBER FUNCTION count_non_negative RETURN BOOLEAN,
  MEMBER FUNCTION count_bounded RETURN BOOLEAN,
  MEMBER FUNCTION empty_if_no_elements RETURN BOOLEAN,
  MEMBER PROCEDURE check_invariants
) NOT FINAL;
/

CREATE OR REPLACE TYPE BODY stack AS
  CONSTRUCTOR FUNCTION stack (n INTEGER) RETURN SELF AS RESULT IS
  BEGIN
    IF dbc.check_preconditions AND n < 0 THEN
      dbc.display_error ('stack - PRE');
    END IF;

    check_invariants;
   
    capacity := n;
    stack_implementation := array_t();
    stack_implementation.EXTEND (n);
   
    IF dbc.check_pre_postconditions AND capacity <> n THEN
      dbc.display_error ('stack - POST');
    END IF;

    check_invariants;
  END;

  MEMBER FUNCTION item (SELF IN OUT stack) RETURN INTEGER IS
  BEGIN
    IF dbc.check_preconditions AND empty THEN
      dbc.display_error ('item - PRE');
    END IF;

    check_invariants;

    RETURN stack_implementation(el_count);
  END;

  MEMBER FUNCTION empty RETURN BOOLEAN IS
  BEGIN
    IF el_count = 0 THEN
      RETURN TRUE;
    ELSE
      RETURN FALSE;
    END IF;
  END;
 
  MEMBER FUNCTION full RETURN BOOLEAN IS
  BEGIN
    IF el_count = capacity THEN
      RETURN TRUE;
    ELSE
      RETURN FALSE;
    END IF;
  END;

  MEMBER PROCEDURE put (x INTEGER) IS
  BEGIN
    IF dbc.check_preconditions AND full THEN
      dbc.display_error ('put - PRE');
    END IF;

    check_invariants;

    el_count := el_count + 1;
    stack_implementation(el_count) := x;

    -- PL/SQL has not Eiffel's OLD
    -- one_more_item: count = old count + 1
    IF dbc.check_pre_postconditions AND (empty OR item <> x) THEN
      dbc.display_error ('put - POST');
    END IF;

    check_invariants;
  END;

  MEMBER PROCEDURE remove IS BEGIN
    IF dbc.check_preconditions AND empty THEN
      dbc.display_error ('remove - PRE');
    END IF;

    check_invariants;

    el_count := el_count - 1;

    -- PL/SQL has not Eiffel's OLD
    -- one_fewer: count = old count - 1
    IF dbc.check_pre_postconditions AND full THEN
      dbc.display_error ('remove - POST');
    END IF;

    check_invariants;
  END;


  -- INVARIANTS

  MEMBER FUNCTION count_non_negative RETURN BOOLEAN IS
  BEGIN
    IF el_count >= 0 THEN
      RETURN TRUE;
    ELSE
      RETURN FALSE;
    END IF;
  END;

  MEMBER FUNCTION count_bounded RETURN BOOLEAN IS
  BEGIN
    IF el_count <= capacity THEN
      RETURN TRUE;
    ELSE
      RETURN FALSE;
    END IF;
  END;

  MEMBER FUNCTION empty_if_no_elements RETURN BOOLEAN IS
  BEGIN
    IF empty AND (el_count = 0)
       OR
       NOT empty AND (el_count <> 0)
    THEN
      RETURN TRUE;
    ELSE
      RETURN FALSE;
    END IF;
  END;

  MEMBER PROCEDURE check_invariants IS
  BEGIN
    IF NOT dbc.check_pre_post_invariants THEN
      RETURN; -- without checking invariants
    END IF;

    IF NOT count_non_negative THEN
      dbc.display_error ('INVARIANT count_non_negative');
    END IF;

    IF NOT count_bounded THEN
      dbc.display_error ('INVARIANT count_bounded');
    END IF;

    IF NOT empty_if_no_elements THEN
      dbc.display_error ('INVARIANT empty_if_no_elements');
    END IF;
  END;
END; -- class body STACK
/


July's Tip of the Month

Concatenating Multiple Rows Into A Single String
By Steven Rea, Reprinted from Steven Rea's Oracle Tips, Tricks and Scripts
BOOKMARK: http://www.quest-pipelines.com/tiptrack.asp?id=20707

If you have multiple rows that you want to concatenate into a single string, such as creating a string of e-mail addresses from a list of those addresses returned by a "select" statement, you can use this tip that I found at asktom.oracle.com, which makes a novel use of the Oracle analytical functions.

In the example below, we are getting a list of e-mail addresses for customers with a zip code of 72204, and concatenating that list into a comma-separated string. To plug in your own SQL, simply replace "select email_addr myvalues from customers where zip = 72204" with your own SQL that returns a single column of values, and label that column "myvalues".

In the example below, "myvalues" is the label of the list of e-mail addresses returned by the "select from customers" SQL. "row_number() over (order by myvalues)" produces a sequential number for each record returned by the "select from customers" SQL (this column is labeled "rn"), where the records are ordered by the e-mail address. "count(*) over ()" produces the total number of e-mail addresses returned by the "select from customers" SQL (this column is labeled "cnt"). "ltrim(sys_connect_by_path(myvalues, ','),',')" concatenates the "path" of values created by the "start with" and "connect by prior" chain, placing a comma between the values and, then, removing the leftmost comma (this column is labeled "catvalues"). "rn = cnt" selects the row where the row number equals the count of records, thus just returning the final row with the full path of values. You can get an idea of what this is doing by removing the "where rn = cnt" line and seeing the records that are returned.

with data
   as
   (
      select myvalues, row_number() over (order by myvalues) rn, count(*) over () cnt
         from
         (
            select email_addr myvalues from customers where zip = 72204
         )
   )
select ltrim(sys_connect_by_path(myvalues, ','),',') catvalues
   from data
   where rn = cnt
   start with rn = 1
   connect by prior rn = rn-1;


August's Tip of the Month

Oracle 11g Sequence Enhancement
By Rajender Singh, Reprinted from OracleBrains
BOOKMARK: http://www.quest-pipelines.com/tiptrack.asp?id=20708

In Oracle, the sequences object provides a sequential series of numbers to applications without the overhead of disk I/O or transaction locking. These values are often used for primary and unique keys.

In the previous release of Oracle, you could refer to the sequence values in SQL statements with the following pseudocolumns:

  • CURRVAL - returns the current value of a sequence.
     
  • NEXTVAL - increments the sequence and returns the next value.

Example:

SELECT employeeid.NEXTVAL
INTO l_employeeid
FROM DUAL;

But one drawback was that these pseudocolumns cannot be used directly in PL/SQL.

Sometimes this can be quite irritating from a programmer's point of view.

In Oracle Database 11g, it is now possible to simply use the pseudocolumns CURRVAL and NEXTVAL in a PL/SQL expression.

Example:

l_employeeid := employeeid.NEXTVAL;


September's Tip of the Month

Dynamic Ref Cursor with Dynamic Fetch - An 11g Version
By Zlatko Sirotic, Istra Informaticki Inzenjering, Croatia
BOOKMARK: http://www.quest-pipelines.com/tiptrack.asp?id=20709

Sometimes there is a problem as follows: We've got a function that, based on dynamically generated query, returns a ref cursor variable. Now we want to use this ref cursor variable in our procedure, but we don't know the record structure. The problem is how to make a "FETCH l_ref_cur INTO record_variable" when we don't know the record variable structure.

This is a hard problem, because ref cursors do not (directly) support description! We are going to use "a good old" DBMS_SQL package and its procedure DESCRIBE_COLUMNS in order to make an unknown record variable. This is my Oracle Magazine tip from 2003: "Dynamic Ref Cursor with Dynamic Fetch".

Prior to 11g, the calling function must return not only the ref cursor, but the original query also. In 11g we can use new features DBMS_SQL.TO_CURSOR_NUMBER and DBMS_SQL.TO_REFCURSOR (to convert a ref cursor to DBMS_SQL cursor and vice versa) - now the calling function can return only the ref cursor.

1. "Generic" package

First we are going to make a "dyn_fetch" package. Procedure "describe_columns" creates "g_record_def" dynamic record structure, with column names as col_1 (col_2 ...), because the SELECT clause in the query can be without aliases (for example "SELECT deptno || dname FROM dept").

CREATE OR REPLACE PACKAGE dyn_fetch IS
   g_count      NUMBER;
   g_record_def VARCHAR2 (32000);

   PROCEDURE describe_columns (p_ref_cur IN OUT SYS_REFCURSOR);

   PROCEDURE fetch_ref_cur (
      p_ref_cur SYS_REFCURSOR,
      p_process_def VARCHAR2);
END;
/

CREATE OR REPLACE PACKAGE BODY dyn_fetch IS
   varchar2_type CONSTANT PLS_INTEGER := 1;
   number_type   CONSTANT PLS_INTEGER := 2;
   date_type     CONSTANT PLS_INTEGER := 12;
   rowid_type    CONSTANT PLS_INTEGER := 11;
   char_type     CONSTANT PLS_INTEGER := 96;
   long_type     CONSTANT PLS_INTEGER := 8;
   raw_type      CONSTANT PLS_INTEGER := 23;
   mlslabel_type CONSTANT PLS_INTEGER := 106;
   clob_type     CONSTANT PLS_INTEGER := 112;
   blob_type     CONSTANT PLS_INTEGER := 113;
   bfile_type    CONSTANT PLS_INTEGER := 114;

   PROCEDURE describe_columns (p_ref_cur IN OUT SYS_REFCURSOR) IS
      l_cur           INTEGER;
      l_desc_tab      DBMS_SQL.DESC_TAB;
      l_type          VARCHAR2 (100);
      l_col_type      PLS_INTEGER;
      l_col_max_len   PLS_INTEGER;
      l_col_precision PLS_INTEGER;
      l_col_scale     PLS_INTEGER;
   BEGIN
      l_cur := DBMS_SQL.TO_CURSOR_NUMBER (p_ref_cur); -- new feature in 11g
      DBMS_SQL.DESCRIBE_COLUMNS (l_cur, g_count, l_desc_tab);
      p_ref_cur := DBMS_SQL.TO_REFCURSOR (l_cur); -- new feature in 11g

      g_record_def := NULL;

      FOR i IN 1..g_count LOOP
         l_col_type      := l_desc_tab(i).col_type;
         l_col_max_len   := l_desc_tab(i).col_max_len;
         l_col_precision := l_desc_tab(i).col_precision;
         l_col_scale     := l_desc_tab(i).col_scale;

         IF    l_col_type = varchar2_type THEN
            l_type := 'VARCHAR2(' || l_col_max_len || ')';
         ELSIF l_col_type = number_type THEN
            l_type := 'NUMBER(' || l_col_precision || ',' || l_col_scale || ')';
         ELSIF l_col_type = date_type THEN
            l_type := 'DATE';
         ELSIF l_col_type = rowid_type THEN
            l_type := 'ROWID';
         ELSIF l_col_type = char_type THEN
            l_type := 'CHAR(' || l_col_max_len || ')';
      -- ELSIF  l_col_type = ...
            -- long_type, raw_type ...
         END IF;

         g_record_def := g_record_def || ' col_' || i || ' ' || l_type || ',';
      END LOOP;

      g_record_def := RTRIM (g_record_def, ',');
   END;

   PROCEDURE fetch_ref_cur (
      p_ref_cur     SYS_REFCURSOR,
      p_process_def VARCHAR2)
   IS
      l_statement VARCHAR2 (32000);
   BEGIN
      l_statement :=
      '  DECLARE
            TYPE record_t IS RECORD (' || dyn_fetch.g_record_def || ');
            l_record  record_t;
         BEGIN
            LOOP
               FETCH :p_ref_cur INTO l_record;
               EXIT WHEN :p_ref_cur%NOTFOUND;' ||
               p_process_def || '
            END LOOP;
            CLOSE :p_ref_cur;
         END;
      ';

      EXECUTE IMMEDIATE l_statement USING p_ref_cur; -- works in 10g
   END;
END;
/

2. Function (or procedure) that returns ref cursor

CREATE OR REPLACE FUNCTION test_ref_cur RETURN SYS_REFCURSOR IS
   l_query   VARCHAR2 (32000);
   l_ref_cur SYS_REFCURSOR;
BEGIN
   l_query :=
   '  SELECT e.empno,  e.ename,
             e.deptno, d.dname
        FROM emp  e,
             dept d
       WHERE e.deptno = d.deptno
   ';

   OPEN l_ref_cur FOR l_query;
   RETURN l_ref_cur;
END;
/

3. Procedure that uses function that returns ref cursor

Procedure first calls "test_ref_cur" function that returns ref cursor. Then it calls the "dyn_fetch.describe_columns" procedure in order to get the dynamically generated record structure, and "dyn_fetch.fetch_ref_cur" procedure, sending two parameters: ref cursor and process definition through the (internal) "process_def" function (in this case to show up rows with DBMS_SQL.PUT_LINE).

CREATE OR REPLACE PROCEDURE test_fetch_ref_cur IS
   l_ref_cur SYS_REFCURSOR;

   FUNCTION process_def RETURN VARCHAR2 IS
      l_process_def VARCHAR2 (32000);
   BEGIN
      l_process_def := 'DBMS_OUTPUT.PUT_LINE (';

      FOR i IN 1 .. dyn_fetch.g_count LOOP
         l_process_def := l_process_def || ' l_record.col_' || i || ' || ''>>'' || ';
      END LOOP;

      l_process_def := RTRIM (l_process_def, ' || ''>>'' || ') || ');';
      RETURN l_process_def;
   END;
BEGIN
   l_ref_cur := test_ref_cur;
   dyn_fetch.describe_columns (l_ref_cur);
   dyn_fetch.fetch_ref_cur (
      p_ref_cur     => l_ref_cur,
      p_process_def => process_def);
END;
/

We can test this with:

SET SERVEROUTPUT ON;
EXECUTE test_fetch_ref_cur;


October's Tip of the Month

OR(DBMS) or R(DBMS), That is the Question
By Zlatko Sirotic, Istra Informaticki Inzenjering, Croatia
BOOKMARK: http://www.quest-pipelines.com/tiptrack.asp?id=20710

In the late 1990s, several vendors (including Oracle) released object-relational DBMS products (ORDBMS).

But, C.J.Date in Database in Depth (2005, page 32) says:

"After all, the whole point of an "object/relational" system is precisely that we can have attribute values in relations that are of arbitrary complexity. Perhaps a better way to say it is this: a proper object/relational system is just a relational system with proper type support - which just means it's a proper relational system, no more and no less."

In An introduction to Database Systems (2004, page 885), Date says:

"... object/relational systems ... are, or should be, basically just relational systems that support the relational domain concept (i.e., types) properly - in other words, true relational systems, meaning in particular systems that allow users to define their own types."

Here is an example of how to use "object-relational" features "in the proper way" (in Date's sense) - to use object type as domain.

First create object type address_t:

CREATE OR REPLACE TYPE address_t AS OBJECT (
  state       VARCHAR2(30),
  city        VARCHAR2(30),
  postal_code VARCHAR2(16)
) NOT FINAL
/

Next, create a relational table that has one column of object type address_t. (Oracle 8.0 / 8i documentation uses the term "object-relational table" for this type of table; but in Oracle 9i / 10g / 11g documentation this is simply "relational table".)

CREATE TABLE users_tab (
  user_id    NUMBER(30) PRIMARY KEY,
  first_name VARCHAR2(30),
  last_name  VARCHAR2(30),
  address    address_t
)
/

INSERT INTO users_tab
  VALUES (1, 'FN1', 'LN1', address_t ('S1', 'C1', 'PC1'));

This SELECT shows the physical structure of table users_tab:

SELECT segcol#, name, segcollength, type#
  FROM sys.col$
 WHERE obj# =
       (SELECT object_id
          FROM user_objects
         WHERE object_name = 'USERS_TAB'
       )
 ORDER BY segcol#;

   SEGCOL# NAME                           SEGCOLLENGTH      TYPE#
---------- ------------------------------ ------------ ----------
         1 USER_ID                                  22          2
         2 FIRST_NAME                               30          1
         3 LAST_NAME                                30          1
         4 ADDRESS                                   1        121
         5 SYS_NC00005$                             16         23
         6 SYS_NC00006$                             30          1
         7 SYS_NC00007$                             30          1
         8 SYS_NC00008$                             16          1

Hidden columns SYS_NC00006$, SYS_NC00007$, SYS_NC00008$ corresponds to attributes state, city, postal_code.

Now, create a subtype address_sub_t under (supertype) address_t:

CREATE OR REPLACE TYPE address_sub_t UNDER address_t (
  street VARCHAR2(40)
)
/

INSERT INTO users_tab
   VALUES (2, 'FN2', 'LN2', address_sub_t ('S2', 'C2', 'PC2', 'STREET 1'));

SELECT shows the changed physical structure of table users_tab:

SELECT segcol#, name, segcollength, type#
  FROM sys.col$
 WHERE obj# =
       (SELECT object_id
          FROM user_objects
         WHERE object_name = 'USERS_TAB'
       )
 ORDER BY segcol#;

   SEGCOL# NAME                           SEGCOLLENGTH      TYPE#
---------- ------------------------------ ------------ ----------
         1 USER_ID                                  22          2
         2 FIRST_NAME                               30          1
         3 LAST_NAME                                30          1
         4 ADDRESS                                   1        121
         5 SYS_NC00005$                             16         23
         6 SYS_NC00006$                             30          1
         7 SYS_NC00007$                             30          1
         8 SYS_NC00008$                             16          1
         9 SYS_NC00009$                             40          1

Oracle automatically adds a new hidden column, SYS_NC00009, that corresponds to attribute street!

Various SELECT statements:

SELECT user_id, first_name, last_name,
       address
  FROM users_tab
 ORDER BY user_id;

   USER_ID FIRST_NAME    LAST_NAME   ADDRESS(STATE, CITY, POSTAL_CODE)
---------- ------------- ----------- -----------------------------------------------------
         1 FN1           LN1         ADDRESS_T('S1', 'C1', 'PC1')
         2 FN2           LN2         ADDRESS_SUB_T('S2', 'C2', 'PC2', 'STREET 1')


SELECT user_id, first_name, last_name,
       u.address.state
  FROM users_tab u
 ORDER BY user_id;

   USER_ID FIRST_NAME                     LAST_NAME                      ADDRESS.STATE
---------- ------------------------------ ------------------------------ --------------
         1 FN1                            LN1                            S1
         2 FN2                            LN2                            S2

We can't (directly) select hidden columns that corresponds to attributes of subtype (address_sub_t):

SELECT user_id, first_name, last_name,
       u.address.street
  FROM users_tab u
 ORDER BY user_id;

       u.address.street
       *
ERROR at line 2:
ORA-00904: "U"."ADDRESS"."STREET": invalid identifier

But we can use TREAT ("object_column" AS "subtype"):

SELECT user_id, first_name, last_name,
       TREAT (address AS address_sub_t).street address_street
  FROM users_tab
 ORDER BY user_id;

   USER_ID FIRST_NAME                     LAST_NAME                      ADDRESS_STREET
---------- ------------------------------ ------------------------------ ----------------
         1 FN1                            LN1
         2 FN2                            LN2                            STREET 1

Show rows that belongs to a selected subtype:

SELECT user_id, first_name, last_name,
       TREAT (address AS address_sub_t).street address_street
  FROM users_tab
 WHERE address IS OF (address_sub_t)
 ORDER BY user_id;

   USER_ID FIRST_NAME                     LAST_NAME                      ADDRESS_STREET
---------- ------------------------------ ------------------------------ ---------------
         2 FN2                            LN2                            STREET 1

Show rows that belongs to a selected (super)type and all its subtypes:

SELECT user_id, first_name, last_name,
       TREAT (address AS address_sub_t).street address_street
  FROM users_tab
 WHERE address IS OF (address_t)
 ORDER BY user_id;

   USER_ID FIRST_NAME                     LAST_NAME                      ADDRESS_STREET
---------- ------------------------------ ------------------------------ ----------------
         1 FN1                            LN1
         2 FN2                            LN2                            STREET 1

Show rows that belongs to selected (super)type only:

SELECT user_id, first_name, last_name,
       TREAT (address AS address_sub_t).street address_street
  FROM users_tab
 WHERE address IS OF (ONLY address_t)
 ORDER BY user_id;

   USER_ID FIRST_NAME                     LAST_NAME                      ADDRESS_STREET
---------- ------------------------------ ------------------------------ ---------------
         1 FN1                            LN1

We can update the whole object column:

UPDATE users_tab
   SET address = address_sub_t ('S2', 'C2', 'PC2', 'STREET 2')
 WHERE user_id = 2;

We can update hidden columns that correspond to attributes of the root type (address_t):

UPDATE users_tab u
   SET u.address.state = 'STATE 2'
 WHERE user_id = 2;

But we can't (directly) update hidden columns that correspond to attributes of a subtype (address_sub_t):

UPDATE users_tab u
   SET u.address.street = 'STREET 2'
 WHERE user_id = 2;

   SET u.address.street = 'STREET 2'
       *
ERROR at line 2:
ORA-00904: "U"."ADDRESS"."STREET": invalid identifier

or

UPDATE users_tab
   SET TREAT (addres AS address_sub_t).street = 'STREET 2'
 WHERE user_id = 2;

   SET TREAT (addres AS address_sub_t).street = 'STREET 2'
             *
ERROR at line 2:
ORA-00927: missing equal sign

This works - UPDATE inline wiew:

UPDATE (SELECT TREAT (address AS address_sub_t).street address_street
          FROM users_tab
         WHERE user_id = 2
       )
   SET address_street = 'STREET 2';

or

UPDATE (SELECT user_id,
               TREAT (address AS address_sub_t).street address_street
          FROM users_tab
       )
   SET address_street = 'STREET 2'
 WHERE user_id = 2;


Novemer's Tip of the Month

Best Practices to Improve SQL Performance
By P. V. Neir, Reprinted from Articles.Freemegazone
BOOKMARK: http://www.quest-pipelines.com/tiptrack.asp?id=20711

Being developers we often see clients complaining about database performance. Performance problems arise when we ignore good practices. In this article I briefly list some of the best practices one should follow while working with SQL.

  1. The database design should be simple and clear. It should be user friendly. It should have flexible structure so that depending on the future requirements the database can easily be expanded.
     
  2. Store relevant and necessary information in the database.
     
  3. Use normalized tables in the database. It is better to use small multiple tables instead of using one large table.
     
  4. Use denormalization as the last option for your database.
     
  5. Use database look up tables. It will help you maintain data integrity of your database.
     
  6. Use numeric or small width primary key for your database tables. Processing small data takes less time.
     
  7. Do not store images in the database. Store URLs of images instead.
     
  8. Do not display SQL errors to users. Most of the attackers get benefit of error messages.
     
  9. Make an ERROR table in the database. Storing errors in the ERROR table will help you keep track of bugs.
     
  10. Assign proper data types to the fields in the database. It is better to use TINYINT for age rather than VARCHAR (45).
     
  11. Use database field names in SELECT, INSERT, UPDATE commands. Avoid using SELECT * or INSERT INTO TABLE VALUES (value1, value2...).
     
  12. Use LIKE clause properly. Improper usage may cause performance problems in your database.
     
  13. Use database server's timestamp instead of web server's timestamp.
     
  14. Use alias in the SQL statements which require multiple tables.
     
  15. Write SQL commands in capital letters. It helps in readability.
     
  16. Use nested queries as minimum as possible. Use JOIN statement instead.
     
  17. Avoid query running in the loop.
     
  18. Use stored procedures. They are fast and help in maintainability and security of the database.
     
  19. Avoid CURSOR, GOTO and IF statements. Prefer SWITCH CASES.
     
  20. Use comments.
     
  21. Do proper indexing. It will improve the speed of operations in the database tables. Improper or too much indexing affects performance of the database.
     
  22. Use inet_aton() function to store IPs as integers in the database. The inet_aton() function converts the string, in the Internet standard dot notation, to a network address, and stores the address in the structure provided.
     
  23. Retrieve the IPs with inet_ntoa() function. The inet_ntoa() function converts the specified Internet host address to a string in the Internet standard dot notation.
     
  24. Always encrypt the sensitive data.
     
  25. Do not rely on server configurations. Always write the most portable code possible.
     
  26. Do proper documentation of your application.
     
  27. Always do testing of your database application. Make cases based on requirements and maintain a checklist of the tests.


Decemer's Tip of the Month

Mixed Parameter Placement in Oracle 11g Functions
By Arup Nanda, for Knowledge Xpert for PL/SQL
BOOKMARK: http://www.quest-pipelines.com/tiptrack.asp?id=20712

Here is a simple function to illustrate the concept:

CREATE OR REPLACE FUNCTION do_something (p_in_1 NUMBER, p_in_2 NUMBER)
   RETURN NUMBER
IS
BEGIN
   RETURN p_in_1 + p_in_2;
END;
/

Since there are two parameters, you can either call the function by passing parameters as positional values as shown below:

BEGIN
   do_something (1, 2);
END;

Or, as named parameters:

BEGIN
   do_something ( p_in_1 => 1, p_in_2 => 2);
END;

However, prior to Oracle Database 11g, the latter had issues if used in select statements. In Oracle Database 10g and below, if you issue the following statement:

SELECT do_something (p_in_1 => 1, p_in_2 => 1)
  FROM DUAL;

You will get the error:

select do_something (p_in_1=>1,p_in_2=>1) from dual
                       *
ERROR at line 1:
ORA-00907: missing right parenthesis

In Oracle Database 11g, you are free to use this notation.

SQL> select do_something (p_in_1=>1,p_in_2=>1) from dual;
 
DO_SOMETHING(P_IN_1=>1,P_IN_2=>1)
-------------------------------
                              2
 
1 row selected.

This works perfectly. You can specify the named notation towards the end; the first ones must be positional. For instance, the following will be valid, where the parameter p_in_1 is set to 1:

SELECT do_something (1, p_in_2 => 2)
  FROM DUAL

But this one will not be (the positional parameter is at the end):

SQL> SELECT do_something (p_in_1=>1, 2)
  FROM DUAL;

select do_something (p_in_1=>1, 2) from dual
       *
ERROR at line 1:
ORA-06553: PLS-312: a positional parameter association may not follow a named association


Oracle Pipeline

For questions or comments on this site: webmaster@quest-pipelines.com
All content Copyright © Quest Software, Inc. All rights reserved.