(21) Object Dependencies


  • If you alter the definition of a referenced object, dependent objects might not continue to function without error, depending on the type of alteration. For example, if you drop a table, no view based on the dropped table is usable.

[ALL_ | DBA_ | USER_]DEPENDENCIES: describe dependencies among db objects

SQL> create or replace view highsal as
 select * from employees
 where salary >=100000;
 
SQL> create or replace view commissioned as
  select first_name, last_name, commission_pct
  from employees
  where commission_pct > 0.00;

SQL> select object_name, status 
from user_objects
where object_type = 'VIEW'
order by object_name;

OBJECT_NAME      STATUS
---------------- -------
COMMISSIONED     VALID
HIGHSAL          VALID

SQL> alter table employees modify email varchar2(100);

SQL> select object_name, status 
from user_objects
where object_type = 'VIEW'
order by object_name;


OBJECT_NAME      STATUS
---------------- -------
COMMISSIONED     VALID
HIGHSAL          INVALID


SQL> select name, type, referenced_name, referenced_type, dependency_type
from user_dependencies
order by name;

NAME            TYPE          REFERENCED_NAME  REFERENCED_TYPE  DEPENDENCY_TYPE
-----------     ------------  ---------------  --------------   -------------
CIRCLE_AREA     PROCEDURE     DBMS_OUTPUT      SYNONYM          HARD
CIRCLE_AREA     PROCEDURE     MY_PKG           PACKAGE          HARD
...
COMMISSIONED    VIEW          EMPLOYEES        TABLE            HARD
HIGHSAL         VIEW          EMPLOYEES        TABLE            HARD
...
MY_PKG          PACKAGE BODY  MY_PKG           PACKAGE          HAD
MY_PKG          PACKAGE BODY  DBMS_DB_VERSION  PACKAGE          HARD
...
MY_PKG          PACKAGE       DBMS_DB_VERSION  PACKAGE          HARD




  • Cascading invalidation: Indirect dependents can be invalidated by changes to the reference object that do not affect them.
    • If a change to C invalidates B, it invalidates A (and all other direct and indirect dependents of B).
  • Coarse-grained invalidation: DDL statement that changes a referenced object invalidates all of its dependents.

On 11g R1: Fine-grained invalidation
  • Fine-grained invalidation: DDL statement that changes a referenced object invalidates only dependents for which:
    1. Dependent relies on the attribute of the referenced object that the DDL statement changed.
    2. The compiled metadata of the dependent is no longer correct for the changed referenced object.
    3. For example, if view v selects columns c1 and c2 from table t, a DDL statement that changes only column c3 of t does not invalidate v.


Minimize object invalidation

(a) Add Items to End of Package
This preserves the entry point numbers of existing top-level package items, preventing their invalidation.

CREATE OR REPLACE PACKAGE pkg1 IS
  FUNCTION get_var RETURN VARCHAR2;
END;

-- Adding an item to the end of pkg1, as follows, 
-- does not invalidate dependents that reference the get_var function:
CREATE OR REPLACE PACKAGE pkg1 IS
  FUNCTION get_var RETURN VARCHAR2;
  PROCEDURE set_var (v VARCHAR2);
END;

-- Inserting an item between the get_var function and the set_var procedure
-- invalidates dependents that reference the set_var function:
CREATE OR REPLACE PACKAGE pkg1 IS
  FUNCTION get_var RETURN VARCHAR2;
  PROCEDURE assert_var (v VARCHAR2);
  PROCEDURE set_var (v VARCHAR2);
END;

(b) Reference Each Table Through a View
Reference tables indirectly, using views, enabling you to:
  • Add columns to the table without invalidating dependent views or dependent PL/SQL objects.
  • Modify or delete columns not referenced by the view without invalidating dependent objects
  • The statement CREATE OR REPLACE VIEW does not invalidate an existing view or its dependents if the new ROWTYPE matches the old ROWTYPE.


Revalidation

To recompile a schema object using the appropriate SQL statement with the COMPILE clause.
  • ALTER PACKAGE pkg1 COMPILE REUSE SETTINGS;

Requisites to use UTL_RECOMP package:

  • This package must be run using SQL*PLUS.
  • You must be connected AS SYSDBA to run this script.
  • The following packages should have VALID status:
    • STANDARD (standard.sql)
    • DBMS_STANDARD (dbmsstdx.sql)
    • DBMS_JOB (dbmsjob.sql)
    • DBMS_RANDOM (dbmsrand.sql)
  • No other DDL must be running on the database, or deadlock may result.
  • This package uses the job queue for parallel recompilation.




You can use UTL_RECOMP package to recompile one or more invalid objects:

  • This is useful after an application upgrade or patch, when is good practice to revalidate invalid objects to avoid application latencies that result from on-demand object revalidation.



-- Recompile all objects sequentially:
EXECUTE UTL_RECOMP.RECOMP_SERIAL();

-- Recompile objects in schema SCOTT sequentially:
EXECUTE UTL_RECOMP.RECOMP_SERIAL('SCOTT');

-- Recompile all objects using 4 parallel threads:
EXECUTE UTL_RECOMP.RECOMP_PARALLEL(4);

-- Recompile objects in schema JOE using the number of threads specified in the parameter JOB_QUEUE_PROCESSES:
EXECUTE UTL_RECOMP.RECOMP_PARALLEL(NULL, 'JOE');


Revalidate individual invalid objects using DBMS_UTILITY.VALIDATE.
-- Revalidate the procedure UPDATE_SALARY in schema HR:
begin
dbms_utility.validate('HR', 'UPDATE_SALARY', namespace=>1);
end;

