Visit the Quest Software Home Page. Pipelines Home

Please Note: Unless otherwise noted, all of the PL/SQL Pipeline's monthly tips are contributed by Steven Feuerstein.

PL/SQL Pipeline  Oracle PL/SQL Tips of the Month - 2001
 
January ROLLUP and CUBE Features of SELECT
February Build a Function to Provide a DBMS_OUTPUT Trace
March Differing Results when running PL/SQL Code in an Anonymous Block vs Procedure
April Improving Performance with FORALL
May Use of the New CASE Function
June Use of the New NVL2 Function
July Loading a Lookup Table in Memory to Speed Up Queries
August A Clever ORDER BY
September Designing a Two-Level Tab Canvas Form
October Bulk Collecting Multiple Rows
November Truncating a Table from a Remote Database
December Database Triggers Containing :NEW and :OLD

2000 Tips
1999 Tips
1998 Tips
1997 Tips


January's Tip of the Month

ROLLUP and CUBE Features of SELECT
Compliments of Fawwad-uz-Zafar Siddiqi (fuzs_98@yahoo.com).

New with Oracle 8i, the ROLLUP and CUBE features greatly simplify SQL coding by eliminating the need to design your own UNION statements. The examples below demonstrate this:

A listing of the rows in the EMP table of Oracle's demo account, SCOTT:

select deptno, empno, ename, job, sal
from emp order by deptno, empno; 

DEPTNO  EMPNO  ENAME  JOB        SAL 
------  -----  -----  ----       ---- 
    10   7782  CLARK  MANAGER    2450 
    10   7839  KING   PRESIDENT  5000 
    10   7934  MILLER CLERK      1300 
    20   7369  SMITH  CLERK       800 
    20   7566  JONES  MANAGER    2975 
    20   7788  SCOTT  ANALYST    3000 
    20   7876  ADAMS  CLERK      1100 
    20   7902  FORD   ANALYST    3000 
    30   7499  ALLEN  SALESMAN   1600 
    30   7521  WARD   SALESMAN   1250 
    30   7654  MARTIN SALESMAN   1250 
    30   7698  BLAKE  MANAGER    2850 
    30   7844  TURNER SALESMAN   1500 
    30   7900  JAMES  CLERK       950 
 
14 rows selected.
If we wish to find the number of employees by department, we issue the query:
select deptno, count(*)
from emp
group by deptno;

DEPTNO  COUNT(*)
------  --------
    10         3
    20         5
    30         6
If we also wish to include the total number of employees in the output, we need to include the UNION clause, as shown here:
select deptno, count(*)
from emp
group by deptno
union
select to_number(null), count(*)
from emp;

DEPTNO  COUNT(*)
------  --------
    10         3
    20         5
    30         6
              14
As of Oracle8i, we can use the ROLLUP option to achieve the same result:
select deptno, count(*)
from emp
group by rollup(deptno); 

DEPTNO  COUNT(*)
------  --------
    10         3
    20         5
    30         6
              14
By simply invoking the ROLLUP facility in the GROUP BY clause, we direct Oracle to summarize the data at levels above the columns specified, all the way to the grand total.

Note that Oracle reports the grand total by leaving the GROUP BY column blank. If the GROUP BY column also contains null values, it might be difficult to distinguish those values from the grand total row. Fortunately, we can use a special function called GROUPING to report the current summarization level. The function returns two values: 0 indicates the current row is grouped at the specified GROUP BY level, and 1 indicates the row is grouped at a higher level.

select deptno, count(*), grouping(deptno)
from emp
group by rollup(deptno);

DEPTNO  COUNT(*)  GROUPING(DEPTNO) 
------  --------  ---------------- 
    10         3                 0 
    20         5                 0 
    30         6                 0 
              14                 1
Now we can take advantage of the GROUPING function to format the output:
select decode(grouping(deptno),0,to_char(deptno),'Total')  deptno, count(*)
from emp
group by rollup(deptno);

DEPTNO  COUNT(*) 
------  -------- 
    10         3 
    20         5 
    30         6 
 Total        14
The ROLLUP feature can in fact be applied to multiple columns. The result is multiple levels of rollup, as illustrated here:
select deptno, job, count(*), grouping(deptno), grouping(job)
from emp
group by rollup(deptno, job);

