CS 579 Database Systems

advertisement
Theory, Practice & Methodology
of Relational Database
Design and Programming
Copyright © Ellis Cohen 2002-2008
Embedded
Database Programming
Using PL/SQL
These slides are licensed under a Creative Commons
Attribution-NonCommercial-ShareAlike 2.5 License.
For more information on how you may use them,
please see http://www.openlineconsult.com/db
1
Overview of Lecture
Basic PL/SQL
Conditional Statements
CASE Expressions & Statements
Loops
Stored Functions
Stored Procedures
Stored Packages
Sequential Values
Exceptions
Record Types
Query-Based FOR Loops
Dynamic SQL
© Ellis Cohen 2001-2008
2
Basic PL/SQL
© Ellis Cohen 2001-2008
3
PL/SQL Block
DECLARE
<declaration statements>
BEGIN
<executable statements>
EXCEPTION
<exception handling code>
END;
© Ellis Cohen 2001-2008
4
Simplest Block
BEGIN
Do
Nothing
NULL;
END;
This block is anonymous (or unnamed)
© Ellis Cohen 2001-2008
5
Hello World
Anonymous (unnamed) PL/SQL block
invoked directly from SQL*Plus
Built-in
package
Procedure in
package
SQL>
BEGIN
dbms_output.put_line( 'Hello World' );
END;
/
SQL*Plus knows how to parse pure SQL, but
not PL/SQL, so it doesn't know when a
PL/SQL block actually ends. Use / to tell it.
© Ellis Cohen 2001-2008
6
Hello World via Stored Procedure
SQL> CREATE PROCEDURE pl( str varchar ) IS
BEGIN
dbms_output.put_line( str );
END;
/
SQL>
BEGIN
pl( 'Hello World' );
END;
/
© Ellis Cohen 2001-2008
7
SQL*Plus EXECUTE
SQL> EXECUTE pl('Hello World')
automatically translated into
SQL>
BEGIN
pl( 'Hello World' );
END;
/
© Ellis Cohen 2001-2008
8
Declaring & Using Variables
Adding 100 to salary of employee 7876
SQL>
PL/SQL variable
DECLARE
declaration &
mgrno int := 7876;
initialization
BEGIN
UPDATE Emps
SET sal = sal + 100
WHERE empno = mgrno;
END;
/
Using PL/SQL variable
in SQL command
PL/SQL does not use a : prefix to identify variables (e.g. :mgrno)
so there is no obvious way to distinguish attributes and variables
© Ellis Cohen 2001-2008
9
Naming Conflicts
Adding 100 to salary of employee 7876
DECLARE
mgrno int := 7876;
BEGIN
UPDATE Emps
SET sal = sal + 100
WHERE empno = mgrno;
END;
Adding 100 to salary of all employees
who are their own manager
DECLARE
mgr int := 7876;
BEGIN
UPDATE Emps
SET sal = sal + 100
WHERE empno = mgr;
END;
The mgr attribute
of Emps overrides
the mgr variable
declaration
© Ellis Cohen 2001-2008
10
Named/Labelled Blocks
Adding 100 to salary of employee 7876
This block is named myBlock
<<myBlock>> DECLARE
mgr int := 7876;
BEGIN
UPDATE Emps
SET sal = sal + 100
WHERE empno = myBlock.mgr;
END;
Indicates that the variable mgr declared in myBlock
should be used instead of emp's mgr attribute.
© Ellis Cohen 2001-2008
11
Using PL/SQL Variables
PL/SQL variable
UPDATE Emps
SET sal = (sal + max_sal) / 2
WHERE empno = 7876;
PL/SQL variable
DELETE FROM Projs
WHERE pname = badPname;
INSERT INTO Projs( pno, pname, pmgr )
VALUES( aPno, aPname, aPmgr );
You can use a PL/SQL
variable wherever you can
use a constant in SQL
PL/SQL variable
© Ellis Cohen 2001-2008
12
PL/SQL is a Typed Language
SQL Data Types:
– CHAR(n), VARCHAR(n), LONG
– NUMBER(n1,n2), INT/INTEGER,
DEC/DECIMAL, SMALLINT, FLOAT/REAL
– DATE
Added PL/SQL-only Data Types
– BOOLEAN
– PLS_INTEGER
-- fast internal integer (can't be NULL)
– structured types (defined later)
© Ellis Cohen 2001-2008
13
Variable Declarations
job_title varchar(80) := 'Salesman';
the_date date := NULL;
is_cool boolean; /* init to NULL */;
counter int := 7;
Anchored
incr CONSTANT int := 1;
declaration:
Whatever the type is
anEname varchar(30)
of the attribute
NOT NULL := 'NONE';
Emps.empno
aDeptno number(5);
anEmpno Emps.empno%TYPE;
xEmpno anEmpno%TYPE;
© Ellis Cohen 2001-2008
14
Assignment Statements
aDeptno := 10;
counter := counter + 1;
SELECT ename, deptno
INTO anEname, aDeptno
FROM Emps
WHERE empno = 7876;
SELECT count(*)
INTO counter
FROM Emps
WHERE deptno = 40;
Raises
exception
• if > 1 row
selected, or
• if 0 rows
selected
Useful Idiom!
Succeeds no
matter how many
rows match the
WHERE clause
© Ellis Cohen 2001-2008
15
Using Single Row Results
DECLARE
anEname varchar(30);
aDeptno int;
BEGIN
SELECT ename, deptno
INTO anEname, aDeptno
FROM Emps
WHERE empno = 7876;
pl( anEname || ' ' || aDeptno );
END;
© Ellis Cohen 2001-2008
16
Variables vs Simple Scalar Subqueries
UPDATE Emps
SET sal = sal + 100
WHERE job =
(SELECT job FROM Emps
WHERE ename = 'BLAKE')
Only slight
performance
penalty
Choice is primarily
aesthetic
DECLARE
blakejob varchar(20);
BEGIN
SELECT job INTO blakejob FROM Emps
WHERE ename = 'BLAKE';
UPDATE Emps
SET sal = sal + 100
WHERE job = blakejob;
END;
© Ellis Cohen 2001-2008
17
RETURNING INTO Delete & Update
DELETE FROM CopyEmps
WHERE empno = 7876
RETURNING ename, deptno
INTO anEname, aDeptno;
Returns information about deleted row
• Raises exception if > 1 rows deleted
• No effect if no rows are deleted
UPDATE CopyEmps SET sal = sal + 100
WHERE empno = 7876
Return value of sal
RETURNING ename, sal
after update
INTO anEname, aSal;
Returns information about updated row
• Raises exception if > 1 rows updated
• No effect if no rows are updated
© Ellis Cohen 2001-2008
18
RETURNING INTO Insert
INSERT INTO CopyEmps( empno, ename, deptno, sal )
VALUES( 6144, 'SONI', 40, getDeptSal( 40 ) )
RETURNING sal INTO theSal;
Returns information about inserted values
INSERT INTO CopyEmps
SELECT * FROM Emps
WHERE empno = 7876
RETURNING ename, deptno
INTO anEname, aDeptno;
Not legal, sigh ….
© Ellis Cohen 2001-2008
19
Conditional
Statements
© Ellis Cohen 2001-2008
20
Conditional Statements
IF profit > 50000 THEN
UPDATE Emps SET sal = sal + 200;
UPDATE Bonus SET amt = .004
WHERE job = 'DEPTMGR';
END IF;
IF profit > 50000 THEN
UPDATE Emps SET sal = sal + 200;
UPDATE Bonus SET amt = .004
WHERE job = 'DEPTMGR';
ELSE
UPDATE Emps SET sal = sal + 50
WHERE job = 'DEPTMGR';
END IF;
© Ellis Cohen 2001-2008
This is a
PL/SQL
variable. It
does not
identify data
in a DB table
21
Using NULLs in
Conditional Statements
IF profit > 50000 THEN
UPDATE Emps SET sal = sal + 200;
UPDATE Bonus SET amt = .004
WHERE job = 'DEPTMGR';
END IF;
IF profit <= 50000 THEN
NULL;
ELSE
UPDATE Emps SET sal = sal + 200;
UPDATE Bonus SET amt = .004
WHERE job = 'DEPTMGR';
END IF;
Equivalent if profit is not NULL
© Ellis Cohen 2001-2008
22
Using NULL Statements
to Avoid NULL Checks
IF a = b THEN
NULL;
ELSE
DoSomething();
END IF;
Suppose both a and b can be NULL
Rewrite the above statement with
only a THEN clause.
© Ellis Cohen 2001-2008
23
Doing NULL Checks
IF a = b THEN
NULL;
ELSE
DoSomething();
END IF;
Equivalent
IF (a != b) OR
(a IS NULL) OR (b IS NULL) THEN
DoSomething();
END IF;
© Ellis Cohen 2001-2008
24
Conditionals with ELSIF
IF profit > 50000 THEN
UPDATE Emps SET sal = sal + 200;
UPDATE Bonus SET pct = .004
WHERE job = 'DEPTMGR';
ELSIF profit > 20000 THEN
UPDATE Emps SET sal = sal + 50
WHERE job = 'DEPTMGR';
ELSIF profit > 0 THEN
NULL;
ELSE
DELETE * FROM Emps WHERE job = 'CLERK';
END IF;
© Ellis Cohen 2001-2008
25
Insert/Update Exercise
Write an anonymous PL/SQL block that
does the following to the Projs table:
if project #30420 is not in the table,
INSERT project #30420 with
pname: 'My Project', and
pmgr: 2020
else if it already exists,
UPDATE project #30420 with
pname: 'Your Project', and
pmgr: 2020
© Ellis Cohen 2001-2008
26
Can’t Coerce Scalar Result Sets
DECLARE
thePno int := (SELECT pno FROM Projs
WHERE pno = 30420);
BEGIN
IF thePno IS NULL THEN
INSERT INTO Projs( pno, pname, pmgr )
VALUES( 30420, 'My Project', 2020 );
ELSE
UPDATE Projs
SET pname = 'Your Project', pmgr = 2020
WHERE pno = 30420;
END IF;
END;
DOESN'T WORK!
The SELECT statement produces a result set,
not an integer
© Ellis Cohen 2001-2008
27
Scalar Coercion Doesn’t Work
BEGIN
IF (SELECT count(*) FROM Projs
WHERE pno = 30420) = 0 THEN
INSERT INTO Projs( pno, pname, pmgr )
VALUES( 30420, 'My Project', 2020 );
ELSE
UPDATE Projs
SET pname = 'Your Project', pmgr = 2020
WHERE pno = 30420;
END IF;
END;
DOESN'T WORK!
Coercion of Scalar Result Sets to
scalar values only works in a SQL Query
© Ellis Cohen 2001-2008
28
Exists Doesn't Work
BEGIN
IF exists( SELECT * FROM Projs
WHERE pno = 30420 ) THEN
UPDATE Projs
SET pname = 'Your Project', pmgr = 2020
WHERE pno = 30420;
ELSE
INSERT INTO Projs( pno, pname, pmgr )
VALUES( 30420, 'My Project', 2020 );
END IF;
END;
DOESN'T WORK!
exists can ONLY be used in a SQL QUERY
© Ellis Cohen 2001-2008
29
Empty SELECTs Fail
DECLARE
thePno int;
BEGIN
SELECT pno INTO thePno FROM Projs
WHERE pno = 30420;
IF thePno IS NULL THEN
INSERT INTO Projs( pno, pname, pmgr )
VALUES( 30420, 'My Project', 2020 );
ELSE
UPDATE Projs
SET pname = 'Your Project', pmgr = 2020
WHERE pno = 30420;
END IF;
END;
DOESN'T WORK!
The SELECT statement will cause an error
if Projs doesn't have project 30420
© Ellis Cohen 2001-2008
30
Insert/Update Exercise Answer
DECLARE
knt number(5);
BEGIN
SELECT count(*) INTO knt FROM Projs
WHERE pno = 30420;
IF knt = 0 THEN
INSERT INTO Projs( pno, pname, pmgr )
VALUES( 30420, 'My Project', 2020 );
ELSE
UPDATE Projs
SET pname = 'Your Project', pmgr = 2020
WHERE pno = 30420;
END IF;
END;
© Ellis Cohen 2001-2008
31
Using RETURNING INTO
DECLARE
thePno int;
BEGIN
UPDATE Projs
SET pname = 'Your Project', pmgr = 2020
WHERE pno = 30420
RETURNING pno INTO thePno;
IF thePno IS NULL THEN
INSERT INTO Projs( pno, pname, pmgr )
VALUES( 30420, 'My Project', 2020 );
END IF;
END;
thePno will remain NULL
if nothing is updated
© Ellis Cohen 2001-2008
32
Using SQL%ROWCOUNT
BEGIN
UPDATE Projs
SET pname = 'Your Project', pmgr = 2020
WHERE pno = 30420;
IF SQL%ROWCOUNT = 0 THEN
INSERT INTO Projs( pno, pname, pmgr )
VALUES( 30420, 'My Project', 2020 );
END IF;
END;
Better approach!
SQL%ROWCOUNT
returns the number of rows affected by
the previous SQL command –
in this case,
the number of rows updated.
© Ellis Cohen 2001-2008
33
Other Insert/Update Approaches
Oracle 10g:
MERGE INTO Projs p
USING DUAL ON (p.pno = 30420)
WHEN MATCHED THEN
UPDATE SET
pname = 'Your Project', pmgr = 2020
WHEN NOT MATCHED THEN
INSERT ( pno, pname, pmgr )
VALUES( 30420, 'My Project', 2020 )
MySQL 5.0:
INSERT INTO Projs( pno, pname, pmgr )
VALUES( 30420, 'My Project', 2020 )
ON DUPLICATE KEY UPDATE
SET pname = 'Your Project', pmgr = 2020
© Ellis Cohen 2001-2008
34
Case
Expressions &
Statements
© Ellis Cohen 2001-2008
35
Simple CASE Expression
salincr := CASE
WHEN profit > 50000 THEN 200
WHEN profit > 20000 THEN 50
WHEN profit > 0 THEN 0
ELSE –profit/200
END;
Returns the value
associated with
the first
expression which
evaluates to TRUE
Similar to SQL case expressions
© Ellis Cohen 2001-2008
36
ELSE NULL Implied
salincr := CASE
WHEN profit > 50000 THEN 200
WHEN profit > 20000 THEN 50
WHEN profit > 0 THEN 0
END;
Equivalent
ELSE NULL
implied if no
explicit ELSE
clause
salincr := CASE
WHEN profit > 50000 THEN 200
WHEN profit > 20000 THEN 50
WHEN profit > 0 THEN 0
ELSE NULL
END;
© Ellis Cohen 2001-2008
37
Searched CASE Expression
salincr := CASE cooljob
WHEN 'DEPTMGR' THEN 500
WHEN 'ANALYST' THEN 300
ELSE 150
END;
Equivalent
salincr := CASE
WHEN cooljob = 'DEPTMGR' THEN 500
WHEN cooljob = 'ANALYST' THEN 300
ELSE 150
END;
© Ellis Cohen 2001-2008
38
Simple CASE Statement
SELECT job INTO cooljob FROM Emps
WHERE empno = coolemp;
CASE
WHEN cooljob = 'DEPTMGR' THEN
bonuspct := .004;
salincr := 500;
WHEN cooljob = 'ANALYST' THEN
salincr := 300;
Only the statements
ELSE
matching the first
expression which
salincr := 150;
evaluates to TRUE
END CASE;
are executed
ELSE NULL implied here as well if no explicit ELSE clause
© Ellis Cohen 2001-2008
39
Searched CASE Statement
SELECT job INTO cooljob FROM Emps
WHERE empno = coolemp;
CASE cooljob
WHEN 'DEPTMGR' THEN
bonuspct := .004;
salincr := 500;
WHEN 'ANALYST' THEN
salincr := 300;
ELSE
Only the statements
matching the first value
salincr := 150;
found are executed
END CASE;
© Ellis Cohen 2001-2008
40
Conditional vs. Simple CASE
Statements
CASE
WHEN profit > 50000 THEN
UPDATE Emps SET sal = sal + 200;
UPDATE Bonus SET pct = .004
WHERE job = 'DEPTMGR';
WHEN profit > 20000 THEN
UPDATE Emps SET sal = sal + 50
WHERE job = 'DEPTMGR';
WHEN profit > 0
NULL;
ELSE
DELETE * FROM Emps
WHERE job = 'CLERK';
END CASE;
© Ellis Cohen 2001-2008
41
Loops
© Ellis Cohen 2001-2008
42
For Loops
FOR LOOP variables are
automatically declared
These don't need to be constants;
they can be arbitrary expression
BEGIN
FOR i IN 1 .. 10 LOOP
INSERT INTO Projs ( pno, pname, pmgr )
VALUES( i, 'Base Project ' || i, 7789 );
END LOOP;
END;
ALWAYS incr by 1, except can also write
FOR i IN REVERSE 1 .. 10
© Ellis Cohen 2001-2008
43
While Loops
Get the first employee tracked by mypkg
DECLARE
anEmpno NUMBER(5) := mypkg.getFirstEmp();
BEGIN
WHILE anEmpno IS NOT NULL LOOP
DELETE FROM Asns
WHERE empno = anEmpno;
anEmpno := mypkg.getNextEmp();
END LOOP;
END;
Get the next employee tracked by mypkg,
return NULL when none left
© Ellis Cohen 2001-2008
44
Loops with Exit
LOOP
Elided code that computes credit rating
...
IF credit_rating < 3 THEN
EXIT;
END IF;
...
END LOOP;
LOOP
EXITs can also be used with
FOR & WHILE loops,
though some authors
frown on this practice
...
EXIT WHEN credit_rating < 3;
...
END LOOP;
© Ellis Cohen 2001-2008
45
Labelled Loops with Exit
outer loop, labelled this_one
<<this_one>> LOOP
...
LOOP inner loop
...
EXIT this_one WHEN credit_rating < 3;
...
END LOOP;
...
END LOOP this_one;
Optional, but recommended
© Ellis Cohen 2001-2008
46
Loop Exercise
lst is a list of employee numbers (all of which
are guaranteed to identify employees in the
Emps table) [don't be concerned, for now, with how
lst is represented]
IntLists.length( lst )
returns the length of lst (i.e. how many
employee's numbers are in lst)
IntLists.get( lst, nth )
returns the nth employee number in lst
Find the first employee listed in lst whose
commission is larger than 2000, and add that
employee's number and name to the Winners
table
© Ellis Cohen 2001-2008
47
Loop Exercise Answer
DECLARE
anEmpno Emps.empno%type;
anEname Emps.ename%type;
aComm Emps.comm%type;
BEGIN
FOR knt in 1..IntLists.length(lst) LOOP
anEmpno := IntLists.nth( lst, knt );
SELECT ename, comm INTO anEname, aComm
FROM Emps WHERE empno = anEmpno;
IF (aComm > 2000) THEN
INSERT INTO winners
VALUES( anEmpno, anEname );
EXIT;
END IF;
END LOOP;
END;
© Ellis Cohen 2001-2008
48
Stored
Functions
© Ellis Cohen 2001-2008
49
Defining & Using Stored Functions
SQL> CREATE FUNCTION sqr( num int ) RETURN int IS
BEGIN
RETURN num * num;
END;
/
SQL> EXECUTE pl( sqr( 7 ) )
-- displays 49
SQL> DECLARE
sqrval int;
BEGIN
sqrval := sqr( 7 );
pl( sqrval );
END;
/
Functions always
return a value.
The RETURN clause
declares the type of
value returned
© Ellis Cohen 2001-2008
50
CREATE or REPLACE
optional
SQL> CREATE OR REPLACE
FUNCTION sqr( num int ) RETURN int IS
BEGIN
RETURN num * num;
END;
/
First drops the function before redefining it.
If just CREATE is used, and the function is
already defined, the CREATE will fail.
© Ellis Cohen 2001-2008
51
Using Stored Functions from SQL
SQL> CREATE FUNCTION sqr( num int )
RETURN int IS
BEGIN
RETURN num * num;
END;
/
SQL> SELECT empno, sal, sqr( sal )
FROM Emps;
© Ellis Cohen 2001-2008
52
The DUAL Table
SELECT ename, sysdate FROM Emps
Lists the employee name of all 14 employees,
along with today's date
SELECT sysdate FROM Emps
Lists today's date 14 times
sysdate is a built-in
parameterless
function
which returns
the current date
SELECT sysdate FROM DUAL
DUAL is a built-in unmodifiable table which has
one column (DUMMY) and one row
Lists today's date ONCE!
© Ellis Cohen 2001-2008
53
Parameterless Functions
SQL> CREATE OR REPLACE
FUNCTION getDate RETURN date
IS
dat date;
Parameterless
BEGIN
SELECT sysdate INTO dat FROM DUAL;
RETURN dat;
END;
/
Built-in functions
can ONLY be
SQL> execute pl( getDate )
used in SQL
SQL> execute pl( getDate() )
commands!
User-defined parameterless functions
can be called with or without
parentheses from PL/SQL
© Ellis Cohen 2001-2008
54
Parameterless Functions from SQL
SQL> CREATE OR REPLACE
FUNCTION getDate RETURN date
IS
dat date;
Parameterless
BEGIN
SELECT sysdate INTO dat FROM DUAL;
RETURN dat;
END;
/
SQL> SELECT * FROM Projs
WHERE pstart > getDate
User-defined parameterless functions
MUST be called without parentheses
from SQL
© Ellis Cohen 2001-2008
55
Stored
Procedures
© Ellis Cohen 2001-2008
56
Functions & Procedures
Functions
– can return a value
– should not have side effects (this allows
calls using them to be optimized)
Procedures
– do not have a return value, but can
return results through OUT parameters
– may have arbitrary side effects
© Ellis Cohen 2001-2008
57
Defining & Using Stored Procedures
SQL> CREATE OR REPLACE
PROCEDURE DeleteEmployee( anEmpno int ) IS
BEGIN
DELETE FROM Emps
WHERE empno = anEmpno;
END;
/
SQL> EXECUTE DeleteEmployee( 3142 )
© Ellis Cohen 2001-2008
58
OUT Parameters
SQL> CREATE OR REPLACE
PROCEDURE DeleteEmployee(
anEmpno int, theDeptno OUT int ) IS
BEGIN
DELETE FROM Emps
WHERE empno = anEmpno
RETURNING deptno INTO theDeptno;
END;
An OUT parameter can be used
/
like a variable, but also passes
a value back to the caller when
the procedure finishes
SQL> DECLARE
deldept int;
BEGIN
DeleteEmployee( 3142, deldept );
pl( 'Employee deleted from dept ' || deldept );
END;
/
There can be any number of OUT parameters
© Ellis Cohen 2001-2008
59
Named Parameter Notation
SQL> CREATE OR REPLACE
PROCEDURE DeleteEmployee(
anEmpno int, theDeptno OUT int ) IS
BEGIN
DELETE FROM Emps
WHERE empno = anEmpno
RETURNING deptno INTO theDeptno;
END;
/
SQL> DECLARE
deldept int;
BEGIN
DeleteEmployee( theDeptno => deldept,
anEmpno => 3142 );
pl( 'Employee deleted from dept ' || deldept );
END;
/
© Ellis Cohen 2001-2008
60
IN OUT Parameters
SQL> CREATE OR REPLACE
PROCEDURE SwapSal(
anEmpno int, theSal IN OUT number )
IS
oldsal number;
BEGIN
IN OUT
SELECT sal INTO oldsal FROM Emps
parameters
WHERE empno = anEmpno;
UPDATE Emps SET sal = theSal
• pass values
WHERE empno = anEmpno;
to the
procedure
theSal := oldsal;
when it is
END;
called
/
• pass values
back to the
SQL> DECLARE
caller when
mysal number := 1000;
the procedure
BEGIN
finishes
SwapSal( 3142, mysal );
pl( 'Previous sal was ' || mysal );
END;
/
© Ellis Cohen 2001-2008
61
Stored
Packages
© Ellis Cohen 2001-2008
62
Role of Packages
A package is used to group
together a cohesive set of
stored database operations
(procedures and functions)
• They all provide related
functionality, and/or
• They use the same set of
variables/tables/views
© Ellis Cohen 2001-2008
63
Package Descriptions
SQL> CREATE OR REPLACE PACKAGE MyUtil AS
FUNCTION getDate RETURN date;
FUNCTION getNumColumns( tblnam varchar )
RETURN int;
END MyUtil;
This only describes the operations in the package;
not their implementation.
That's done separately in the package body.
Invoking packaged functions
SQL> execute pl( MyUtil.getDate() )
SQL> execute pl( MyUtil.getNumColumns( 'Emps' ) )
SQL> SELECT * FROM Projs
WHERE pstart > MyUtil.getDate;
© Ellis Cohen 2001-2008
64
Package Body
SQL> CREATE OR REPLACE PACKAGE BODY MyUtil AS
FUNCTION getDate RETURN date IS
dat date;
BEGIN
SELECT sysdate INTO dat FROM dual;
RETURN dat;
END;
FUNCTION getNumColumns( tblnam varchar )
RETURN int
IS
numcols int;
BEGIN
SELECT count(*) INTO numcols
FROM user_tab_columns
WHERE table_name = upper(tblnam);
RETURN numcols;
END;
END MyUtil;
© Ellis Cohen 2001-2008
65
Table-Based Packages
A package of operations dealing with the Emps table
PACKAGE EmpPkg AS
PROCEDURE ChangeSal( anEmpno int, aSal number );
-- Changes the salary of an employee
PROCEDURE ChangeJob( anEmpno int, aJob varchar );
-- Changes the job of some other employee
PROCEDURE ChangeMgr( anEmpno int, aMgr int );
-- Changes mgr of an employee
PROCEDURE ChangePosition( anEmpno int, aMgr int,
aDeptno int, aJob varchar );
-- Changes one or more of job/dept/mgr of employee
PROCEDURE AddEmp( anEmpno int, anEname varchar,
aSal number, aJob varchar, aMgr int, aDeptno int );
-- Adds an employee to a department
PROCEDURE TerminateEmp( anEmpno int );
-- Terminates an employee
END EmpPkg;
It can also be useful for a package to encapsulate the
procedures & functions for a group of related tables
© Ellis Cohen 2001-2008
66
Table-Based Package Body
PACKAGE BODY EmpPkg AS
…
PROCEDURE ChangePosition(
anEmpno int, aMgr int, aDeptno int, aJob varchar ) IS
BEGIN
UPDATE Emps SET mgr = aMgr, deptno = aDeptno, job = aJob
WHERE empno = anEmpno;
END;
-------------------PROCEDURE AddEmp( anEmpno int, anEname varchar,
aSal number, aJob varchar, aMgr int, aDeptno int ) IS
BEGIN
INSERT INTO Emps( empno, ename, sal, job, mgr, deptno )
VALUES ( anEmpno, anEname, aSal, aJob, aMgr, aDeptno );
END;
-------------------PROCEDURE TerminateEmp( anEmpno int ) IS
BEGIN
DELETE FROM Emps WHERE empno = anEmpno;
END;
END EmpPkg;
© Ellis Cohen 2001-2008
67
Sequential
Values
© Ellis Cohen 2001-2008
68
Hiding Implementation Details
Given a relational model with
Asns
asnid
empno
pno
hrs
int primary key
int references Emps
int references Projs
int
We would like to write a procedure
AddAssignment( empno, pno, hrs )
which would add a new tuple to Asns
but automatically fill in asnid
© Ellis Cohen 2001-2008
69
Hiding Id Assignment
CREATE SEQUENCE asnseq START WITH 1000;
-- Defines a sequence used to generate
-- sequential values.
-- Can specify start value and increment
CREATE OR REPLACE PROCEDURE AddAssignment(
anEmpno int, aPno int, theHrs number )
IS
BEGIN
The first value
generated will be 1001
INSERT INTO Asns
VALUES( asnseq.nextval,
anEmpno, aPno, theHrs );
END;
nextval is a parameterless
function which gets the next
value from the sequence
Suppose you want
to return the value
as well as insert it?
© Ellis Cohen 2001-2008
70
Returning Assigned Id's
CREATE SEQUENCE asnseq START WITH 1001;
-- Defines a sequence used to generate
-- sequential values. Can specify start value and increment
CREATE OR REPLACE PROCEDURE AddAssignment(
anEmpno int, aPno int, theHrs number, OUT theId int )
IS
BEGIN
SELECT asnseq.nextval INTO theId
FROM DUAL;
-- nextval is an Oracle built-in parameter-less
-- function which gets the next value from sequence,
-- but can ONLY be called from SQL code
-- DUAL is a convenient,
-- built-in single row/column table
INSERT INTO Asns
VALUES( theId, anEmpno, aPno, theHrs );
END;
theId := asnseq.nextval DOESN'T WORK!
asnseq.nextval must be used in a SQL statement
© Ellis Cohen 2001-2008
71
Using RETURNING INTO
CREATE SEQUENCE asnseq START WITH 1001;
CREATE OR REPLACE PROCEDURE AddAssignment(
anEmpno int, aPno int, theHrs number,
OUT theId int )
IS
Associates this value
with the asnid attribute
BEGIN
INSERT INTO Asns
VALUES( asnseq.nextval,
anEmpno, aPno, theHrs )
RETURNING asnid INTO theId;
END;
Also, asnseq.currval gets the current value
of the sequence, without incrementing it!
© Ellis Cohen 2001-2008
72
Data-Specific Sequences
Given a relational model with
Entries
invid
linenum
prodid
qty
primary key
int references Invoices
int
int
int
( invid, linenum )
We would like to write a procedure
AddEntry
which would add a new tuple to Entries but
automatically fill in the next linenum
© Ellis Cohen 2001-2008
73
Computing Next Sequence Number
SELECT 1 + nvl(max(linenum),0)
INTO nxtlin
FROM Entries WHERE invid = anInvid;
nxtlin will get 1 more
than the highest linenum for anInvid
Because this uses an aggregate function (MAX),
this SELECT always succeeds and returns a result,
even if there are no current entries for anInvid
In that case, MAX returns NULL,
and because of nvl, nxtlin will get 1
© Ellis Cohen 2001-2008
74
Inserting Next Sequence Number
CREATE OR REPLACE PROCEDURE AddEntry(
anInvid int, aProdid int, aQty int )
IS
nxtlin int;
BEGIN
SELECT 1 + nvl(max(linenum),0)
INTO nxtlin
FROM Entries WHERE invid = anInvid;
INSERT INTO Entries VALUES(
anInvid, nxtlin, aProdid, aQty);
END;
Later, we'll see how to use Dynamic SQL to
define a sequence for each invoice ID.
But assuming the number of invoices is large,
and the number of lines per invoice is small, it's
reasonable to just compute the next linenum
© Ellis Cohen 2001-2008
75
Exceptions
© Ellis Cohen 2001-2008
76
Exceptions
An anonymous block
ZERO_DIVIDE
exception raised if
earnings is 0
DECLARE
pe_ratio number(5,1);
BEGIN
SELECT price / earnings INTO pe_ratio
FROM Stocks WHERE symbol = :sym;
INSERT INTO Stats( symbol, ratio )
VALUES( :sym, pe_ratio );
END;
If an exception is raised
• the INSERT is not executed
• the exception is reported
to the user
© Ellis Cohen 2001-2008
:sym is a middle-tier
variable, not a
declared local
variable
77
Catching ZERO_DIVIDE
DECLARE
pe_ratio number(5,1);
BEGIN
SELECT price / earnings INTO pe_ratio
FROM Stocks WHERE symbol = :sym;
INSERT INTO Stats( symbol, ratio )
VALUES( :sym, pe_ratio );
EXCEPTION
WHEN ZERO_DIVIDE THEN
INSERT INTO Stats( symbol, ratio )
VALUES( :sym, NULL );
END;
If the exception is raised, control jumps to the
EXCEPTION clause, where an exception handler
for ZERO_DIVIDE has code for an alternative
INSERT statement which is executed instead
© Ellis Cohen 2001-2008
78
Discarding All Other Exceptions
Suppose some other kind of
DECLARE
error is raised while
executing this code
pe_ratio number(3,1);
BEGIN
SELECT price / earnings INTO pe_ratio
FROM Stocks WHERE symbol = :sym;
INSERT INTO Stats( symbol, ratio )
VALUES( :sym, pe_ratio );
EXCEPTION
WHEN ZERO_DIVIDE THEN
INSERT INTO Stats( symbol, ratio )
VALUES( :sym, NULL );
WHEN OTHERS THEN
NULL;
END;
WHEN OTHERS catches all other exceptions
NULL does nothing (in response)
© Ellis Cohen 2001-2008
79
Raise Application Error
DECLARE
pe_ratio number(3,1);
BEGIN
SELECT price / earnings INTO pe_ratio
FROM Stocks WHERE symbol = :sym;
INSERT INTO Stats( symbol, ratio )
VALUES( :sym, pe_ratio );
EXCEPTION
WHEN ZERO_DIVIDE THEN
INSERT INTO Stats( symbol, ratio )
VALUES( :sym, NULL );
WHEN OTHERS THEN
RAISE_APPLICATION_ERROR( -20069,
'Couldn''t Calculate PE for ' || :sym );
END;
Reports a more descriptive error
Note: the error number must be in the range
-20000 to -20999
© Ellis Cohen 2001-2008
80
Propagating Exceptions
BEGIN
DoSomething();
EXCEPTION WHEN OTHERS THEN
pl( 'Problem doing something' );
END;
PROCEDURE DoSomething() IS
BEGIN
…
RAISE_APPLICATION_ERROR( -20069,
'Some problem encountered' );
…
END
Exceptions not caught within a procedure/function
are propagated to the site where it was called
© Ellis Cohen 2001-2008
81
Exceptional Insert/Update
Attempting to insert a row into a table will raise an
exception if another tuple in that table has the
same primary key value
Use that information to write an anonymous PL/SQL
block that does the following without using
SELECT, MERGE, or SQL%ROWCOUNT or
RETURNING:
if a project does not exist with project # 30420,
INSERT it with
pname: 'My Project', and
pmgr: 2020
else if it already exists,
UPDATE it with
pname: 'Your Project', and
pmgr: 2020
© Ellis Cohen 2001-2008
82
Exceptional Insert/Update Answer
BEGIN
INSERT INTO Projs( pno, pname, pmgr )
VALUES( 30420, 'My Project', 2020 )
EXCEPTION WHEN OTHERS THEN
UPDATE Projs
SET pname = 'Your Project', pmgr = 2020
WHERE pno = 30420;
END;
Although using SQL%ROWCOUNT
is generally more efficient
© Ellis Cohen 2001-2008
83
Reraising Exceptions
PROCEDURE DoSomething IS
BEGIN
-- Do something complicated
-- which could raise an exception
EXCEPTION WHEN OTHERS THEN
IF … THEN
-- under some circumstances,
-- we can simply
-- do something else
ELSE
-- but in other circumstances
-- we propagate the exception
-- back to the caller
RAISE;
END IF;
END;
© Ellis Cohen 2001-2008
84
Non-Existence Exceptions
If there is no employee with empno anEmpno,
raise an exception
BEGIN
SELECT count(*) INTO knt FROM Emps
WHERE empno = anEmpno;
IF knt = 0 THEN
RAISE_APPLICATION_ERROR( … );
END IF;
Equivalent result;
END;
same performance
(since at most one such employee)
BEGIN
SELECT empno INTO theEmpno FROM Emps
WHERE empno = anEmpno;
EXCEPTION WHEN OTHERS THEN
RAISE_APPLICATION_ERROR( … );
END;
© Ellis Cohen 2001-2008
85
Record Types
© Ellis Cohen 2001-2008
86
Record Types
DECLARE
TYPE NameDeptRec IS RECORD (
ename varchar(30),
deptno int );
aRec NameDeptRec;
BEGIN
SELECT ename, deptno
INTO aRec.ename, aRec.deptno
FROM Emps
WHERE empno = 7876;
INSERT INTO CoolEmps( ename, deptno )
VALUES( aRec.ename, aRec.deptno );
END;
© Ellis Cohen 2001-2008
87
Record Based Select/Insert
DECLARE
TYPE NameDeptRec IS RECORD (
ename varchar(30),
deptno int );
aRec NameDeptRec;
BEGIN
SELECT ename, deptno INTO aRec
FROM Emps
WHERE empno = 7876;
INSERT INTO CoolEmps( ename, deptno )
VALUES aRec;
END;
match based on
position, not name
© Ellis Cohen 2001-2008
88
Using ROWTYPE
You can declare a record whose type
corresponds to a row of a table or view
DECLARE
aRec CoolEmps%ROWTYPE;
BEGIN
SELECT ename, deptno INTO aRec
FROM Emps
WHERE empno = 7876;
INSERT INTO CoolEmps( ename, deptno )
VALUES aRec;
END;
© Ellis Cohen 2001-2008
89
Pretty Printing Tuples
SELECT ename, street, city, state, zip
FROM Emps
WHERE empno = 7876;
DECLARE
aRec Emps%ROWTYPE;
BEGIN
SELECT * INTO aRec FROM Emps
WHERE empno = 7876;
pl( aRec.ename );
pl( aRec.street );
pl( aRec.city || ', ' || aRec.state || ', ' || aRec.zip );
pl( '' );
END;
© Ellis Cohen 2001-2008
90
Record Based Update
PROCEDURE IncludeProject(
pRec Projs%ROWTYPE ) IS
BEGIN
INSERT INTO Projs VALUES pRec;
EXCEPTION
Built-in; a
record with that
WHEN DUP_VAL_ON_INDEX
index is already
in the table
THEN
UPDATE Projs SET ROW = pRec
WHERE pno = pRec.pno;
END;
© Ellis Cohen 2001-2008
91
Query-Based
FOR Loops
© Ellis Cohen 2001-2008
92
Query-Based FOR Loops
FOR i IN 1 .. 10
LOOP … END LOOP
loops through the numbers from 1..10
FOR rec IN (SELECT …) LOOP … END LOOP
loops through the tuples resulting from the SELECT
BEGIN
pl( '
Name
Dept#');
Query-Based FOR
pl('-----------------------------');
Loops automatically
declare the loop
FOR erec IN
variable to be
(SELECT ename, deptno
consistent with the
FROM Emps
tuples in the result set
WHERE job = 'ANALYST')
LOOP
pl( rpad( erec.ename,15) || erec.deptno );
END LOOP;
END;
Very similar to SQL*Plus
display of the bare SELECT
© Ellis Cohen 2001-2008
93
Queries as Loops
CREATE TABLE Results AS
SELECT empno, ename
FROM Emps e
WHERE sal > 1500
CREATE TABLE Results
(empno int, ename varchar(30));
BEGIN
FOR erec IN (SELECT * FROM Emps
WHERE sal > 1500) LOOP
INSERT INTO Results
VALUES ( erec.empno, erec.ename );
END LOOP;
END;
Don't fill a table this way!
We're just using PL/SQL as a way of
describing how queries work
© Ellis Cohen 2001-2008
94
Joins as Nested Loops
CREATE TABLE Results AS
SELECT ename, dname FROM Emps e, Depts d
WHERE e.deptno = d.deptno
AND e.sal > 1500
CREATE TABLE Results
(ename varchar(30), dname varchar(16));
BEGIN
FOR drec IN (SELECT * FROM Depts) LOOP
FOR erec IN (SELECT * FROM Emps
WHERE deptno = drec.deptno
AND sal > 1500) LOOP
INSERT INTO Results
VALUES( erec.ename, drec.dname );
END LOOP;
END LOOP;
Don't fill a table this way!
END;
We're just using PL/SQL as a way of
describing how queries work
© Ellis Cohen 2001-2008
95
Bare SELECT Problem
BEGIN
DoSomething();
SELECT empno, ename
FROM Emps
WHERE job = 'CLERK';
END;
This anonymous block is illegal
Why?
Write code that works, still written as a single
anonymous block
© Ellis Cohen 2001-2008
96
Eliminate Bare SELECTs
BEGIN
DoSomething();
FOR erec IN
(SELECT empno, ename
FROM Emps
WHERE job = 'CLERK')
LOOP
pl( erec.empno || ': ' || erec.ename );
END LOOP;
END;
Can't use a bare SELECT as a PL/SQL statement.
Can use a Query For Loop instead
© Ellis Cohen 2001-2008
97
List Employees in Each Dept
ACCOUNTING: CLARK, KING, MILLER
OPERATIONS
RESEARCH: SMITH, JONES, SCOTT, ADAMS, FORD
SALES: ALLEN, WARD, MARTIN, BLAKE, TURNER, JAMES
DECLARE
fstr varchar(200);
sep varchar(5);
BEGIN
FOR drec IN
(SELECT deptno, dname FROM Depts ORDER BY dname)
LOOP
fstr := drec.dname;
sep := ': ';
FOR erec IN
(SELECT ename FROM Emps
WHERE deptno = drec.deptno) LOOP
fstr := fstr || sep || erec.ename;
sep := ', ';
END LOOP;
pl( fstr );
END LOOP;
END;
© Ellis Cohen 2001-2008
98
Existence Exceptions
Raise an error if dept aDeptno has any employees
BEGIN
SELECT count(*) INTO knt FROM Emps
WHERE deptno = aDeptno;
IF knt > 0 THEN
RAISE_APPLICATION_ERROR( … );
END IF;
END;
FOR erec IN (
SELECT empno FROM Emps
WHERE deptno = aDeptno) LOOP
RAISE_APPLICATION_ERROR( … );
END LOOP;
This code is more efficient if there are many such employees,
and there is no index on deptno, since it can quit when the first
such employee is found, rather than after counting all of them
© Ellis Cohen 2001-2008
99
Unnecessary Looped UPDATEs
Increase salary of all project managers
FOR prec IN
(SELECT DISTINCT pmgr FROM Projects) LOOP
UPDATE Emps SET sal = sal + 100
WHERE empno = prec.pmgr;
END LOOP;
UPDATE Emps SET sal = sal + 100
WHERE empno IN
(SELECT pmgr FROM Projects)
This code is
much more
efficient
In general, avoid loops
when equivalent code can be written without loops
© Ellis Cohen 2001-2008
100
Unnecessary Looped SELECTs
Raise an error if some dept has no employees
DECLARE
knt int;
BEGIN
FOR drec IN (
SELECT deptno FROM Depts) LOOP
SELECT count(*) INTO knt FROM Emps
WHERE deptno = drec.deptno;
IF knt = 0 THEN
RAISE_APPLICATION_ERROR( -20023,
'There is a dept with no employees' );
END LOOP;
END;
The inner SELECT statement can
be avoided by using a more
complicated outer SELECT
© Ellis Cohen 2001-2008
101
Handling Multiple Errors
Raise an error if some dept has no employees
CREATE VIEW DeptKntsView AS
SELECT deptno, count(empno) AS eknt
FROM Depts NATURAL LEFT JOIN Emps
GROUP BY deptno;
BEGIN
FOR drec IN (
SELECT deptno FROM DeptKntsView
WHERE eknt = 0) LOOP
RAISE_APPLICATION_ERROR( -20023,
'There is a dept with no employees' );
END LOOP;
END;
This code works, but suppose we
want to indicate exactly which
departments don't have employees.
What can we do?
© Ellis Cohen 2001-2008
102
Identifying the First Error
Raise an error if some dept has no employees
CREATE VIEW DeptKntsView AS
SELECT deptno, count(empno) AS eknt
FROM Depts NATURAL LEFT JOIN Emps
GROUP BY deptno;
BEGIN
FOR drec IN (
SELECT deptno FROM DeptKntsView
WHERE eknt = 0) LOOP
RAISE_APPLICATION_ERROR( -20023,
'Dept ' || drec.deptno || ' has no employees' );
END LOOP;
END;
This only identifies the first such
department. Suppose we want to
identify all of them?
© Ellis Cohen 2001-2008
103
Building Informative Error Messages
Identify all depts which have no employees
DECLARE
empties varchar(200) := '';
sep varchar(2) := '';
BEGIN
FOR drec IN (
SELECT deptno FROM DeptKntsView
WHERE eknt = 0) LOOP
empties := empties || sep || drec.deptno;
sep := ', ';
END LOOP;
IF length(empties) > 0 THEN
RAISE_APPLICATION_ERROR( -20023,
'Depts without employees: ' || empties );
END IF;
END;
© Ellis Cohen 2001-2008
104
Dynamic
SQL
© Ellis Cohen 2001-2008
105
Dynamic SQL
PROCEDURE InsertProject(
aPno number, aPname varchar, aPmgr number )
IS BEGIN
INSERT INTO Projs( pno, pname, pmgr )
VALUES( aPno, aPname, aPmgr );
END;
PROCEDURE InsertProject(
aPno number, aPname varchar, aPmgr number )
IS
Dynamically construct &
sqlstr varchar(100);
execute a SQL command!
BEGIN
sqlstr :=
'INSERT INTO Projs( pno, pname, pmgr ) ' ||
' VALUES( ' || aPno || ', ' ||
quote(aPname) || ', ' || aPmgr || ' )';
EXECUTE IMMEDIATE sqlstr;
END;
© Ellis Cohen 2001-2008
106
Runtime Table Names & Conditions
PROCEDURE DeleteRows(
table_name varchar, condition varchar )
IS
where_clause varchar(100);
BEGIN
IF condition IS NOT NULL THEN
where_clause := ' WHERE ' || condition;
END IF;
EXECUTE IMMEDIATE
'DELETE FROM ' || table_name || where_clause;
END;
EXECUTE IMMEDIATE works with
PL/SQL blocks as well as SQL statements!
© Ellis Cohen 2001-2008
107
Runtime DDL
PROCEDURE DropTable( tbl varchar ) IS
sqlstr varchar(99) := 'drop table ' || tbl;
BEGIN
EXECUTE IMMEDIATE sqlstr;
EXCEPTION
WHEN OTHERS THEN NULL;
END;
Prevents error message if
table doesn't exist.
DDL statements (CREATE, DROP, …)
cannot be executed inside of
PL/SQL except via Dynamic SQL
© Ellis Cohen 2001-2008
108
Creating Sequences Dynamically
CREATE OR REPLACE PROCEDURE AddEntry(
anInvid int, aProdid int, aQty int )
IS
seqstr varchar(20) := 'InvSeq' + anInvid;
insstr varchar(100) :=
'INSERT INTO Entries VALUES (' ||
'anInvid, ' || seqstr || '.nextval, aProdid, aQty)';
BEGIN
EXECUTE IMMEDIATE insstr;
EXCEPTION WHEN OTHERS THEN
EXECUTE IMMEDIATE 'CREATE SEQUENCE ' || seqstr;
EXECUTE IMMEDIATE insstr;
END;
Dynamically creates a sequence for an invoice if the
sequence doesn’t already exist (causing an exception).
For invoice 347, the sequence will be named InvSeq347
© Ellis Cohen 2001-2008
109
Execute Immediate INTO
FUNCTION SelectEmployee( condition in varchar )
RETURN Emps.empno%TYPE
IS
sqlstr varchar(200);
anEmp Emps.empno%TYPE;
BEGIN
sqlstr := 'SELECT FROM ' || table_name ||
' WHERE ' || condition;
EXECUTE IMMEDIATE sqlstr INTO anEmp;
RETURN anEmp;
END;
Allows SQL query to be reused with
different INTO clauses
© Ellis Cohen 2001-2008
110
Parameterized Dynamic SQL
PROCEDURE InsertProject(
aPno number, aPname varchar, aPmgr number )
IS BEGIN
INSERT INTO Projs( pno, pname, pmgr )
VALUES( aPno, aPname, aPmgr );
END;
PROCEDURE InsertProject(
aPno number, aPname varchar, aPmgr number )
IS
sqlstr varchar(100);
BEGIN
Using numbered bind parameters
sqlstr :=
'INSERT INTO Projs( pno, pname, pmgr ) ' ||
' VALUES( :1, :2, :3 )'
EXECUTE IMMEDIATE sqlstr USING
aPno, aPname, aPmgr;
END;
© Ellis Cohen 2001-2008
111
Download