-- Revalidate the package body HR.ACCT_MGMT:
begin
dbms_utility.validate('HR', 'ACCT_MGMT', namespace=>2);
end;





(ref) On Compound Triggers

Compound Triggers

About Compound triggers:
  • First appeared in Oracle 11g.
  • Can fire at multiple timing points.
  • Each timing point section has its own executable parte and optional exection-handling part.
  • Act as both statement- and row-level triggers.
  • Can be defined on either a table or a view
  • Do not support filtering actions with the WHEN clause or the use of the autonomous transaction PRAGMA
  • The firing order of compound triggers is not guaranteed because they can be interleaved (mixed between) with the firing of standalone triggers.
  • There is a declaration section for the trigger as a whole. Individual timing points share this single global declaration section, whose state is maintained for the lifetime of the statement. In addition, each timing point section has its own local declaration section.
  • Compound triggers do not support an EXCEPTION block, but you can implement EXCEPTION
    blocks in any of the subordinate timing point blocks.
  • Compound triggers do not fire when (a) the DML statement doesn’t change any rows and (b) the trigger does not implement at least a BEFORE STATEMENT or AFTER STATEMENT block.
  • In previous releases this type of functionality was only possible by defining multiple triggers whose code and global variables were defined in a separate package.

Common uses of Compound DML Triggers:
  • Accumulate rows - with values obtained or generated at each "EACH ROW" firing - so that these can be inserted with BULK-INSERT
  • To avoid the mutating-table error (ORA-04091)

Restrictions:
  • OLD, NEW, and PARENT cannot appear in the declarative part, the BEFORE STATEMENT section, or the AFTER STATEMENT section.
  • Only the BEFORE EACH ROW section can change the value of NEW.
  • A timing-point section cannot handle exceptions raised in another timing-point section.
  • If a timing-point section includes a GOTO statement, the target of the GOTO statement must be in the same timing-point section.


Syntax:
CREATE [OR REPLACE] TRIGGER trigger_name
 FOR {INSERT | UPDATE | UPDATE OF column1 [, column2 [, column(n+1)]] | DELETE}
 ON table_name
 COMPOUND TRIGGER
[BEFORE STATEMENT IS
  [declaration_statement;]
BEGIN
  execution_statement;
END BEFORE STATEMENT;]
[BEFORE EACH ROW IS
  [declaration_statement;]
BEGIN
  execution_statement;
END BEFORE EACH ROW;]
[AFTER EACH ROW IS
  [declaration_statement;]
BEGIN
  execution_statement;
END AFTER EACH ROW;]
[AFTER STATEMENT IS
  [declaration_statement;]
BEGIN
  execution_statement;
END AFTER STATEMENT;]
END [trigger_name];
/


Mutating-Table error: what is it?

  • A mutating table is a table that is being modified by a DML statement (i.e. by the effects of a DELETE CASCADE constraint).
  • If a trigger is fired as the result of a DML statement in table A, it cannot query or modify table A (the table that the triggering statement is modifying.)
  • When a row-level trigger encounters a mutating table, ORA-04091 occurs, the effects of the trigger and triggering statement are rolled back, and control returns to the user or application that issued the triggering statement.

Example:

(a) create table ELOG to log updates in the table EMPLOYEES
(b) create trigger to insert into ELOG the values (:OLD) that are modified when an UPDATE is issued against EMPLOYEES.
(c) The trigger should (1) Insert the OLD values into ELOG and (2) Print a statement with the number of rows remaining on EMPLOYEES.
  • For this, it has to perform a SELECT in the table that is being modified (EMPLOYEES). And there the bicho pega...

create table elog (
  emp_id number(6),
  l_name varchar2(25),
  f_name varchar2(20));

create or replace trigger log_deletions
  after delete on employees
  for each row
declare
 n integer;
begin
 insert into log values
   (:old.employee_id,
    :old.last_name, 
    :old.first_name);
 select count(*) into n from employees;
 dbms_output.put_line('there are now ' || n || ' employees.');
end;

delete from employees where employee_id=197;

SQL Error: ORA-04091: table EMPLOYEES is mutating, trigger/function may not see it
ORA-06512: at "LOG_DELETIONS", line 8
ORA-04088: error during execution of trigger 'LOG_DELETIONS'
04091. 00000 -  "table %s.%s is mutating, trigger/function may not see it"
*Cause:    A trigger (or a user defined plsql function that is referenced in
           this statement) attempted to look at (or modify) a table that was
           in the middle of being modified by the statement which fired it.
*Action:   Rewrite the trigger (or function) so it does not read that table.

(?) How to solve this error?

(1) You may Use a temporary table
Instead of using one AFTER each row trigger that updates the mutating table, use two triggers
(a) an AFTER each row trigger that updates the temporary table and
(b) an AFTER statement trigger that updates the mutating table with the values from the temporary table.

(2) Use a compound DML trigger:


Compound Trigger: Bulk Logging updates to solve ORA-04091
The example below uses a compound trigger to log salary updates in the employees table, and capture old and new values for the updated columns.
(a) create table emp_log_update
(b) create sequence emp_log_update_seq
(c) Create trigger emp_log_tg
(d) Configure client information (use sys_context to set client info)
(e) Update rows on employees to fire the compound trigger
(f) check the inserted row on emp_log_update

-- create table to log updates. The table will be used by the compound trigger.
create table emp_log_update
 (emp_log_id       number,
  emp_id           number,
  oldval           varchar2(20),
  newval           varchar2(20),
  created_by       varchar2(20), 
  creation_date    date,
  last_updated_by  varchar2(20),
  last_update_date date);