DEPTNO  JOB        COUNT(*)  GROUPING(DEPTNO)  GROUPING(JOB) 
------  ----       --------  ----------------  ------------- 
    10  CLERK             1                 0              0 
    10  MANAGER           1                 0              0 
    10  PRESIDENT         1                 0              0 
    10                    3                 0              1
    20  ANALYST           2                 0              0
    20  CLERK             2                 0              0
    20  MANAGER           1                 0              0
    20                    5                 0              1
    30  CLERK             1                 0              0 
    30  MANAGER           1                 0              0 
    30  SALESMAN          4                 0              0 
    30                    6                 0              1
                         14                 1              1
As shown in this example, we're able to count the employees by 1) department and job; 2) department; and 3) grand total.

Similar to ROLLUP is the CUBE function. CUBE groups data at multiple dimensions. In particular, it summarizes data based on all possible combinations of the columns specified in the GROUP BY clause. Let's see how this works:

select deptno, job, count(*), grouping(deptno), grouping(job)
from emp
group by cube(deptno, job);

DEPTNO JOB       COUNT(*) GROUPING(DEPTNO) GROUPING(JOB) 
------ ----      -------- ---------------- ------------- 
    10 CLERK            1                0             0 
    10 MANAGER          1                0             0 
    10 PRESIDENT        1                0             0 
    10                  3                0             1 
    20 ANALYST          2                0             0 
    20 CLERK            2                0             0 
    20 MANAGER          1                0             0 
    20                  5                0             1 
    30 CLERK            1                0             0 
    30 MANAGER          1                0             0 
    30 SALESMAN         4                0             0 
    30                  6                0             1 
       ANALYST          2                1             0 
       CLERK            4                1             0 
       MANAGER          3                1             0 
       PRESIDENT        1                1             0 
       SALESMAN         4                1             0 
                       14                1             1
The CUBE function includes the output generated by ROLLUP. In addition, it includes the number of employees by job - independent of the department number.  


February's Tip of the Month

Build a Function to Provide a DBMS_OUTPUT Trace

Build a function that can be called from within a SQL statement that provides a DBMS_OUTPUT trace of each row returned by a query, showing the table name and the ROWID.

The following implementation of this trace function is really just a “pass-through” to DBMS_OUTPUT.PUT_LINE:

CREATE OR REPLACE FUNCTION traceit (tab IN VARCHAR2, rowid_in IN ROWID)
   RETURN INTEGER
IS
   PROCEDURE showinfo
   IS
   BEGIN
      DBMS_OUTPUT.put_line (tab || ‘-‘ || ROWIDTOCHAR (rowid_in));
   END;
BEGIN
   showinfo;
   RETURN 0;
EXCEPTION
   WHEN OTHERS
   THEN
      DBMS_OUTPUT.enable (1000000);
      showinfo;
END;
/
I cannot call DBMS_OUTPUT directly in SQL, so I put it inside a function that always returns 0, but in the process put information into the DBMS_OUTPUT buffer. I also add an exception section to trap and automatically correct for an error that might arise from the DBMS_OUTPUT buffer's being too small.

Note that if you call this function in an SQL statement that is executed outside of a PL/SQL block, you will not see any output UNTIL you execute a PL/SQL block. That is because SQL*Plus (and other execution environments for PL/SQL) does not automatically "dump" the DBMS_OUTPUT buffer when a SQL statement completes, only when a PL/SQL block terminates.


March's Tip of the Month

Differing Results when running PL/SQL Code in an Anonymous Block vs Procedure
Overheard on the PL/SQL Pipeline...

Have you ever run code in an anonymous block and then used the identical code in a stored procedure and gotten different results? The problem may be due to the fact that when running a stored procedure, it runs as the owner, not the caller. It may also be that objects referenced in the code have had privileges granted via a role rather than direct grants. Stored procedure and functions require all database objects being "granted" explicitly to the stored object owner.


April's Tip of the Month

Improving Performance with FORALL
Compliments of Jeff Thomas (jsthomas@ixl.com)

I was recently experimenting with an Oracle 8.1.6 feature called "Bulk Binding", and was amazed at the performance gain. Any developer who is not aware of this capability is really missing out. I packaged my little test, and here is the result. Of course, your results will vary, but the point should be clear enough. The script and the output are shown for both the normal PL/SQL method and the FORALL method.

SQL Script

set echo on
set feedback on
spool forall

/*  
 A simple timing test of the FORALL bulk bind. It is WAY faster.
 To illustrate, we load 100,000 records from a plsql table into a 
 database table.
 This is done 2 ways, as seen below. Be sure to check the timings...
 
 DDL for the example table:
 drop table test1;
 create table test1 (testnum number);
*/  
EXAMPLE 1: REGULAR PL/SQL WAY, NOT USING A BULK BIND
set timing on

DECLARE
type t1_type is table of test1.testnum%type index by binary_integer;
t1_tab t1_type;
t1_idx binary_integer := 0;

BEGIN

 -- load the plsql table
FOR n in 1 .. 100000
LOOP
  t1_tab(n) := n;
  t1_idx := t1_idx + 1;
END LOOP;

 -- load the db table
FOR n in 1 .. t1_idx
LOOP
  insert into test1 (testnum) values (t1_tab(n));
END LOOP;

END;
/

set timing off
select count(*) from test1;
truncate table test1 reuse storage;
set timing on
EXAMPLE 2: USING FORALL
DECLARE
type t1_type is table of test1.testnum%type index by binary_integer;
t1_tab t1_type;
t1_idx binary_integer := 0;

BEGIN

 -- load the plsql table
FOR n in 1 .. 100000
LOOP
  t1_tab(n) := n;
  t1_idx := t1_idx + 1;
END LOOP;

FORALL n in 1 .. t1_idx
  insert into test1 (testnum) values (t1_tab(n));

END;
/

set timing off
select count(*) from test1;
truncate table test1 reuse storage;

spool off
Output

EXAMPLE 1: REGULAR PL/SQL WAY, NOT USING A BULK BIND

set timing on

DECLARE
  2  type t1_type is table of test1.testnum%type index by binary_integer;
  3  t1_tab t1_type;
  4  t1_idx binary_integer := 0;
  5  
  6  BEGIN
  7  
  8   -- load the plsql table
  9  FOR n in 1 .. 100000
 10  LOOP
 11    t1_tab(n) := n;
 12    t1_idx := t1_idx + 1;
 13  END LOOP;
 14  
 15   -- load the db table
 16  FOR n in 1 .. t1_idx
 17  LOOP
 18    insert into test1 (testnum) values (t1_tab(n));
 19  END LOOP;
 20  
 21  END;
 22  /

PL/SQL procedure successfully completed.

Elapsed: 00:00:25.97

set timing off
select count(*) from test1;

COUNT(*)
--------
  100000

1 row selected.

truncate table test1 reuse storage;

Table truncated.

set timing on
EXAMPLE 2: USING FORALL
DECLARE
  2  type t1_type is table of test1.testnum%type index by binary_integer;
  3  t1_tab t1_type;
  4  t1_idx binary_integer := 0;
  5  
  6  BEGIN
  7  
  8   -- load the plsql table
  9  FOR n in 1 .. 100000
 10  LOOP
 11    t1_tab(n) := n;
 12    t1_idx := t1_idx + 1;
 13  END LOOP;
 14  
 15  FORALL n in 1 .. t1_idx
 16    insert into test1 (testnum) values (t1_tab(n));
 17  
 18  END;
 19  /

PL/SQL procedure successfully completed.

Elapsed: 00:00:01.73

set timing off
select count(*) from test1;

COUNT(*)
--------
  100000

1 row selected.

truncate table test1 reuse storage;

Table truncated.


spool off

May's Tip of the Month

Use of the New CASE Function
Compliments of Andrew Simkovsky (asimkovsky@msn.com)

Oracle version 8.1.6 provided the Oracle community with a wealth of new features. One of which is the new searched CASE function. This feature works very similar to the DECODE function, but surpasses its ability tenfold. It is much more flexible and often performs much better. The classic example offered by Oracle documentation gives this statement:

SELECT AVG(foo(e.sal)) FROM emps e;
In this statement FOO is a function that will return its input if the input is greater than 2000, but will return 2000 otherwise. This implementation requires the use of PL/SQL code, which takes much longer to write and will probably run much slower. With the CASE functionality built into the RDBMS, the logic can be built straight into the SQL statement quickly and easily as shown:
SELECT AVG   (CASE
		WHEN e.sal > 2000 
		THEN e.sal
		ELSE 2000
              END)
FROM emps e;
The CASE function can be nested, extending the conditional logic even further. But what if you wanted to use this type of statement from within PL/SQL? Being on the bleeding edge, this functionality has not yet been implemented within the PL/SQL engine. For example, if you tried this from within PL/SQL you would receive an error:
DECLARE 
vNum number; 
begin 
select sum(case
	     when e.sal>2000 
	     then e.sal 
	     else 2000
	   end) 
into vNum from emps e; 
end;
The error you would receive is “PLS-00103: Encountered the symbol "CASE" when expecting…”. This is because the PL/SQL engine does not yet recognize the CASE function. The work-around to this would be to assign the entire statement to a variable, and execute the statement using the EXECUTE IMMEDIATE command:
DECLARE
sqlStr varchar2(500);
vNum number;
begin
sqlStr:= 'select avg(case '||
      'when e.sal>2000 '||
      'then e.sal '||
      'else 2000 '||
      'end) '||
             'from emps e';