create sequence emp_log_update_seq;

-- create compound trigger. 
-- The For each row section: stores :old and :new values for each updated row
-- The statement section: performs a bulk insert in the log table.

CREATE OR REPLACE TRIGGER employee_update_tg
 FOR UPDATE of SALARY on EMPLOYEES
 COMPOUND TRIGGER
 TYPE emp_rec IS RECORD
  (emp_log_id      emp_log_update.emp_log_id%type,
   emp_id          emp_log_update.emp_id%type,
   oldval          emp_log_update.oldval%type,
   newval          emp_log_update.newval%type,
   created_by      emp_log_update.created_by%type, 
   creation_date   emp_log_update.creation_date%type,
   last_updated_by emp_log_update.last_updated_by%type,
   last_update_date emp_log_update.last_update_date%type);
 
 TYPE emp_list IS TABLE OF emp_rec;
 emp_updates emp_list := emp_list();

BEFORE EACH ROW IS
 -- declare local timing point variables
 c number;
 -- stores the client info (identification, set up in the application)
 user_id varchar2(20) := nvl((sys_context('userenv','client_info')), -1);
BEGIN
 emp_updates.EXTEND;
 c := emp_updates.LAST;
 emp_updates(c).emp_log_id := emp_log_update_seq.nextval; 
 emp_updates(c).emp_id := :old.employee_id; 
 emp_updates(c).oldval := to_char(:old.salary); 
 emp_updates(c).newval := to_char(:new.salary); 
 emp_updates(c).created_by := user_id; 
 emp_updates(c).creation_date := sysdate; 
 emp_updates(c).last_updated_by := user_id; 
 emp_updates(c).last_update_date := sysdate; 
END BEFORE EACH ROW;
AFTER STATEMENT IS
BEGIN
 FORALL i in emp_updates.FIRST..emp_updates.LAST
   insert into emp_log_update
   values (
      emp_updates(i).emp_log_id, 
      emp_updates(i).emp_id, 
      emp_updates(i).oldval, 
      emp_updates(i).newval, 
      emp_updates(i).created_by,
      emp_updates(i).creation_date,
      emp_updates(i).last_updated_by,
      emp_updates(i).last_update_date
      );
END AFTER STATEMENT;
END;

Trigger created.

-- Set the client info: 
SQL> EXEC dbms_application_info.set_client_info('dev2');

PL/SQL procedure successfully completed.

SQL>update employees set salary=salary*1.15 where department_id=60;
5 rows updated.

SQL> select * from emp_event_log;
(formatted result..)
ID  EMP_ID  OLDVAL    NEWVAL   CREATED_BY   CREATION  LAST_UPDATED  LAST_UPDATED
                                              DATE         BY          DATE
--- ------- --------- -------- ------------ --------- ------------- ------------
  2     103 9000      10350    dev2         26-AUG-11 dev2          26-AUG-11
  3     104 6000      6900     dev2         26-AUG-11 dev2          26-AUG-11
  4     105 4800      5520     dev2         26-AUG-11 dev2          26-AUG-11
  5     106 4800      5520     dev2         26-AUG-11 dev2          26-AUG-11
  6     107 4200      4830     dev2         26-AUG-11 dev2          26-AUG-11
DBMS_APPLICATION_INFO package
  • Use DBMS_APPLICATION_INFO.{SET_CLIENT_INFO | READ_CLIENT_INFO} to maintain client info
  • Read/Write column CLIENT_INFO on V$SESSION and V$SQL_AREA
  • You can set module name or (trans)action name.
  • Also useful with Oracle Trace and SQL Trace: to record names of modules or transactions for performance analysis or debugging.
  • set action name before the start of a transaction.
  • DBMSAPIN.SQL run by catproc

#set module and transaction name
CREATE or replace PROCEDURE add_employee( 
  name VARCHAR2, 
  salary NUMBER, 
  manager NUMBER, 
  title VARCHAR2, 
  commission NUMBER, 
  department NUMBER) AS 
BEGIN 
  DBMS_APPLICATION_INFO.SET_MODULE( 
    module_name => 'add_employee', 
    action_name => 'insert into emp'); 
  INSERT INTO emp 
    (ename, empno, sal, mgr, job, hiredate, comm, deptno) 
    VALUES (name, emp_seq.nextval, salary, manager, title, SYSDATE, 
            commission, department); 
  DBMS_APPLICATION_INFO.SET_MODULE(null,null); 
END;


# Set transaction name
CREATE OR REPLACE PROCEDURE bal_tran (amt IN NUMBER(7,2)) AS 
BEGIN 
-- balance transfer transaction 
   DBMS_APPLICATION_INFO.SET_ACTION(
      action_name => 'transfer from chk to sav'); 
   UPDATE chk SET bal = bal + :amt 
      WHERE acct# = :acct; 
   UPDATE sav SET bal = bal - :amt 
      WHERE acct# = :acct; 
   COMMIT; 
   DBMS_APPLICATION_INFO.SET_ACTION(null); 
END;  

Other example:
Session 1 (anonymous block):
(a) set action and client info.
(b) delete rows from employees

Session 2:
(a) query V$SESSION

#SID 38: 
SQL> BEGIN
    dbms_application_info.set_action(action_name => 'delete from temp_emp');
    dbms_application_info.set_client_info(client_info => 'Issued from terminal 2');
    delete from temp_emp where city='Denver';
    END;
SQL> /

PL/SQL procedure successfully completed

#SID 2:
SQL> select sid, serial#, username, osuser, module, action, client_info from v$session;

(formatted output)
 SID  SERIAL# USERNAME  OSUSER   MODULE     ACTION                CLIENT_INFO
---- -------- --------- -------- ---------- --------------------- ----------------------
   2       10 SYSTEM    jdba     SQL*Plus
(...) 
  38        7 JFARR     jdev     SQL*Plus   delete from temp_emp  Issued from terminal 2
(...)



(8) Cursors and Static SQL


What?
  • PL/SQL implicitly declares a cursor for all SQL data manipulation statements
  • Feature of PL/SQL that allows SQL syntax directly in a PL/SQL statement
  • SQL Statement here is defined during compile time.
  • PL/SQL static SQL statements:
    • SELECT, INSERT, UPDATE, DELETE, and MERGE
    • COMMIT, ROLLBACK, SAVEPOINT, SET TRANSACTION
    • LOCK TABLE

  • A PL/SQL static SQL statement can have a PL/SQL identifier wherever its SQL counterpart can have a placeholder for a bind argument.
  • PseudoColumns: behave like a table column, but it is not stored in the table
    • CURRVAL and NEXTVAL
    • LEVEL
    • OBJECT_VALUE
    • ROWID and ROWNUM

Cursors
  • Pointer to a private SQL area that stores information about processing a specific SELECT or DML statement.
  • Session cursors live in session memory until the session ends
  • A Session cursor can be implicit or explicit.
  • V$OPEN_CURSOR lists session cursors that each user session has opened and parsed
  • A PL/SQL cursor variable lets you retrieve multiple rows from a stored subprogram.
  • You can pass cursor variables as parameters in your 3GL application.
  • With cursors, an SQL stmt can be reexecuted without including the parse stage.
  • Repeated execution of the same SQL statements: can begin at the describe, define, bind, or run step, saving the repeated cost of opening cursors and parsing.

Check the performance characteristics of a cursor
V$SQL_PLAN
Execution plan information for each child cursor loaded in the library cache
V$SQL_PLAN_STATISTICS
Execution plan information for each child cursor loaded in the library cache
V$SQL_PLAN_STATISTICS_ALL
Memory usage statistics for row sources that use SQL memory (sort or hash-join). This view concatenates information in V$SQL_PLAN with execution statistics from V$SQL_PLAN_STATISTICS and V$SQL_WORKAREA



How many cursors can a session maintain?
  • Depends on: The amount of memory available to the session
  • Depends on: The value of the initialization parameter OPEN_CURSORS






Implicit cursors
  • Also called SQL Cursor
  • PL/SQL opens an implicit cursor every time you run a SELECT or DML statement
  • %ROWCOUNT attribute: stores the number of rows affected by a DML.
  • You cannot control an implicit cursor, but you can get information from its attributes
  • SQLattribute always refers to the most recently run SELECT or DML statement. If no such statement has run, the value of SQLattribute is NULL.
  • Cursor attribute values remain available until another SELECT or DML statement runs.

Implicit Cursor Attributes
SQL%ISOPEN
(Is the Cursor Open?)

  • Always returns FALSE. (Since implicit cursors always close after the statement runs.)
SQL%FOUND and SQL%NOTFOUND
(Were any rows affected?)
  • NULL: If no SELECT OR DML was executed.
  • TRUE: If SELECT or DML returned/affected +0 rows.
  • FALSE: If no rows affected.
create table dept_temp  as select * from departments;

create or replace procedure p ( dept_no number) 
     authid definer as
begin
 delete from dept_temp
 where department_id = dept_no;
 If sql%found then
   dbms_output.put_line('delete succeeded. dept_no: '
                                        || dept_no);
 else
     dbms_output.put_line('No Dept_no: '|| dept_no);
 end if;
end;
/

SQL> set serveroutput on
SQL> exec p(400);

anonymous block completed
No Dept_no: 400
  • If SELECT INTO returns no rows: predefined exception NO_DATA_FOUND is raised immediately (before SQL%NOTFOUND can be checked).
  • SELECT INTO that invokes a SQL aggregate function (i.e. avg, sum, count, etc) always returns a value (possibly NULL). Here SQL%NOTFOUND is always FALSE.
SQL%ROWCOUNT
(how many rows affected?)
  • NULL: If no SELECT OR DML was executed.
  • N: N is the number of rows returned (select) or affected (dml).
  • FALSE: If no rows affected.
SQL> set serveroutput on 
SQL> declare
 mgr_no  number(6) := 122;
begin
 delete from emp_temp where manager_id = mgr_no;
 dbms_output.put_line
  ('number of employees deleted: ' || to_char(sql%rowcount));
end;
/

anonymous block completed
number of employees deleted: 8

set serveroutput on
declare
 n number;
begin
 dbms_output.put_line('Select.. ---------');
 select count(*) into n from employees;
 dbms_output.put_line('SQL%ROWCOUNT: [' || SQL%ROWCOUNT 
                                   ||'] rows returned');
 dbms_output.put_line('Update.. ---------');
 update employees
   set salary = salary*1.10
   where job_id = 'SA_REP'; 
 dbms_output.put_line('SQL%ROWCOUNT: [' || SQL%ROWCOUNT 
                                    ||'] rows updated');
end;

anonymous block completed
Select.. ---------
SQL%ROWCOUNT: [1] rows returned
Update.. ---------
SQL%ROWCOUNT: [30] rows updated
SQL%BULK_ROWCOUNT
  • Is like an associative array whose ith element is the number of rows affected by the ith DML statement in the most recently completed FORALL statement..
set serveroutput on 
declare
 type NumList is table of number;
 depts numlist := NumList(30, 50, 60);