EXECUTE IMMEDIATE sqlStr INTO vNum;
DBMS_OUTPUT.PUT_LINE('Average salary: '||vNum)
end;
Assigning the statement to a variable, then executing it with the EXECUTE IMMEDIATE command will avoid the error from the PL/SQL engine.


June's Tip of the Month

Use of the New NVL2 Function
Compliments of Adinath Kamode (adi_kamode@usa.net)

Most of us are familiar with the NVL function which checks for the existence of NULL values. Oracle provides a new function called NVL2 which checks for the existence of NOT NULL. The syntax for this function is as follows.

NVL2(expr1,expr2,expr3);
If expr1 is not null then the function will return expr2. Otherwise, the function will return expr3. The expr1 can have any datatype and arguments expr2 and expr3 can be of any datatype other than LONG. The datatype of the return value is that of expr2.

For example:

SELECT  EMPNO,ENAME,SAL,COMM,NVL2(COMM,1,0) COMMPRE
FROM EMP
ORDER BY EMPNO;

EMPNO    ENAME      SAL        COMM     COMMPRE 
-------- ---------- ---------- -------- ----------- 
    7499 ALLEN            5000      300           1 
    7521 WARD           3906.5      500           1 
    7566 JONES            9297                    0 
    7654 MARTIN         3906.5     1400           1 
    7698 BLAKE          8906.5                    0 
    7782 CLARK          7656.5                    0 
    7788 SCOTT            9375                    0 
    7839 KING            15625                    0 
    7844 TURNER         4687.5                    0 
    7876 ADAMS          3437.5                    0 
    7900 JAMES            2969                    0 
    7902 FORD             9375                    0 
    7934 MILLER         4062.5                    0
The above query checks for the presence of NOT NULL in the COMM field, and returns 1 where COMM is NOT NULL and 0 where it is NULL.

Here are the differences between the NVL and NVL2 functions:

  1. NVL takes 2 arguments while NVL2 takes 3.
  2. NVL returns the first argument if the first argument is not null, whereas NVL2 returns the second argument if the first argument is not null and returns the third argument if the first argument is null.
  3. In NVL, the datatype of the return is that of the first argument, in NVL2 the datatype of the return is that of the second argument.

Dan Clamage, Pipeline SYSOP, had the following to add:

DECODE can also detect NULL/NOT NULL, but it must return a value of the same data type as the expression (at least in earlier database versions - in 8i it seems to be more flexible). Of course, DECODE can only be used in SQL.

select
DECODE('ha',
null, 'ho',
'he')
from dual;
returns 'he'.

select
DECODE(to_char(null),
null,'ho',
'he')
from dual;
returns 'ho'.

declare
b varchar2(10);
c date;
begin
select
DECODE(c,
null,'ha',
'ho') into b
from dual;
dbms_output.put_line(b);
end;
/
returns 'ha'

July's Tip of the Month

Loading a Lookup Table in Memory to Speed Up Queries
Compliments of Dan Clamage, PL/SQL Pipeline SYSOP (danielj@clamage.com)

Recently, I worked with a developer who faced the challenge of speeding up his Pro*C application. It was taking about 2 hours to run. After examining his application, I discovered that he had one table on which every row returned in the driving loop required a random lookup. Furthermore, he could expect nearly every row in this lookup table to be accessed at some point over the life of the program. I instructed him to preload this moderately sized table (~50k rows) into a persistent PL/SQL table. In order to be persistent, the PL/SQL table had to be declared in the package body, outside of any routine:

CREATE OR REPLACE
PACKAGE BODY lookup
IS
TYPE typ_val_data IS TABLE OF tbl_lookup.val_data%TYPE
  INDEX BY BINARY_INTEGER;
tab_val_data typ_val_data;
I explained to him that the numeric primary key could be used as the index offset, and the column value he needed could then be stored at that offset. For example:

Lookup Table

OID NUMBER(10)  VAL_DATA VARCHAR2(4)
3               'GHGL'
27              'ACBF'
101             'LEFP'
157             'KPOI'
PL/SQL Table (also known as index-by table or array)
Array Offset    Array Value
3               'GHGL'
**no data in array elements 4-26**
27              'ACBF'
**no data in array elements 28-100**
101             'LEFP'
**no data in array elements 158+**
157             'KPOI'
This would result in a sparsely populated array. He populated the array up front, as part of his program initialization, using a packaged procedure. This procedure simply read every row in his lookup table in a cursor FOR loop, and used the primary key as the element offset into the PL/SQL table. For example:
PROCEDURE populate_lookup
IS
  CURSOR get_all_data IS
  SELECT oid, val_data
  FROM tbl_lookup;
BEGIN
  FOR rec IN get_all_data LOOP
    tab_val_data(rec.oid) := rec.val_data;
  END LOOP;
END populate_lookup;
Then he provided a packaged function to interface with this array. He could use this function in PL/SQL or in SQL. For example:
FUNCTION get_val_data(p_oid IN tbl_lookup.oid%TYPE)
RETURN tbl_lookup.val_data%TYPE
IS
BEGIN
  RETURN(tab_val_data(p_oid));
EXCEPTION
  WHEN NO_DATA_FOUND THEN
  RETURN(NULL);
END get_val_data;
Inside his main loop, he coded something like this:
INSERT INTO tbl_target (val_data, ...)
VALUES (lookup.get_val_data(fk_oid), ...);
He had to fully qualify the function, since it's being called from SQL. He reported that his program now runs in 20 minutes, a 5x speed improvement.


August's Tip of the Month

A Clever ORDER BY
Compliments of Jeffrey Pearse, Middlesex University (j.pearse@mdx.ac.uk)

This script returns the most recently created row in a table followed by an alphabetical listing of the remaining rows in the table.

For this task I have created a table named TEST with three columns: id, alpha and created_when. The small amount of data held in this table is as follows:

ID 	ALPHA CREATED_WHEN
1  	A     	10-JUL-99
2  	B     	02-SEP-99
3  	C     	18-AUG-99
4  	D     	08-SEP-99
5  	S     	10-JUL-99
6  	N     	13-AUG-99
In order to select the most recent row data followed by alpha records I used the following select:
--

 1  COLUMN 1 NOPRINT
 2  --
 3  select id
 4          ,alpha
 5          ,created_when
 6          ,1
 7  from  test
 8  where created_when = (select max(created_when)
 9                                        from test)
10  union all
11  select id
12          ,alpha
13          ,created_when
14          ,2
15  from  test
16  where created_when <> (select max(created_when)
17                                          from test)
18  order by 4
19*         ,alpha
Explanation:

Line 1: COLUMN 1 NOPRINT
This ensures that column 4 is not displayed as it is only used to order the results returned from each part of the UNION ALL. (UNION ALL is preferable to UNION because UNION returns distinct rows selected by both queries while UNION ALL returns all the rows. Therefore, if the table has duplicates, UNION will remove them, which is not what we want. If the table has no duplicates, UNION will force a sort and cause performance degradation as compared to UNION ALL.)

Line 6: select 1
This ensures that this part of the UNION ALL is returned first as the final order by is by column 4.

Line 14: select 2
This ensures that this part of the UNION ALL is returned second as the final order by is by column 4 followed by the alpha column.

The key to this is that the order by ensures that the max date, which is returned from the upper select in the UNION ALL, selects the numeric value 1 and is returned first followed by the ordering on the column alpha. The column 1 noprint ensures that the fourth column is not selected, as these values are only useful for the order by clause.

The script returns the following output with the most recent row followed by the others ordered by the column alpha.

        ID ALPHA CREATED_WHEN
---------- ----- ------------
         4 D     08-SEP-99
         1 A     10-JUL-99
         2 B     02-SEP-99
         3 C     18-AUG-99
         6 N     13-AUG-99
         5 S     10-JUL-99      


September's Tip of the Month

Designing a Two-Level Tab Canvas Form
Compliments of Alexey Gorbenko, (avg@webmercs.com)

In my work, I use Oracle Developer/2000 v6.0. and very often I have to use Forms with multi-level tab-canvases. Unfortunately, however, Forms doesn’t allow the inclusion of a tab-canvas in the tab-page of another tab-canvas. Here is a way to work around this.