begin
 forall j in depts.first..depts.last
   delete from emp_temp where department_id = depts(j);
 
 for i in depts.first..depts.last loop
   dbms_output.put_line(
     'Statement #' || i || ' deleted ' || 
      sql%bulk_rowcount(i) || 'rows.');
 end loop;
 
 dbms_output.put_line('Total rows deleted: ' 
                             || sql%rowcount);
end;
/

anonymous block completed
Statement #1 deleted 0rows.
Statement #2 deleted 45rows.
Statement #3 deleted 5rows.
Total rows deleted: 50
SQL%BULK_EXCEPTIONS
  • SQL%BULK_EXCEPTIONS is like an associative array of information about the DML statements that failed during the most recently run FORALL statement.
  • To allow a FORALL statement to continue even if some of its DML statements fail, include the SAVE EXCEPTIONS clause.
  • After the FORALL statement completes, PL/SQL raises a single exception for the FORALL statement (ORA-24381). In the exception handler for ORA-24381, you can get information about each individual DML statement failure from the implicit cursor attribute SQL%BULK_EXCEPTIONS.

  • SQL%BULK_EXCEPTIONS.COUNT:# of DML stmts that failed.
  • SQL%BULK_EXCEPTIONS(i).ERROR_INDEX: # of the ith failed DML stmt.
  • SQL%BULK_EXCEPTIONS(i).ERROR_CODE: error code for the ith failure.
  • SQLERRM(-(SQL%BULK_EXCEPTIONS(i).ERROR_CODE)): returns the associated error message
create or replace procedure p
  authid definer as 
  type deplist is table of number;
  depts deplist := deplist(40, 90, 100);
  
  type emptab is table of employees.employee_id%type;
  type deptab is table of employees.department_id%type;
  
  enums emptab;
  dnums deptab;
  
  errm          varchar2(100);
  bad_stmt_no   pls_integer;
  bad_deptno    emp_temp.department_id%type;
  bad_empno     emp_temp.employee_id%type;
  bad_email     emp_temp.email%type;
  
  dml_errors    exception;
  pragma exception_init(dml_errors, -24381);
  
begin
     select employee_id, department_id 
     bulk collect into enums, dnums
     from emp_temp
     where department_id in (depts(1), depts(2), depts(3));
     
     forall j in enums.first..enums.last save exceptions
       update emp_temp 
       set email = email || '@dep' || dnums(j)
                         || '.company.com'
       where employee_id = enums(j);
exception
    when dml_errors then
     for i in 1..sql%bulk_exceptions.count loop
       errm := sqlerrm(-(sql%bulk_exceptions(i).error_code));
       dbms_output.put_line(error_message);
       
       bad_stmt_no := sql%bulk_exceptions(i).error_index;
       dbms_output.put_line('Bad stmt #: '|| bad_stmt_no);
      
       bad_empno := enums(bad_stmt_no);   
       dbms_output.put_line('Bad empno #: '|| bad_empno);
     
       bad_deptno := dnums(bad_stmt_no);
       select email into bad_email 
       from emp_temp
       where employee_id = bad_empno;

       dbms_output.put_line('Bad email #' || i 
                           || ' : '|| bad_email);
    end loop;
    
    -- commit;
    
    when others then
      dbms_output.put_line('other error');
      raise;
end;

set serveroutput on;
exec p;

anonymous block completed
ORA-12899: value too large for column  (actual: , maximum: )
Bad stmt #: 2
Bad empno #: 101
Bad email #1 : NKOCHHAR
(...)
ORA-12899: value too large for column  (actual: , maximum: )
Bad stmt #: 5
Bad empno #: 109
Bad email #3 : DFAVIET
(...)


Using SELECT INTO: Implicit cursor
- May return a single row - (here select directly into a scalar or record)
- you may also use WHERE ROWNUM = n to select a specific row.
- Or return multiple rows: here you need to use Select xxx BULK COLLECT INTO collection;

Returning a single row:

DECLARE
  bonus   NUMBER(8,2);
BEGIN
  SELECT salary * 0.10 INTO bonus
  FROM employees
  WHERE employee_id = 100;
END;

DBMS_OUTPUT.PUT_LINE('bonus = ' || TO_CHAR(bonus));
returning into a record
SELECT select_list INTO record_variable_name FROM table_or_view_name;
DECLARE
  TYPE RecordTyp IS RECORD (
    last employees.last_name%TYPE
    id   employees.employee_id%TYPE
  );
  rec1 RecordTyp;
BEGIN
  SELECT last_name, employee_id INTO rec1
  FROM employees
  WHERE job_id = 'AD_PRES';

  DBMS_OUTPUT.PUT_LINE ('Employee #' || rec1.id || ' = ' || rec1.last);
END;
/
Returning multiple rows: BULK COLLECT clause
DECLARE
  TYPE NumTab IS TABLE OF employees.employee_id%TYPE;
  TYPE NameTab IS TABLE OF employees.last_name%TYPE;
 
  enums NumTab;
  names NameTab;
 
  PROCEDURE print_first_n (n POSITIVE) IS
  BEGIN
    IF enums.COUNT = 0 THEN
      DBMS_OUTPUT.PUT_LINE ('Collections are empty.');
    ELSE
      DBMS_OUTPUT.PUT_LINE ('First ' || n || ' employees:');
 
      FOR i IN 1 .. n LOOP
        DBMS_OUTPUT.PUT_LINE (
          '  Employee #' || enums(i) || ': ' || names(i));
      END LOOP;
    END IF;
  END;
 
BEGIN
  SELECT employee_id, last_name
  BULK COLLECT INTO enums, names
  FROM employees
  ORDER BY employee_id;
 
  print_first_n(3);
  print_first_n(6);
END;
/

Using RETURNING INTO clause on a DML

SQL> create table emp as 
        select employee_id, last_name, salary 
        from employees;

set serveroutput on
declare
 type emprec is record 
          (empid emp.employee_id%type,
           lname emp.last_name%type, 
           sal   emp.salary%type);
 vemprec emprec;
begin
  delete from emp
  where employee_id = 188
  returning employee_id, last_name, salary 
  INTO vemprec;

  dbms_output.put_line('the record deleted was: ');
  dbms_output.put_line(vemprec.lname || 'id: '|| vemprec.empid 
                       ||' sal: '|| vemprec.sal);
exception
  when others then 
    dbms_output.put_line(sqlerrm);
end;
/

anonymous block completed
the record deleted was: 
Chungid: 188 sal: 3800



Cursor FOR LOOP
  • Use with either implicit or explicit cursor.
  • The cursor FOR LOOP statement implicitly declares its loop index as a %ROWTYPE record variable of the type that its cursor returns.
  • Implicit cursor FOR LOOP stmt
  • SELECT stmt inside the FOR LOOP

set serveroutput on
begin 
 for item in 
   ( select last_name, job_id 
     from employees
     where job_id like '%CLERK%'
     and manager_id > 100
     order by last_name)
  loop
    dbms_output.put_line
      ('Name = '|| item.last_name || ',  Job = ' || item.job_id);
  end loop;
end;

anonymous block completed
Name = Atkinson,  Job = ST_CLERK
Name = Baida,  Job = PU_CLERK
Name = Bell,  Job = SH_CLERK
(...)

- explicit cursor FOR LOOP
- Move the select statement to the declaration section and explicitly delcare a cursor.
set serveroutput on
declare
 cursor c1 is
  select last_name, job_id 
     from employees
     where job_id like '%CLERK%'
     and manager_id > 100
     order by last_name;
begin 
 for item in c1 loop
    dbms_output.put_line
      ('Name = '|| item.last_name || ',  Job = ' || item.job_id);
 end loop;
end;
anonymous block completed
Name = Atkinson,  Job = ST_CLERK
Name = Baida,  Job = PU_CLERK
Name = Bell,  Job = SH_CLERK
(...)

- Cursor FOR LOOP with parameters

set serveroutput on
declare
 cursor c1 (job employees.job_id%type, max_wage number) is
   select * from employees
   where job_id = job
   and salary > max_wage;
begin
 for person in c1 ('ST_CLERK', 3000) loop
    dbms_output.put_line(
      'Name = '|| person.last_name || ', salary = ' ||
      person.salary ||',  Job Id = '|| person.job_id);
  end loop;
end;

anonymous block completed
Name = Nayer, salary = 3200,  Job Id = ST_CLERK
Name = Bissot, salary = 3300,  Job Id = ST_CLERK
(...)

Cursors with subqueries

set serveroutput on
declare
  cursor c1 is 
    select t1.department_id, department_name, staff
    from departments t1,
         ( select department_id, count(*) as staff
           from employees
           group by department_id 
          ) t2
    where (t1.department_id = t2.department_id) 
      and staff >= 5
    order by staff;
begin
  for dept in c1 loop
    dbms_output.put_line ('Department = '
        || dept.department_name || ', staff = ' || dept.staff);
  end loop;
end;

anonymous block completed
Department = IT, staff = 5
Department = Finance, staff = 6
Department = Purchasing, staff = 6
Department = Administration, staff = 7
Department = Sales, staff = 34
Department = Shipping, staff = 45


Explicit cursors

  • Also called a named cursor, since unlike implicit ones, an explicit cursor can be referenced by its name
  • is a named pointer to a private SQL area that stores information for processing a specific query or DML statement
  • Before using an explicit cursor, you must declare (cursor specification) and define it (cursor body).
  • You cannot assign a value to an explicit cursor, use it in an expression, or use it as a formal subprogram parameter or host variable.
  • You can do those things with a cursor variable.


Explicit cursor declaration, which only declares a cursor:
CURSOR cursor_name [ parameter_list ] RETURN return_type;


An explicit cursor definition has this syntax:
CURSOR cursor_name [ parameter_list ]
[ RETURN return_type ] IS select_statement;


DECLARE
  CURSOR c1 RETURN departments%ROWTYPE;    -- Declare c1
 
  CURSOR c2 IS                             -- Declare and define c2
    SELECT employee_id, job_id, salary FROM employees
    WHERE salary > 2000; 
 
  CURSOR c1 RETURN departments%ROWTYPE IS  -- Define c1,
    SELECT * FROM departments              -- repeating return type
    WHERE department_id = 110;
BEGIN
  NULL;
END;










OPEN, FETCH, and CLOSE

  • Process multiple result sets in parallel, using multiple cursors.
  • Process multiple rows in a single loop iteration, skip rows, or split the processing into multiple loops.
  • Specify the query in one PL/SQL unit but retrieve the rows in another.