Suppose that we need to design a two-level tab-canvas form:

  1. Create the content canvas (e.g. MAIN)

  2. Create the tab-canvas of the first level (e.g. T_GEN, Note: For this example, let the names of tab-canvases and the names of tab-pages which will be included in sub-tab-canvases start with the letter T and names of tab-pages which will be included in stacked canvases with information start with the letter C)

  3. Minimize the height of the canvas so that you only see the tabs.

  4. Create as many tab-pages as you need. Rename them (e.g. T_DEP_EMP, T_CUST_ORD_IT…).

  5. Create the second level tab-canvases. Their names must be equal to the names of their corresponding tab-pages of the first-level tab canvas (in our case: T_DEP_EMP, T_CUST_ORD_IT).

  6. Also minimize the height of canvases to only see the tabs. Set their Y positions so that you can see them under the first-level tab canvas.

  7. Create tab-pages in the second level tab canvases. Rename them (e.g. C_DEP, C_EMP, C_CUST, C_ORDER, C_ITEMS).

  8. Create the stacked canvases (information canvases). Their names must be equal to the names of their corresponding tab-pages of the second-level tab canvases (in our case: C_DEP, C_EMP, C_CUST, C_ORDER, C_ITEMS). Set the Y positions so that you can see them under the second-level tab canvas.

  9. Create the data blocks (e.g. EMP, DEP, CUSTOMER, ORD, ITEMS) and place the information into the corresponding stacked-canvases.

  10. Add two form-level triggers:
    WHEN-NEW-FORM-INSTANCE
    
        /* initial query */
    
                   go_block('emp');
    
                   execute_query;
    
                   go_block('customer');
    
                   execute_query;
    
                   go_block('ord');
    
                   execute_query;
    
                   go_block('item');
    
                   execute_query;
    
                   go_block('dept');
    
                   execute_query;
    
                   /* to show start canvases */
    
                   show_view('t_dep_emp');
    
                   show_view('c_dep');
    
                   :global.cnv:='C_DEP';
    
       
    WHEN-TAB-PAGE-CHANGED
    
     hide_view(:global.cnv);
    
     :global.cnv:=:SYSTEM.TAB_NEW_PAGE;
    
     if substr(:global.cnv,1,1)='T'  then 
    
     show_view(:global.cnv);
    
     :global.cnv:=GET_CANVAS_PROPERTY(:global.cnv,TOPMOST_TAB_PAGE);
    
     end if; 
    
     show_view(:global.cnv); 

  11. The form is ready!
This method may also be used for creating tab canvas forms with many levels.


October's Tip of the Month

Bulk Collecting Multiple Rows
Compliments of Dan Clamage, PL/SQL Pipeline SYSOP (danielj@clamage.com)

I needed to perform a query that bulk collected multiple rows for all the keys stored in another collection. But you can't use the SELECT..BULK COLLECT statement in a FORALL statement, and using an array in a SELECT's WHERE clause is unsupported. So I faked it out by updating a non-indexed column to itself. The only alternative would have been to load the collection one key at a time in a loop.

In this example, I needed to collect the old indicator values for a set of primary keys before updating them to new values. Inputs are two strings representing comma-delimited lists of values. The parse_list routine parses out these values and loads them into a collection. Cold Fusion does not support Oracle's Collection datatype, but it does have a routine for quickly building delimited list strings. I have removed all of the error handling and input validation for readability.

-- Updates xref_brand promo verification indicator
-- according to the new ind list, (a comma-delimited 
-- string: 0=unverified, 1=verified) for each primary 
-- key in the xref brand oid list (a comma-delimited string).
-- Both comma-delimited lists are treated as parallel arrays.
-- Track changes in History table.
PROCEDURE update_xref_brand(
  p_new_ind_list        IN VARCHAR2
 ,p_xref_brand_oid_list IN VARCHAR2)
IS
  c_delim CONSTANT CHAR(1) := ',';
  -- Array used to capture OID's stored in a comma-delimited list
  -- declared as: TYPE ARR_STR IS TABLE OF VARCHAR2(255) INDEX BY
  -- BINARY_INTEGER.
  -- Primary keys for table xref_brand.
  -- New indicator values ('0'=unverified,'1'=verified).
  -- Capture old indicator values before update.
  a_xref_brand_oid utl.ARR_STR;  
  a_new_ind        utl.ARR_STR;  
  a_old_ind        utl.ARR_STR;  
BEGIN
  -- Parse out comma-delimited values from each string
  -- and store them in parallel collections.
  utl.parse_list(p_new_ind_list, c_delim, a_new_ind);
  utl.parse_list(p_xref_brand_oid_list, c_delim, a_xref_brand_oid);

  -- Capture old value of indicator code before doing the update.
  -- You cannot use the SELECT..BULK COLLECT 
  -- statement in a FORALL statement
  -- and array in SELECT WHERE clause is unsupported.
  -- So I faked it out to get the old value by updating 
  -- column to itself.
  FORALL i IN a_xref_brand_oid.FIRST..a_xref_brand_oid.LAST
    UPDATE xref_brand
    SET promo_verification_ind = promo_verification_ind
    WHERE xref_brand_oid = a_xref_brand_oid(i)
    RETURNING promo_verification_ind
    BULK COLLECT INTO a_old_ind;

  -- Apply the indicator codes to xref brand rows 
  -- identified by primary key.
  -- The primary key is implicitly converted from varchar2->number.
  FORALL i IN a_xref_brand_oid.FIRST..a_xref_brand_oid.LAST
    UPDATE xref_brand
    SET promo_verification_ind = a_new_ind(i)
    WHERE xref_brand_oid = a_xref_brand_oid(i);

  -- Track verification (shortened here).
  FORALL i IN a_xref_brand_oid.FIRST..a_xref_brand_oid.LAST
    INSERT INTO hist_xref_brand_update (
      hist_xref_brand_update_oid
     ,xref_brand_oid
     ,original_value
     ,new_value
    ) VALUES (
      hist_xref_brand_update_oid.NEXTVAL
     ,a_xref_brand_oid(i)
     ,a_old_ind(i)
     ,a_new_ind(i)
    );
END update_xref_brand;;
/

November's Tip of the Month

Truncating a Table from a Remote Database
Compliments of Giovanni Jaramillo, (gjaram@hotmail.com)

Recently I was trying to process data from one table and insert it into another. Prior to the insert, I wanted to truncate the receiving table. In the past I had been able to truncate a table under the same schema, or under a different schema but on the same database. This time I had to truncate a table on a remote database. So I tried the usual truncate and received the following error:

ORA-02021: DDL operations are not allowed on a remote database.
I had a database link to this remote database so I could see objects there and execute them (e.g. procedures, functions, packages, triggers, etc). So the solution was to create the following procedure on the remote database, then execute it from the local one.
CREATE OR REPLACE PROCEDURE 
Truncate_Remote_Table(p_table_name VARCHAR2) AS

/*
   Procedure Name: Truncate_Remote_Table

   Purpose:To truncate a table on a local database from a remote
           database.  This procedure is executed remotely via a dblink
           and passed in the table name that exists on the local
           database.

   Developer Name:   Giovanni Jaramillo
   Developer Email:  gjaram@hotmail.com

*/

   -- VARIABLES

   v_sql_error_code PLS_INTEGER;
   v_sql_error_message VARCHAR2(512);--Maximum SQL error message size:512

BEGIN

   EXECUTE IMMEDIATE 'TRUNCATE TABLE ' || p_table_name;

EXCEPTION

   WHEN OTHERS THEN

      v_sql_error_code := SQLCODE;
      v_sql_error_message := SQLERRM(v_sql_error_code);

      DBMS_OUTPUT.ENABLE(5000);
      DBMS_OUTPUT.PUT_LINE('OTHER ERROR');
      DBMS_OUTPUT.PUT_LINE(v_sql_error_message);

END Truncate_Remote_Table;
To execute the procedure, use the following from the local database:
BEGIN 

   Truncate_Remote_Table@db_link(‘remote_table_name’); 

END;

December's Tip of the Month

Database Triggers Containing :NEW and :OLD
Compliments of Zlatko Sirotic, (zlatko.sirotic@iii.hr)

We often want to create and execute 'generic' dynamic statements in database triggers containing :NEW, referring to new values of current records, or :OLD, referring to old values of current records. In execution time, however, :OLD and :NEW are flagged as bad bind variables.

One workaround is to use AFTER STATEMENT triggers in the following manner:

  1. Create a database package with a PL/SQL table for storing ROWID values
     
  2. Clear the PL/SQL table in the BEFORE STATEMENT trigger
     
  3. Populate the PL/SQL table with ROWID values in the BEFORE (or AFTER) ROW trigger
     
  4. Read the rows in the AFTER STATEMENT trigger with dynamic PL/SQL, using ROWIDs from the PL/SQL table and using dynamically selected column names with this statement:
    SELECT column_name
     FROM user_tab_columns
      WHERE UPPER (table_name) = UPPER (p_table);
In this way, you can read :NEW values of columns and, with AUTONOMOUS_TRANSACTION, :OLD values as well.