Managing Explicit Cursors
OPEN
- Allocates database resources
- Process the query (identify result set and lock rows (if FOR UPDATE clause is there)
- Position cursor before the first row of the result set.
FETCH
FETCH cursor_name INTO into_clause
- retrieves the current row of the result set, stores the column values of that row into the variables or record, and advances the cursor to the next row.
- PL/SQL does not raise an exception when a FETCH statement returns no rows. To detect this, use the attribute %NOTFOUND.

CLOSE
- After closing a cursor, you cannot fetch records from its result set or reference its attributes. If you try, PL/SQL raises the predefined exception INVALID_CURSOR.
- You can reopen a closed cursor. You must close an explicit cursor before you try to reopen it. Otherwise, PL/SQL raises the predefined exception CURSOR_ALREADY_OPEN.


set serveroutput on
declare 
 cursor c1 is 
   select last_name, job_id from employees
   where regexp_like(job_id, 'S[HT]_CLERK')
   order by last_name;

 v_lastname employees.last_name%type;
 v_jobid    employees.job_id%type;
 
 cursor c2 is 
   select * from employees
   where regexp_like(job_id, '[ACADFIMKSA]_M[ANGR]')
   order by job_id;
 
 v_employees employees%rowtype;
 
begin
  open c1;
  loop
    fetch c1 into v_lastname, v_jobid;
    exit when c1%notfound;
    dbms_output.put_line(RPAD(v_lastname, 25, ' ') || v_jobid);
  end loop;
  close c1;
  
  dbms_output.put_line( '-----------------------------');
   
  open c2;
  loop
    fetch c2 into v_employees;
    exit when c2%notfound;
    dbms_output.put_line(RPAD(v_employees.last_name, 25, ' ') || 
                               v_employees.job_id);
  end loop;
  close c2;
end;


anonymous block completed
Atkinson                 ST_CLERK
Bell                     SH_CLERK
...
Walsh                    SH_CLERK
-----------------------------
Higgins                  AC_MGR
Glenn                    AC_MGR
...
Zlotkey                  SA_MAN




Explicit cursors with parameter
set serveroutput on
declare
  cursor c (job varchar2, max_sal number) IS
    select last_name, first_name, (salary-max_sal) overpayment
    from employees
    where job_id = job
    and salary > max_sal
    order by salary;

  procedure print_overpaid is
    ln_ employees.last_name%type;
    fn_ employees.first_name%type;
    overpayment_ employees.salary%type;
  begin
    loop
      fetch c into ln_, fn_, overpayment_;
      exit when c%notfound;
      dbms_output.put_line(ln_ || ', ' || fn_ ||
        ' (by '  || overpayment_ || ')');
    end loop;
  end print_overpaid;

begin
 dbms_output.put_line('---------------------');
 dbms_output.put_line('Overpaid Clerks:');
 dbms_output.put_line('---------------------');
 Open c('ST_CLERK', 5000);
 print_overpaid;
 close c;
 

 dbms_output.put_line('---------------------');
 dbms_output.put_line('Overpaid Sales Rep:');
 dbms_output.put_line('---------------------');
 Open c('SA_REP', 10000);
 print_overpaid;
 close c;
end;
Explicit Cursor Attributes
Syntax: cursor_name%attribute
%ISOPEN
(Is the Cursor Open?)



  • Useful for: Checking if an explicit cursor is already open before trying to open it.
  • If you try to open an explicit cursor that is already open, PL/SQL raises the predefined exception CURSOR_ALREADY_OPEN. You must close an explicit cursor before you can reopen it.
  • Checking that an explicit cursor is open before you try to close it.
declare 
 cursor c1 is 
   select last_name, salary from employees
   where rownum < 11;
 v1 employees.last_name%type;
 v2 employees.salary%type;
begin
 if not c1%isopen then
    open c1;
 end if;
 
 fetch c1 into v1, v2;
 
 if c1%isopen then
   close c1;
 end if;
end;
%FOUND and %NOTFOUND
(Has a row been fetched?)
  • NULL: after the explicit cursor is opened but before the first fetch
  • TRUE: if the most recent fetch from the explicit cursor returned a row
  • FALSE: otherwise
  • %FOUND use to check if there is a fetched row to process.
set serveroutput on
DECLARE
  CURSOR c1 IS
    SELECT last_name, salary FROM employees
    WHERE ROWNUM < 11
    ORDER BY last_name;

  my_ename   employees.last_name%TYPE;
  my_salary  employees.salary%TYPE;
BEGIN
  OPEN c1;
  LOOP
    FETCH c1 INTO my_ename, my_salary;
    IF c1%FOUND THEN  -- fetch succeeded
      DBMS_OUTPUT.PUT_LINE('Name = ' || my_ename || ', 
                             salary = ' || my_salary);
    ELSE  -- fetch failed
      DBMS_OUTPUT.PUT_LINE('---------------------');
      DBMS_OUTPUT.PUT_LINE('C''est fini');
      EXIT;
    END IF;
  END LOOP;
END;
/
%ROWCOUNT
(How may rows fetched?)

  • Zero after the explicit cursor is opened but before the first fetch
  • Otherwise, the number of rows fetched
Cursor variables

Cursor variable is a pointer: its value is the address of an item, not the item itself. - Similar to an an explicit cursor, except that:
  • It is not limited to one query.
  • You can assign a value to it.
  • You can use it in an expression.
  • It can be a subprogram parameter.
  • It can be a host variable.
  • It cannot accept parameters. But you can pass whole queries to it.
  • To create a cursor variable, either declare a variable of the predefined type SYS_REFCURSOR or define a REF CURSOR type and then declare a variable of that type.


Cursor variable declarations: Syntax: TYPE type_name IS REF CURSOR [ RETURN return_type ]
DECLARE
 DECLARE
  -- strong type
  TYPE empcurtyp IS REF CURSOR RETURN employees%ROWTYPE;
  -- weak type
  TYPE genericcurtyp IS REF CURSOR;                      

  cursor1  empcurtyp;       -- strong cursor variable
  cursor2  genericcurtyp;   -- weak cursor variable
  my_cursor SYS_REFCURSOR;  -- weak cursor variable

BEGIN
  NULL;
END;




Cursor Variable - Examples
Cursor Variable

(1) Opens a cursor variable for a query that references the variable factor.
(2) Notice that the variable is evaluated only once: when the cursor is opened.


set serveroutput on
declare
  sal                employees.salary%type;
  sal_multiple       employees.salary%type;
  factor             integer := 2;
  
  -- declare cursor variable predefined type
  cv sys_refcursor;

begin
 -- open cursor defining SQL stmt. 
 -- once closed later, the cursor can be opened
 -- again with a different stmt
 Open cv for 
   select salary, salary*factor
   from employees
   where job_id like 'AD_%'; 
 
 loop
   --load the next row of data into variables
   -- if no rows left, exit loop
   fetch cv into sal, sal_multiple;
   exit when cv%notfound;
   dbms_output.put_line('factor = '|| factor);
   dbms_output.put_line('sal = '|| sal);
   dbms_output.put_line('sal_multiple = '|| sal_multiple);
   -- note that the variable factor is evaluated only one
   -- when the cursor is opened. After this, new variable
   -- values are not seen by the cursor unless it is 
   -- closed and opened again.
   factor := factor + 1;
 end loop;
 close cv;
end;

anonymous block completed
factor = 2
sal = 4400
sal_multiple = 8800
factor = 3
sal = 24000
sal_multiple = 48000
factor = 4
sal = 17000
sal_multiple = 34000
factor = 5
sal = 17000
sal_multiple = 34000
Passing cursor variables as parameter
(1) Package specification: Define public (a) cursor variable; (b) procedures to open and manipulate cursor. (2) Package body: Procedures that open the cursor for read and write (IN OUT). And list cursor contents (IN). (3) Anonymous block: declares cursor variable of the same type defined in package specification. (4) Anonymous block: Calls procedures to open and iteract through the cursor.
set serveroutput on

-- Package specification. 
create or replace package emp_data as
  -- Defines public cursor variable type (strongly enforced)
  type empcurtype is ref cursor return employees%rowtype;
  -- define public procedures
  procedure open_emp_cv (emp_cv in out empcurtype);
  procedure list_emp_cv (emp_cv in empcurtype);
  procedure close_emp_cv (emp_cv in empcurtype);
end emp_data;

-- package body
create or replace package body emp_data as
  -- procedure receives cursor var as parameter 
  -- and writes on it.
  -- NOTICE: that the SQL is fixed. You can't change 
  -- parameters without recompiling the package body.
  procedure open_emp_cv(emp_cv in out empcurtype) is
  begin
     open emp_cv for select * 
     from employees
     where rownum <=5
     order by hire_date;
  end open_emp_cv;
  
  -- procedure receives cursor var as parameter.
  -- read only. 
  procedure list_emp_cv (emp_cv in empcurtype) is 
   v_emp employees%rowtype;
  begin
  -- loop through all cursor records.
  -- exit loop when last row reached.
  -- each FETCH increments %ROWCOUNT attribute.
  loop
    fetch emp_cv into v_emp;
    exit when emp_cv%notfound;
     dbms_output.put_line('Oldest #' || emp_cv%rowcount ||': '
                        || v_emp.first_name 
                        ||' '|| v_emp.last_name ||
                        '. Hire date: '|| v_emp.hire_date);
  end loop;
  end list_emp_cv;


  procedure close_emp_cv (emp_cv in empcurtype) is
  begin 
   if emp_cv%isopen then
     dbms_output.put_line('Closing cursor');
     close emp_cv;
   end if;
  end close_emp_cv;
end emp_data;

-- anonymous block
set serveroutput on
declare
  -- declare cursor variable of the same type 
  -- specified in the package spec. 
  v_empcv emp_data.empcurtype;
begin
  emp_data.open_emp_cv(v_empcv);
  emp_data.list_emp_cv(v_empcv);
  emp_data.close_emp_cv(v_empcv);
end;

anonymous block completed
Oldest #1: Steven King. Hire date: 17-JUN-87
Oldest #2: Neena Kochhar. Hire date: 21-SEP-89
Oldest #3: Alexander Hunold. Hire date: 03-JAN-90
Oldest #4: Bruce Ernst. Hire date: 21-MAY-91
Oldest #5: Lex De Haan. Hire date: 13-JAN-93
Closing cursor
Using Cursor expression
set serveroutput on

declare 
 type emp_cur_type is ref cursor;
 
 emp_cur emp_cur_type;
 dept_name departments.department_name%type;
 emp_name  employees.last_name%type;
 
 cursor c1 is 
    select department_name, 
      cursor ( select e.last_name
               from employees e
               where e.department_id = d.department_id
               order by e.last_name
              ) employees
    from departments d
    where department_name like 'A%'
    order by department_name;

begin
  open c1;
  loop
    fetch c1 into dept_name, emp_cur;
    exit when c1%notfound;
    dbms_output.put_line('Dept: ' || dept_name);
    
    loop
     fetch emp_cur into emp_name;
     exit when emp_cur%notfound;
     dbms_output.put_line('-- Employee: '|| emp_name);
    end loop;
  end loop;
  close c1;
end;

anonymous block completed
Dept: Accounting
-- Employee: Gietz
-- Employee: Higgins
Dept: Administration
-- Employee: Fansom
-- Employee: Fay
-- Employee: Fergunson
-- Employee: Glenn
-- Employee: Hartstein
-- Employee: Mavris
-- Employee: Whalen
Using SELECT FOR UPDATE and FOR UPDATE Cursors