Example (scott.dept table):

  1. DATABASE TRIGGERS (UPDATE)
    CREATE OR REPLACE TRIGGER bus_dept
    BEFORE UPDATE ON dept
    BEGIN
       plsql_table.clear;
    END;
    /
     
    CREATE OR REPLACE TRIGGER bur_dept
    BEFORE UPDATE ON dept
    FOR EACH ROW
    BEGIN
       plsql_table.populate_with_rowid (:OLD.ROWID);
    END;
    /
     
    CREATE OR REPLACE TRIGGER aus_dept
    AFTER UPDATE ON dept
    DECLARE
       v_current_rowid ROWID;
    BEGIN
       dynamic_new_old.set_table_name ('dept');
       dynamic_new_old.create_column_names;
     
       WHILE plsql_table.rowid_exists LOOP
          v_current_rowid := plsql_table.current_rowid;
     
          DBMS_OUTPUT.PUT_LINE ('OLD VALUES:');
          dynamic_new_old.display_old_values (v_current_rowid);
     
          DBMS_OUTPUT.PUT_LINE ('NEW VALUES:');
          dynamic_new_old.display_new_values (v_current_rowid);
     
          DBMS_OUTPUT.PUT_LINE ('*****');
       END LOOP;
    END;
    /
  2. PACKAGES
    CREATE OR REPLACE PACKAGE plsql_table IS
       PROCEDURE clear;
       PROCEDURE populate_with_rowid (p_rowid ROWID);
       FUNCTION rowid_exists RETURN BOOLEAN;
       FUNCTION current_rowid RETURN ROWID;
    END;
    /
     
    CREATE OR REPLACE PACKAGE BODY plsql_table IS
       TYPE type_plsql_table IS TABLE OF ROWID INDEX BY BINARY_INTEGER;
       m_plsql_table type_plsql_table;
     
       m_rec_number BINARY_INTEGER;
     
       PROCEDURE clear IS
       BEGIN
          m_rec_number := 0;
       END;
     
       PROCEDURE populate_with_rowid (p_rowid ROWID) IS
       BEGIN
          m_rec_number := m_rec_number + 1;
          m_plsql_table (m_rec_number) := p_rowid;
       END;
     
       FUNCTION rowid_exists RETURN BOOLEAN IS
       BEGIN
          RETURN (m_rec_number > 0);
       END;
     
       FUNCTION current_rowid RETURN ROWID IS
          v_rowid VARCHAR2 (18);
       BEGIN
          v_rowid := m_plsql_table (m_rec_number);
          m_rec_number := m_rec_number - 1;
          RETURN v_rowid;
       END;
    END;
    /
     
    CREATE OR REPLACE PACKAGE dynamic_new_old IS
       PROCEDURE set_table_name (p_table VARCHAR2);
       PROCEDURE create_column_names;
       PROCEDURE display_old_values (p_rowid ROWID);
       PROCEDURE display_new_values (p_rowid ROWID);
    END;
    /
     
    CREATE OR REPLACE PACKAGE BODY dynamic_new_old IS
       m_table VARCHAR2 (30);
       m_columns VARCHAR2 (32000);
     
       PROCEDURE set_table_name (p_table VARCHAR2) IS
       BEGIN
          m_table := UPPER (p_table);
       END;
     
       PROCEDURE create_column_names IS
          v_first_column BOOLEAN;
       BEGIN
          v_first_column := TRUE;
          FOR rec IN
             (SELECT column_name
                FROM user_tab_columns
               WHERE table_name = m_table)
          LOOP
             IF v_first_column THEN
                v_first_column := FALSE;
                m_columns := 'v_record.' || rec.column_name;
             ELSE
                m_columns := m_columns ||
                   '||' || '''--''' || '|| v_record.' || rec.column_name;
             END IF;
          END LOOP;
       END;
     
       PROCEDURE display_values (p_rowid ROWID) IS
          v_statement VARCHAR2 (32000);
       BEGIN
          v_statement :=
          ' DECLARE '  ||
          '    v_record ' || m_table || '%ROWTYPE;' ||
          ' BEGIN' ||
          '    SELECT * INTO v_record' ||
          '      FROM ' || m_table ||
          '     WHERE ROWID = :rowid_p;' ||
          '    DBMS_OUTPUT.PUT_LINE (' || m_columns || ');' ||
          ' END;';
     
          EXECUTE IMMEDIATE v_statement USING p_rowid;
       END;
     
       PROCEDURE display_old_values (p_rowid ROWID) IS
          PRAGMA AUTONOMOUS_TRANSACTION;
       BEGIN
          display_values (p_rowid);
       END;
     
       PROCEDURE display_new_values (p_rowid ROWID) IS
       BEGIN
          display_values (p_rowid);
       END;
    END;
    /
If you need :OLD values only, you can write a simpler solution (without statement triggers and 'plsql_table' package), using 'dynamic_new_old' package and AUTONOMOUS_TRANSACTION in the BEFORE (or AFTER) ROW trigger.

 


PL/SQL Pipeline

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