Exercises - Working with
Collections
For all this TESTS, use the following Table:
SET DEFINE OFF;
CREATE TABLE EMPLOYEES
(
EMPLOYEE_ID NUMBER(6),
FIRST_NAME VARCHAR2(20 BYTE),
LAST_NAME VARCHAR2(25 BYTE) NOT
NULL,
EMAIL
VARCHAR2(25 BYTE) NOT NULL,
PHONE_NUMBER VARCHAR2(20 BYTE),
HIRE_DATE DATE NOT NULL,
JOB_ID
VARCHAR2(10 BYTE) NOT NULL,
SALARY
NUMBER(8,2),
COMMISSION_PCT
NUMBER(2,2),
MANAGER_ID NUMBER(6),
DEPARTMENT_ID
NUMBER(4)
);
CREATE INDEX EMP_DEPARTMENT_IX ON
EMPLOYEES(DEPARTMENT_ID);
CREATE UNIQUE INDEX EMP_EMAIL_UK
ON EMPLOYEES(EMAIL);
CREATE UNIQUE INDEX EMP_EMP_ID_PK
ON EMPLOYEES(EMPLOYEE_ID);
CREATE INDEX EMP_JOB_IX ON
EMPLOYEES(JOB_ID);
CREATE INDEX EMP_MANAGER_IX ON
EMPLOYEES(MANAGER_ID);
CREATE INDEX EMP_NAME_IX ON
EMPLOYEES(LAST_NAME, FIRST_NAME);
ALTER TABLE EMPLOYEES ADD
CONSTRAINT EMP_EMP_ID_PK PRIMARY KEY(EMPLOYEE_ID) USING INDEX
EMP_EMP_ID_PK;
ALTER TABLE EMPLOYEES ADD
CONSTRAINT EMP_EMAIL_UK UNIQUE (EMAIL) USING INDEX EMP_EMAIL_UK;
Insert into EMPLOYEES (EMPLOYEE_ID,
FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB_ID, SALARY,
COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID)
Values (100,
'Steven', 'King', 'SKING', '515.123.4567', TO_DATE('06/17/1987
00:00:00', 'MM/DD/YYYY HH24:MI:SS'), 'AD_PRES', 24000, NULL, NULL, 90);
Insert into EMPLOYEES
(EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE,
JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID)
Values (101, 'Neena',
'Kochhar', 'NKOCHHAR', '515.123.4568', TO_DATE('09/21/1989 00:00:00',
'MM/DD/YYYY HH24:MI:SS'), 'AD_VP', 17000, NULL,
100, 90);
Insert into EMPLOYEES
(EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE,
JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID)
Values (102, 'Lex',
'De Haan', 'LDEHAAN', '515.123.4569', TO_DATE('01/13/1993 00:00:00',
'MM/DD/YYYY HH24:MI:SS'), 'AD_VP', 17000, NULL, 100, 90);
Insert into EMPLOYEES
(EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE,
JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID)
Values (103,
'Alexander', 'Hunold', 'AHUNOLD', '590.423.4567', TO_DATE('01/03/1990
00:00:00', 'MM/DD/YYYY HH24:MI:SS'), 'IT_PROG', 9000, NULL, 102, 60);
Insert into EMPLOYEES
(EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE,
JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID)
Values (104, 'Bruce',
'Ernst', 'BERNST', '590.423.4568', TO_DATE('05/21/1991 00:00:00',
'MM/DD/YYYY HH24:MI:SS'), 'IT_PROG', 6000, NULL, 103, 60);
Insert into EMPLOYEES
(EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE,
JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID)
Values (105, 'David',
'Austin', 'DAUSTIN', '590.423.4569', TO_DATE('06/25/1997
00:00:00', 'MM/DD/YYYY HH24:MI:SS'), 'IT_PROG', 4800, NULL, 103,
60);
Insert into EMPLOYEES
(EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE,
JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID)
Values (106, 'Valli',
'Pataballa', 'VPATABAL', '590.423.4560', TO_DATE('02/05/1998 00:00:00',
'MM/DD/YYYY HH24:MI:SS'), 'IT_PROG', 4800, NULL, 103, 60);
Insert into EMPLOYEES
(EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE,
JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID)
Values (107, 'Diana',
'Lorentz', 'DLORENTZ', '590.423.5567', TO_DATE('02/07/1999
00:00:00', 'MM/DD/YYYY HH24:MI:SS'), 'IT_PROG', 4200, NULL, 103,
60);
Insert into EMPLOYEES
(EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE,
JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID)
Values (108, 'Nancy',
'Greenberg', 'NGREENBE', '515.124.4569', TO_DATE('08/17/1994 00:00:00',
'MM/DD/YYYY HH24:MI:SS'), 'FI_MGR', 12000, NULL, 101, 100);
Insert into EMPLOYEES
(EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE,
JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID)
Values (109, 'Daniel',
'Faviet', 'DFAVIET', '515.124.4169', TO_DATE('08/16/1994 00:00:00',
'MM/DD/YYYY HH24:MI:SS'), 'FI_ACCOUNT', 9000, NULL, 108, 100);
Insert into EMPLOYEES
(EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE,
JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID)
Values (110, 'John',
'Chen', 'JCHEN', '515.124.4269', TO_DATE('09/28/1997 00:00:00',
'MM/DD/YYYY HH24:MI:SS'), 'FI_ACCOUNT', 8200, NULL, 108, 100);
Insert into EMPLOYEES
(EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE,
JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID)
Values (111, 'Ismael',
'Sciarra', 'ISCIARRA', '515.124.4369', TO_DATE('09/30/1997 00:00:00',
'MM/DD/YYYY HH24:MI:SS'), 'FI_ACCOUNT', 7700, NULL, 108, 100);
Insert into EMPLOYEES
(EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE,
JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID)
Values (112, 'Jose Manuel',
'Urman', 'JMURMAN', '515.124.4469', TO_DATE('03/07/1998 00:00:00',
'MM/DD/YYYY HH24:MI:SS'), 'FI_ACCOUNT', 7800, NULL, 108, 100);
Insert into EMPLOYEES
(EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE,
JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID)
Values (113, 'Luis',
'Popp', 'LPOPP', '515.124.4567', TO_DATE('12/07/1999 00:00:00',
'MM/DD/YYYY HH24:MI:SS'), 'FI_ACCOUNT', 6900, NULL, 108, 100);
Insert into EMPLOYEES
(EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE,
JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID)
Values (114, 'Den',
'Raphaely', 'DRAPHEAL', '515.127.4561', TO_DATE('12/07/1994 00:00:00',
'MM/DD/YYYY HH24:MI:SS'), 'PU_MAN', 11000, NULL, 100, 30);
Insert into EMPLOYEES
(EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE,
JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID)
Values (115, 'Alexander',
'Khoo', 'AKHOO', '515.127.4562', TO_DATE('05/18/1995 00:00:00',
'MM/DD/YYYY HH24:MI:SS'), 'PU_CLERK', 3100, NULL, 114, 30);
Insert into EMPLOYEES
(EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE,
JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID)
Values (116, 'Shelli',
'Baida', 'SBAIDA', '515.127.4563', TO_DATE('12/24/1997 00:00:00',
'MM/DD/YYYY HH24:MI:SS'), 'PU_CLERK', 2900, NULL, 114, 30);
Insert into EMPLOYEES
(EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE,
JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID)
Values (117, 'Sigal',
'Tobias', 'STOBIAS', '515.127.4564', TO_DATE('07/24/1997 00:00:00',
'MM/DD/YYYY HH24:MI:SS'), 'PU_CLERK', 2800, NULL, 114, 30);
Insert into EMPLOYEES
(EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE,
JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID)
Values (118, 'Guy',
'Himuro', 'GHIMURO', '515.127.4565', TO_DATE('11/15/1998 00:00:00',
'MM/DD/YYYY HH24:MI:SS'), 'PU_CLERK', 2600, NULL, 114, 30);
Insert into EMPLOYEES
(EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE,
JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID)
Values (119, 'Karen',
'Colmenares', 'KCOLMENA', '515.127.4566', TO_DATE('08/10/1999
00:00:00', 'MM/DD/YYYY HH24:MI:SS'), 'PU_CLERK', 2500, NULL, 114, 30);
Insert into EMPLOYEES
(EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE,
JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID)
Values (120,
'Matthew', 'Weiss', 'MWEISS', '650.123.1234', TO_DATE('07/18/1996
00:00:00', 'MM/DD/YYYY HH24:MI:SS'), 'ST_MAN', 8000, NULL, 100, 50);
COMMIT;
01.
Declare and define types, instances, and elements
Declare array types, declare variables based on those types (instances
of the type) and define elements in the collections with assignment
statements.
01-1: Declare and define with
associative arrays
a. Declare a local associative array of numbers indexed by PLS_INTEGER.
b. Declare a variable based on that type.
c. Fill up the first 100 elements (starting from index 1) with numbers
of your choice.
Solution: for 01-1: Declare and define with associative arrays
DECLARE
-- a. Declare a local associative array of numbers indexed by PLS_INTEGER
TYPE numbers_aat IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
-- b. Declare a variable based on that type
my_numbers numbers_aat;
-- c. Fill up the first 100 elements (starting from index 1) with numbers of your choice.
BEGIN
FOR indx IN 1 .. 100
LOOP
my_numbers(indx) := indx * 1958;
END LOOP;
--Show the Data
FOR indx IN 1 .. my_numbers.COUNT
LOOP
DBMS_OUTPUT.put_line ( 'Numbers Collected: ' ||to_char(my_numbers(indx)) );
END LOOP;
END;
/
01-2: Declare and define with
nested tables
a. Declare a local nested table of numbers.
b. Declare a variable based on that type.
c. Fill up the first 100 elements (starting from index 1) with numbers
of your choice.
Solution: for 01-2: Declare and define with nested tables
DECLARE
-- a. Declare a local nested table of numbers indexed by PLS_INTEGER
TYPE numbers_ntt IS TABLE OF NUMBER;
-- b. Declare a variable based on that type
my_numbers numbers_ntt := numbers_ntt ( );
-- c. Fill up the first 100 elements (starting from index 1) with numbers of your choice.
BEGIN
FOR indx IN 1 .. 100
LOOP
my_numbers.EXTEND;
my_numbers ( indx ) := indx * 1958;
END LOOP;
--Show the Data
FOR indx IN 1 .. my_numbers.COUNT
LOOP
DBMS_OUTPUT.put_line ( 'Numbers Collected: ' ||to_char(my_numbers(indx)) );
END LOOP;
END;
/
01-3: Declare and define with
varrays
a. Declare a local varray of numbers.
b. Declare a variable based on that type.
c. Fill up the first 100 elements (starting from index 1) with numbers
of your choice.
Solution: for 01-3: Declare and define with varrays
DECLARE
-- a. Declare a local varray of numbers indexed by PLS_INTEGER.
TYPE numbers_vat IS VARRAY(100) OF NUMBER;
-- b. Declare a variable based on that type.
my_numbers numbers_vat := numbers_vat();
-- c. Fill up the first 100 elements (starting from index 1) with numbers of your choice.
BEGIN
FOR indx IN 1 .. 100
LOOP
my_numbers.EXTEND;
my_numbers ( indx ) := indx * 1958;
END LOOP;
--Show the Data
FOR indx IN 1 .. my_numbers.COUNT
LOOP
DBMS_OUTPUT.put_line ( 'Numbers Collected: ' ||to_char(my_numbers(indx)) );
END LOOP;
END;
/
01-4: Work with collections of
records
Declare an associative array of records based on the employees table.
Use a cursor FOR loop to populate that collection with the contents of
the employees table.
Fill the collection sequentially using the cursor %ROWCOUNT attribute
to specify the index value.
Solution: for 01-4: Work with collections of records
DECLARE
-- Declare an associative array of records based on the employees table.
TYPE employees_aat IS TABLE OF employees%ROWTYPE INDEX BY PLS_INTEGER;
-- Instance of table-based collection type
l_employees employees_aat;
CURSOR all_rows_cur IS SELECT *
FROM employees;
BEGIN
-- Use a cursor FOR loop to populate that collection with the contents of the employees table.
FOR rec IN all_rows_cur
LOOP
DBMS_OUTPUT.put_line ( 'Loading....' || rec.last_name );
-- Fill the collection sequentially using the cursor %ROWCOUNT attribute to specify the index value.
l_employees(all_rows_cur%ROWCOUNT) := rec;
END LOOP;
-- Show the Data
FOR indx IN 1 .. l_employees.COUNT
LOOP
DBMS_OUTPUT.put_line ( 'Employee ID=' || to_char(l_employees(indx).EMPLOYEE_ID) || ' Name= ' || l_employees(indx).last_name );
END LOOP;
END;
/
02.
Introduction to BULK COLLECT and FORALL for better performance
BULK COLLECT and FORALL offer array processing features to both query
and update the contents of database tables from a PL/SQL program.
This section takes you through the basics of working with these
statements.
02-1: Use BULK COLLECT to query
all rows from table
Write an anonymous block that copies all the rows from the employees
table to an associative array based on that table.
After the collection has been filled, iterate through the rows in the
collection and display the employee last name.
Solution: for 02-1: Use BULK COLLECT to query all rows from table
DECLARE
-- Collection type of records based on the table ROWTYPE.
TYPE employees_aat IS TABLE OF employees%ROWTYPE INDEX BY PLS_INTEGER;
-- Instance of table-based collection type
l_employees employees_aat;
BEGIN
SELECT * BULK COLLECT INTO l_employees
FROM employees;
-- Iterate through all rows using a FOR loop...
FOR indx IN 1 .. l_employees.COUNT
LOOP
DBMS_OUTPUT.put_line ( l_employees(indx).last_name );
END LOOP;
END;
/
02-2: Use FORALL to insert data.
Write an anonymous block that copies all the rows from the employees
table to an associative array based on that table.
After the collection has been filled, iterate through the rows in the
collection and add 10000 to each of the employee_id column values.
Also concatenate the value in the email column to itself (satisfy the
unique index).
Then use a FORALL statement to insert this data back into the employees
table.
Rollback your changes.
Solution: for 02-2: Use FORALL to insert data.
DECLARE
-- Collection type of records based on the table ROWTYPE.
TYPE employee_ids_aat IS TABLE OF employees%ROWTYPE INDEX BY PLS_INTEGER;
-- Instance of table-based collection type
l_employees employee_ids_aat;
BEGIN
SELECT * BULK COLLECT INTO l_employees
FROM employees;
FOR indx IN 1 .. l_employees.COUNT
LOOP
l_employees (indx ).employee_id := l_employees(indx).employee_id + 10000;
l_employees (indx ).email := l_employees(indx).email || l_employees(indx).email;
END LOOP;
FORALL indx IN 1 .. l_employees.COUNT
INSERT INTO employees VALUES l_employees(indx);
ROLLBACK;
END;
/
02-3: Use FORALL to update data.
Write an anonymous block that copies all the employee IDs from the
employees table to an associative array based on that column.
After the collection has been filled, use FORALL to set the salary of
all employees to the sum of the salary and the (percentage of the
commission * salary) for all elements in the collection (if commission
is NULL, add $1000 to the salary).
Rollback your changes.
Solution: for 02-3: Use FORALL to update data.
DECLARE
-- Collection type of records based on the table ROWTYPE.
TYPE employee_ids_aat IS TABLE OF employees.employee_id%TYPE INDEX BY PLS_INTEGER;
-- Instance of table-based collection type
l_employee_ids employee_ids_aat;
BEGIN
SELECT employee_id BULK COLLECT INTO l_employee_ids
FROM employees;
FORALL indx IN 1 .. l_employee_ids.COUNT
UPDATE employees
SET salary = salary + NVL (commission_pct * salary, 1000)
WHERE employee_id = l_employee_ids(indx);
ROLLBACK;
END;
/
02-4: Use FORALL to delete data.
Write an anonymous block that copies all the employee last names from
the employees table to an associative array based on that column.
After the collection has been filled, use FORALL to delete all
employees with matching last names from the collection.
Rollback your changes.
Solution: for 02-4: Use FORALL to delete data.
DECLARE
-- Collection type of records based on the table ROWTYPE.
TYPE last_names_aat IS TABLE OF employees.last_name%TYPE INDEX BY PLS_INTEGER;
-- Instance of table-based collection type
l_last_names last_names_aat;
BEGIN
SELECT last_name BULK COLLECT INTO l_last_names
FROM employees;
FORALL indx IN 1 .. l_last_names.COUNT
DELETE FROM employees
WHERE last_name = l_last_names(indx);
ROLLBACK;
END;
/
03.
Scan the contents of collections
You can use any kind of PL/SQL loop to iterate through the elements in
a collection, but certain kinds of loops make sense under certain
circumstances:
* Only use a numeric FOR loop for densely-filled collections that are
indexed by integer.
* Use a WHILE or simple loop for (possibly) sparsely-filled collections
or string-indexed collections.
03-1: Use FOR loop to iterate
through contents of densely-filled collection
Write a block of code that queries the contents of the employees table
into an associative array using BULK COLLECT.
Then iterate through all the rows in that collection with a FOR loop
and display the employee last name.
(Note: depending on the approach you took in "02-1: Use BULK COLLECT to
query all rows from table", you may have already completed this task!)
Solution: for 03-1: Use FOR loop to iterate through contents of
densely-filled collection
DECLARE
-- Declare an associative array of records based on the employees table.
TYPE employees_aat IS TABLE OF employees%ROWTYPE INDEX BY PLS_INTEGER;
-- Instance of table-based collection type
l_employees employees_aat;
BEGIN
SELECT * BULK COLLECT INTO l_employees
FROM employees;
-- Use a cursor FOR loop to populate that collection with the contents of the employees table.
FOR indx IN 1 .. l_employees.COUNT
LOOP
DBMS_OUTPUT.put_line (l_employees(indx).last_name);
END LOOP;
END;
/
03-2: Use FOR loop and EXISTS
method to iterate through contents of sparsely-filled collection
Write a block of code that queries the contents of the employees table
into an associative array using a cursor FOR loop.
Use the primary key of the employees table (employee_id) as the row
number in the collection.
Then iterate through all the rows in that collection with a FOR loop
and display the employee last name.
Use the EXISTS operator to ensure that Oracle does not raise a
NO_DATA_FOUND exception.
Solution: for 03-2: Use FOR loop and EXISTS method to iterate
through contents of sparsely-filled collection
DECLARE
-- Declare an associative array of records based on the employees table.
TYPE employees_aat IS TABLE OF employees%ROWTYPE INDEX BY PLS_INTEGER;
-- Instance of table-based collection type
l_employees employees_aat;
CURSOR all_rows_cur IS SELECT *
FROM employees;
BEGIN
FOR rec IN all_rows_cur
LOOP
--Save Data into the associative array based on the Employee ID Position
l_employees(rec.employee_id) := rec;
END LOOP;
FOR indx IN l_employees.FIRST .. l_employees.LAST
LOOP
IF l_employees.EXISTS(indx) THEN
DBMS_OUTPUT.put_line('Position = ' || to_char(l_employees(indx).EMPLOYEE_ID) || ' Name=' || l_employees(indx).last_name );
END IF;
END LOOP;
END;
/
03-3: Use WHILE loop to iterate
through contents of collection
Write a block of code that queries the contents of the employees table
into an associative array using a cursor FOR loop.
Use the primary key of the employees table, employee_id, as the row
number in the collection.
Then iterate through all the rows in that collection with a WHILE loop.
Implement a scan in both directions:
1. From first to last defined element.
2. From last to first defined element.
Solution: for 03-3: Use WHILE loop to iterate through contents of
collection
DECLARE
-- Declare an associative array of records based on the employees table.
TYPE employees_aat IS TABLE OF employees%ROWTYPE INDEX BY PLS_INTEGER;
-- Instance of table-based collection type
l_employees employees_aat;
CURSOR all_rows_cur IS SELECT *
FROM employees;
l_row PLS_INTEGER;
BEGIN
FOR rec IN all_rows_cur
LOOP
l_employees ( rec.employee_id ) := rec;
END LOOP;
DBMS_OUTPUT.put_line('*** Print from First to Last One ***');
l_row := l_employees.FIRST;
WHILE (l_row IS NOT NULL)
LOOP
DBMS_OUTPUT.put_line( l_employees(l_row).last_name );
l_row := l_employees.NEXT(l_row);
END LOOP;
DBMS_OUTPUT.put_line('*** Print from Last One to First ***');
l_row := l_employees.LAST;
WHILE (l_row IS NOT NULL)
LOOP
DBMS_OUTPUT.put_line(l_employees( l_row ).last_name );
l_row := l_employees.PRIOR(l_row);
END LOOP;
END;
/
03-4: Emulate a unique
index/primary key
Write a package whose specification contains a single function that
returns a row of data from the employees table for a given primary key.
Assume for this exercise that the data in the employees table is
static; it does not change during a user's session.
Implement that function in the package body as follows:
1. Copy the data from the table into a collection of records just once.
What will you use as the index values for this collection?
2. Return the row in the collection for the given primary key.
3. If no match is found for that primary key, raise the NO_DATA_FOUND
exception.
Advanced options:
* Can you figure out how to use BULK COLLECT to populate the collection
of records and still emulate the primary key/unique index?
* Assuming that this is a single-user system (!), how could you update
the collection whenever the table changes?
Solution: for 03-4s: Emulate a unique index/primary key
CREATE OR REPLACE PACKAGE employees_lu
/*
The LookUp package for EMPLOYEES
Caches the contents of EMPLOYEES into a package-level collection.
Requests for data via the onerow function are satisfied from the
collection, avoiding additional queries against the database.
This only makes sense if the data in the table is static, though
the onerow function will re-initialize the collection if it fails
to find the data requested. It will try this once, and if the
data still is not present, it will propagate out the NO_DATA_FOUND
exception.
*/
IS
-- Copies contents of table into collection.
PROCEDURE initialize;
-- Returns one row of data from the collection, not the table.
FUNCTION onerow ( employee_id_in IN employees.employee_id%TYPE )
RETURN employees%ROWTYPE;
END employees_lu;
/
-- Solution 1: when data is not found in collection, raise NO_DATA_FOUND.
CREATE OR REPLACE PACKAGE BODY employees_lu
IS
TYPE employees_tt IS TABLE OF employees%ROWTYPE INDEX BY BINARY_INTEGER;
g_employees_cache employees_tt;
FUNCTION onerow ( employee_id_in IN employees.employee_id%TYPE )
RETURN employees%ROWTYPE
IS
retval employees%ROWTYPE;
BEGIN
-- Grab the data in the collection row identified by the PKY value.
retval := g_employees_cache(employee_id_in);
-- Return the row of data.
RETURN retval;
END onerow;
PROCEDURE initialize
IS
BEGIN
-- Populate the cache; first clear it out.
g_employees_cache.DELETE;
-- For each row in table, copy to collection,
-- using the primary key value as the index.
-- Note: can't use BULK COLLECT since it always
-- fills rows sequentially.
FOR rec IN (SELECT *
FROM employees)
LOOP
g_employees_cache(rec.employee_id) := rec;
END LOOP;
END initialize;
BEGIN
initialize;
END employees_lu;
/
-- Solution 2: when data is not found in collection, then try once to refresh the cache
CREATE OR REPLACE PACKAGE BODY employees_lu
IS
TYPE employees_tt IS TABLE OF employees%ROWTYPE INDEX BY BINARY_INTEGER;
g_employees_cache employees_tt;
-- Keep track of whether or not there was a missing row in the cache.
g_failed_already BOOLEAN DEFAULT FALSE;
FUNCTION onerow ( employee_id_in IN employees.employee_id%TYPE )
RETURN employees%ROWTYPE
IS
retval employees%ROWTYPE;
BEGIN
-- Grab the data in the collection row identified by the PKY value.
retval := g_employees_cache(employee_id_in);
-- No failure this round; collection copy is OK.
g_failed_already := FALSE;
-- Return the row of data.
RETURN retval;
EXCEPTION
WHEN NO_DATA_FOUND
THEN
-- If I already re-initialized and STILL got a failure,
-- then it is time simply to bail out.
IF g_failed_already THEN
RAISE;
ELSE
-- Data was not present, so re-initialize the array and try again.
g_failed_already := TRUE;
initialize;
RETURN onerow(employee_id_in);
END IF;
END onerow;
PROCEDURE initialize
IS
BEGIN
-- Populate the cache; first clear it out.
g_employees_cache.DELETE;
g_failed_already := FALSE;
-- For each row in table, copy to collection,
-- using the primary key value as the index.
-- Note: can't use BULK COLLECT since it always
-- fills rows sequentially.
FOR rec IN (SELECT *
FROM employees)
LOOP
g_employees_cache(rec.employee_id) := rec;
END LOOP;
END initialize;
BEGIN
initialize;
END employees_lu;
/
04. Pass collections as
parameters
To pass a collection as a parameter, the collection type must already
be defined, either in the same scope or at the schema level.
04-1: Pass collection as IN
argument
Create a package with a specification that contains:
1. An associative array type of strings, set to the largest size
allowed in PL/SQL.
2. A nested table type of records based on the employees table.
3. A procedure that accepts two IN parameters, one for each of these
types.
Implement a package body in which the procedure displays the contents
of each of these collections (just the employee name for 2 is fine),
making no assumptions about the contents of the collections.
Solution: for 04-1: Pass collection as IN argument
CREATE OR REPLACE PACKAGE hoc_pkg
IS
TYPE strings_aat IS TABLE OF VARCHAR2(32767) INDEX BY PLS_INTEGER;
TYPE employees_aat IS TABLE OF employees%ROWTYPE;
PROCEDURE coll_parms ( strings_in IN strings_aat, emps_in IN employees_aat );
END hoc_pkg;
/
CREATE OR REPLACE PACKAGE BODY hoc_pkg
IS
PROCEDURE coll_parms ( strings_in IN strings_aat, emps_in IN employees_aat )
IS
l_row PLS_INTEGER;
BEGIN
l_row := strings_in.FIRST;
WHILE (l_row IS NOT NULL)
LOOP
DBMS_OUTPUT.put_line(strings_in(l_row));
l_row := strings_in.NEXT(l_row);
END LOOP;
l_row := emps_in.FIRST;
WHILE (l_row IS NOT NULL)
LOOP
DBMS_OUTPUT.put_line(emps_in(l_row).last_name );
l_row := emps_in.NEXT(l_row);
END LOOP;
END coll_parms;
END hoc_pkg;
/
04-2: Pass collection as RETURN of function
Create a package that contains the following elements:
1. A nested table type based on the employees table (TABLE OF
employees%ROWTYPE).
2. A function that accepts a last name filter (possibly wild-carded
string) and returns a nested table of employees records for which the
last name is LIKE the filter value provided (case insensitive).
Write an anonymous block that calls this function for all employees
whose last name contains an "E". Display the employee last names.
Solution: for 04-2: Pass collection as RETURN of function
CREATE OR REPLACE PACKAGE hoc_pkg
IS
TYPE employees_aat IS TABLE OF employees%ROWTYPE;
FUNCTION matching_employees ( name_like_in IN VARCHAR2 )
RETURN employees_aat;
END hoc_pkg;
/
CREATE OR REPLACE PACKAGE BODY hoc_pkg
IS
FUNCTION matching_employees ( name_like_in IN VARCHAR2 )
RETURN employees_aat
IS
l_employees employees_aat;
BEGIN
SELECT * BULK COLLECT INTO l_employees
FROM employees
WHERE UPPER(last_name) LIKE UPPER(name_like_in);
RETURN l_employees;
END matching_employees;
END hoc_pkg;
/
DECLARE
l_employees hoc_pkg.employees_aat;
BEGIN
l_employees := hoc_pkg.matching_employees('%E%');
FOR indx IN 1 .. l_employees.COUNT
LOOP
DBMS_OUTPUT.put_line(l_employees(indx).last_name );
END LOOP;
END;
/
04-3: Pass collection as IN OUT
argument
1. Create a package with a specification that contains:
a. An associative array type of strings, set to the largest size
allowed in PL/SQL.
b. A procedure that accepts a collection of strings based on that as an
IN OUT parameter.
2. Implement a package body in which the procedure concatenates the
contents of each element in the collection to itself, and places that
value back into the collection.
3. Write an anonymous block that populates a collection of the type
specified in 1.a with the last names of all employees.
Then call the procedure specified in 1.b and afterwards display the
contents of that collection.
Solution: for 04-3: Pass collection as IN OUT argument
CREATE OR REPLACE PACKAGE hoc_pkg
IS
TYPE strings_aat IS TABLE OF VARCHAR2(32767) INDEX BY PLS_INTEGER;
PROCEDURE inout_collection ( strings_inout IN OUT strings_aat );
END hoc_pkg;
/
CREATE OR REPLACE PACKAGE BODY hoc_pkg
IS
PROCEDURE inout_collection ( strings_inout IN OUT strings_aat )
IS
l_row PLS_INTEGER;
BEGIN
l_row := strings_inout.FIRST;
WHILE (l_row IS NOT NULL)
LOOP
strings_inout(l_row) := strings_inout (l_row) || strings_inout(l_row);
l_row := strings_inout.NEXT(l_row);
END LOOP;
END inout_collection;
END hoc_pkg;
/
DECLARE
l_names hoc_pkg.strings_aat;
BEGIN
SELECT last_name BULK COLLECT INTO l_names
FROM employees;
hoc_pkg.inout_collection(l_names);
FOR indx IN 1 .. l_names.COUNT
LOOP
DBMS_OUTPUT.put_line(l_names(indx ));
END LOOP;
END;
/
04-4: Use NOCOPY for collection
argument
1. Create a package with a specification that contains:
a. An associative array type of strings, set to the largest size
allowed in PL/SQL.
b. A procedure that accepts a collection of strings based on that as an
IN OUT parameter.
c. Create another procedure that does exactly the same thing as 1.b,
but specifies NOCOPY for the parameter.
2. Implement a package body in which the procedure concatenates the
contents of each element in the collection to itself, and places that
value back into the collection.
3. Write an anonymous block based on the code found in "Comparison of
elapsed time for multiple implementations" that does the following:
a. Populates two collections of the type specified in 1.a with
1,000,000 strings of your choice.
b. Within a "timing bracket", calls the procedure specified in 1.b to
"double up" each of the values in one collection.
c. Within a second "timing bracket", calls the procedure specified in
1.c (NOCOPY) to "double up" each of the values in the other collection.
What difference do you see in performance?
Solution: for 04-4: Use NOCOPY for collection argument
CREATE OR REPLACE PACKAGE hoc_pkg
IS
TYPE strings_aat IS TABLE OF VARCHAR2(32767) INDEX BY PLS_INTEGER;
PROCEDURE inout_collection ( strings_inout IN OUT strings_aat );
PROCEDURE inout_nocopy ( strings_inout IN OUT NOCOPY strings_aat );
END hoc_pkg;
/
CREATE OR REPLACE PACKAGE BODY hoc_pkg
IS
PROCEDURE inout_collection ( strings_inout IN OUT strings_aat )
IS
l_row PLS_INTEGER;
BEGIN
l_row := strings_inout.FIRST;
WHILE (l_row IS NOT NULL)
LOOP
strings_inout(l_row) := strings_inout(l_row) || strings_inout(l_row);
l_row := strings_inout.NEXT(l_row);
END LOOP;
END inout_collection;
PROCEDURE inout_nocopy ( strings_inout IN OUT NOCOPY strings_aat )
IS
l_row PLS_INTEGER;
BEGIN
l_row := strings_inout.FIRST;
WHILE (l_row IS NOT NULL)
LOOP
strings_inout(l_row) := strings_inout(l_row) || strings_inout(l_row);
l_row := strings_inout.NEXT(l_row);
END LOOP;
END inout_nocopy;
END hoc_pkg;
/
DECLARE
l_iterations PLS_INTEGER DEFAULT 1;
l_start_time PLS_INTEGER;
PROCEDURE start_timing
IS
BEGIN
l_start_time := DBMS_UTILITY.get_cpu_time;
END start_timing;
PROCEDURE show_elapsed ( str IN VARCHAR2 )
IS
BEGIN
DBMS_OUTPUT.put_line( '"'
|| str
|| '" Elapsed CPU time: '
|| TO_CHAR ( DBMS_UTILITY.get_cpu_time
- l_start_time
)
|| ' for '
|| l_iterations
|| ' iterations.'
);
END show_elapsed;
PROCEDURE init_test ( str_in IN VARCHAR2 DEFAULT NULL )
IS
BEGIN
start_timing;
DBMS_OUTPUT.put_line(str_in);
END init_test;
PROCEDURE finish_test ( str_in IN VARCHAR2 DEFAULT NULL )
IS
BEGIN
show_elapsed(str_in);
END finish_test;
PROCEDURE timing_bracket_1
IS
l_names hoc_pkg.strings_aat;
BEGIN
FOR indx IN 1 .. 1000000
LOOP
l_names(indx) := TO_CHAR(indx);
END LOOP;
init_test ( 'IN OUT' );
FOR indx IN 1 .. l_iterations
LOOP
hoc_pkg.inout_collection(l_names);
END LOOP;
finish_test;
END timing_bracket_1;
PROCEDURE timing_bracket_2
IS
l_names hoc_pkg.strings_aat;
BEGIN
FOR indx IN 1 .. 1000000
LOOP
l_names(indx) := TO_CHAR(indx);
END LOOP;
init_test ( 'IN OUT NOCOPY' );
FOR indx IN 1 .. l_iterations
LOOP
hoc_pkg.inout_nocopy(l_names);
END LOOP;
finish_test;
END timing_bracket_2;
BEGIN
timing_bracket_1;
timing_bracket_2;
END test_varieties;
/
04-5: Understand impact of
error raised in NOCOPY program
1. Make the modifications described below to the code you built in
exercise 04-4.
a. Add a procedure to your package that displays the contents of an
array based on that collection type of strings.
b. Modify the procedure so that it only defines 20 strings instead of
1,000,000 and if it encounters a string = "ABC", it raises the
PROGRAM_ERROR exception.
2. Write an anonymous block that:
a. Declares two different collections based on the packaged type and
populate each one with twenty strings, one of which is "ABC".
b. Passes each collection to the two different packaged procedures
(with and without NOCOPY), each call within its own nested anonymous
block that also has an "I don't care" exception section. That is:
BEGIN
EXCEPTION WHEN OTHERS NULL; END;
c. Call the procedure to display the contents of each of the
collections. Are they the same? What is different? Why are they
different (they SHOULD be!)?
Solution: for 04-5: Understand impact of error raised in NOCOPY
program
CREATE OR REPLACE PACKAGE hoc_pkg
IS
TYPE strings_aat IS TABLE OF VARCHAR2(32767) INDEX BY PLS_INTEGER;
PROCEDURE inout_collection ( strings_inout IN OUT strings_aat );
PROCEDURE inout_nocopy ( strings_inout IN OUT NOCOPY strings_aat );
PROCEDURE show_collection ( title_in IN VARCHAR2, strings_in IN strings_aat );
END hoc_pkg;
/
CREATE OR REPLACE PACKAGE BODY hoc_pkg
IS
PROCEDURE show_collection ( title_in IN VARCHAR2, strings_in IN strings_aat )
IS
l_row PLS_INTEGER;
BEGIN
DBMS_OUTPUT.put_line(title_in);
l_row := strings_in.FIRST;
WHILE ( l_row IS NOT NULL )
LOOP
DBMS_OUTPUT.put_line( ' ' || strings_in(l_row));
l_row := strings_in.NEXT(l_row);
END LOOP;
END show_collection;
PROCEDURE inout_collection ( strings_inout IN OUT strings_aat )
IS
l_row PLS_INTEGER;
BEGIN
l_row := strings_inout.FIRST;
WHILE ( l_row IS NOT NULL )
LOOP
IF strings_inout(l_row) = 'ABC'
THEN
RAISE PROGRAM_ERROR;
END IF;
strings_inout (l_row) := strings_inout(l_row) || strings_inout(l_row);
l_row := strings_inout.NEXT(l_row);
END LOOP;
END inout_collection;
PROCEDURE inout_nocopy ( strings_inout IN OUT NOCOPY strings_aat )
IS
l_row PLS_INTEGER;
BEGIN
l_row := strings_inout.FIRST;
WHILE ( l_row IS NOT NULL )
LOOP
IF strings_inout(l_row) = 'ABC'
THEN
RAISE PROGRAM_ERROR;
END IF;
strings_inout(l_row) := strings_inout (l_row) || strings_inout(l_row);
l_row := strings_inout.NEXT(l_row);
END LOOP;
END inout_nocopy;
END hoc_pkg;
/
DECLARE
l_names1 hoc_pkg.strings_aat;
l_names2 hoc_pkg.strings_aat;
BEGIN
FOR indx IN 1 .. 20
LOOP
IF indx = 13 THEN
l_names1(indx) := 'ABC';
ELSE
l_names1(indx) := TO_CHAR(indx);
END IF;
END LOOP;
FOR indx IN 1 .. 20
LOOP
IF indx = 13 THEN
l_names2(indx) := 'ABC';
ELSE
l_names2(indx) := TO_CHAR(indx);
END IF;
END LOOP;
BEGIN
hoc_pkg.inout_collection ( l_names1 );
EXCEPTION
WHEN OTHERS THEN
NULL;
END;
BEGIN
hoc_pkg.inout_nocopy ( l_names2 );
EXCEPTION
WHEN OTHERS THEN
NULL;
END;
hoc_pkg.show_collection ( 'IN OUT', l_names1 );
hoc_pkg.show_collection ( 'IN OUT NOCOPY', l_names2 );
END;
/
05. Modify contents of collections
with collection methods
Exercises in this section ensure that you know how to use the methods
that modify the contents of collections, including:
DELETE
TRIM
EXTEND
05-1: Delete elements from a
collection.
Define an associative array of numbers and populate index values 1
through 10 with values of your choice.
Then use the DELETE method to:
1. Remove just the 4th row,
2. Remove rows with index values between 6 and 9.
3. Remove all the remaining rows.
Solution: for 05-1s: Delete elements from a collection.
DECLARE
-- Define an associative array of numbers and populate index values 1 through 10 with values of your choice.
TYPE numbers_aat IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
l_numbers numbers_aat;
BEGIN
FOR indx IN 1 .. 10
LOOP
DBMS_OUTPUT.put_line(MOD(3.14 * indx * 176, 3));
l_numbers (indx) := MOD(3.14 * indx * 176, 3);
END LOOP;
-- Then use the DELETE method to:
-- 1. Remove just the 4th row,
l_numbers.DELETE ( 4 );
-- 2. Remove rows with index values between 6 and 9.
l_numbers.DELETE ( 6, 9 );
-- 3. Remove all the remaining rows.
l_numbers.DELETE;
END;
/
05-2: Extend nested tables and
varrays
Use the EXTEND method as follows:
1. Declare a nested table based on a locally-defined nested table type
of numbers.
2. Write a numeric FOR loop that populates the nested table with 25
elements, extending one element at a time.
3. Rewrite the above code as follows:
a. Define a schema-level nested table type.
b. Create a function that accepts the total number
of elements needed in the nested table, and then returns that nested
table in the function RETURN clause. Use a single call to the EXTEND
method.
4. Change the above code to work with varrays with a maximum number of
elements set to 100.
Solution: for 05-2: Extend nested tables and varrays
DECLARE
-- 1. Declare a nested table based on a locally-defined nested table type of numbers.
TYPE numbers_ntt IS TABLE OF NUMBER;
l_numbers numbers_ntt := numbers_ntt();
BEGIN
-- 2. Write a numeric FOR loop that populates the nested table with 25 elements, extending one element at a time.
FOR indx IN 1 .. 25
LOOP
l_numbers.EXTEND;
l_numbers(indx) := indx;
-- Or...
-- l_numbers(l_numbers.COUNT) := indx;
END LOOP;
END;
/
/*
3. Rewrite the above code as follows:
a. Define a schema-level nested table type.
b. Create a function that accepts the total number of elements needed in the
nested table, and then returns that nested table in the function RETURN
clause. Use a single call to the EXTEND method.
*/
CREATE OR REPLACE TYPE numbers_ntt IS TABLE OF NUMBER;
/
CREATE OR REPLACE FUNCTION all_my_numbers ( count_in IN PLS_INTEGER )
RETURN numbers_ntt
IS
l_numbers numbers_ntt := numbers_ntt();
BEGIN
l_numbers.EXTEND(count_in);
FOR indx IN 1 .. count_in
LOOP
l_numbers(indx) := indx;
END LOOP;
RETURN l_numbers;
END all_my_numbers;
/
-- 4. Change the above code to work with varrays with a maximum number of elements set to 100.
CREATE OR REPLACE TYPE numbers_vat IS VARRAY(100) OF NUMBER;
/
CREATE OR REPLACE FUNCTION all_my_numbers ( count_in IN PLS_INTEGER )
RETURN numbers_vat
IS
l_numbers numbers_vat := numbers_vat();
BEGIN
l_numbers.EXTEND(count_in);
FOR indx IN 1 .. count_in
LOOP
l_numbers(indx) := indx;
END LOOP;
RETURN l_numbers;
END all_my_numbers;
/
05-3: Trim elements from the
end of a varray
Declare a varray of numbers with a maximum of 20 elements.
Populate it with 20 values of your choice.
Trim 5 elements from the varray and display the remaining values, along
with the count in the collection.
Solution: for 05-3: Trim elements from the end of a varray
DECLARE
-- Declare a varray of numbers with a maximum of 20 elements.
TYPE numbers_vat IS VARRAY(20) OF NUMBER;
l_numbers numbers_vat := numbers_vat();
BEGIN
l_numbers.EXTEND(20);
-- Populate it with 20 values of your choice.
FOR indx IN 1 .. 20
LOOP
l_numbers(indx) := indx * ( 1958 + 09 + 23 );
END LOOP;
FOR indx IN 1 .. 5
LOOP
l_numbers.TRIM;
END LOOP;
/* Or...
l_numbers.TRIM (5);
*/
-- Trim 5 elements from the varray and display the remaining values,
-- along with the count in the collection.
DBMS_OUTPUT.put_line('Count in numbers varray = ' || l_numbers.COUNT);
FOR indx IN 1 .. l_numbers.COUNT
LOOP
DBMS_OUTPUT.put_line(l_numbers(indx));
END LOOP;
END all_my_numbers;
/
06.
String-indexed collections
Oracle9i Release 2 and above allows you to define associative arrays
that are indexed by strings, offering a wide range of possibilities.
06-1: Populate a string-indexed
collection
Write an anonymous block that populates a collection with rows from the
employees table, indexed by the first and last names (no two employees
have the same first and last names; but you should ensure that their
combination will also be unique).
After filling the collection, write a loop to iterate through the rows
in the collection, displaying the index value and the employee ID for
that index.
Solution: for 06-1: Populate a string-indexed collection
DECLARE
SUBTYPE full_name_t IS VARCHAR2 ( 1000 );
-- Declare two associative array types of records based on the employees table, indexed by the primary key and the name.
TYPE employees_aat IS TABLE OF employees%ROWTYPE INDEX BY PLS_INTEGER;
TYPE employees_by_name_aat IS TABLE OF employees%ROWTYPE INDEX BY full_name_t;
-- Instance of table-based collection types
l_temp_employees employees_aat;
l_employees employees_by_name_aat;
--
l_index full_name_t;
FUNCTION full_name ( employee_in IN employees%ROWTYPE )
RETURN full_name_t
IS
BEGIN
RETURN employee_in.last_name || '^' || employee_in.first_name;
END full_name;
BEGIN
SELECT * BULK COLLECT INTO l_temp_employees
FROM employees;
FOR indx IN 1 .. l_temp_employees.COUNT
LOOP
l_employees ( full_name ( l_temp_employees ( indx ))) := l_temp_employees ( indx );
END LOOP;
l_index := l_employees.FIRST;
WHILE ( l_index IS NOT NULL )
LOOP
DBMS_OUTPUT.put_line( l_index || ' - ' || l_employees ( l_index ).employee_id);
l_index := l_employees.NEXT ( l_index );
END LOOP;
END;
/
06-2: Use string-indexed
collection to keep track of usages
Within a particular section of my application, I need to keep track of
whether a certain name has already been used.
Write a utility package that provides the following functionality:
* Record a name/string as being used.
* Tell me if a particular name is already in use.
* Reset my list.
* Display the list of used names.
Start with the package specification in the Code tab of this script.
Solution: for 06-2: Use string-indexed collection to keep track of
usages
CREATE OR REPLACE PACKAGE string_tracker
/*
Within a particular section of my application, I need to keep
track of whether a certain name has already been used.
Write a utility package that provides the following functionality:
* Record a name/string as being used.
* Tell me if a particular name is already in use.
* Reset my list.
* Display the list of used names.
*/
IS
SUBTYPE maxvarchar2_t IS VARCHAR2 ( 32767 );
PROCEDURE clear_used_list;
FUNCTION string_in_use ( value_in IN maxvarchar2_t ) RETURN BOOLEAN;
PROCEDURE mark_as_used ( value_in IN maxvarchar2_t );
END string_tracker;
/
CREATE OR REPLACE PACKAGE BODY string_tracker
IS
TYPE used_aat IS TABLE OF BOOLEAN INDEX BY maxvarchar2_t;
g_names_used used_aat;
PROCEDURE clear_used_list
IS
BEGIN
g_names_used.DELETE;
END clear_used_list;
FUNCTION string_in_use ( value_in IN maxvarchar2_t )
RETURN BOOLEAN
IS
BEGIN
RETURN g_names_used.EXISTS ( value_in );
END string_in_use;
PROCEDURE mark_as_used ( value_in IN maxvarchar2_t )
IS
BEGIN
g_names_used ( value_in ) := TRUE;
END mark_as_used;
END string_tracker;
/
07.
Multi-level collections
Oracle9i Release 2 and above allows you to create very complex
structures of multi-level collections (collections of collections,
collections of records which contain collections, etc.).
Structures used in this demo:
SET DEFINE OFF;
CREATE OR REPLACE TYPE
uc_employee_names_vat IS VARRAY(1000) OF VARCHAR2(4000);
/
CREATE OR REPLACE TYPE
employee_ids_ntt IS TABLE OF INTEGER;
/
CREATE TABLE DEPARTMENT_DENORMS
(
DEPARTMENT_ID
NUMBER(6),
MAX_SALARY NUMBER,
EMPLOYEE_NAMES
UC_EMPLOYEE_NAMES_VAT,
EMPLOYEE_IDS EMPLOYEE_IDS_NTT
)
NESTED TABLE EMPLOYEE_IDS STORE
AS NESTED_ID_TABLE;
SET DEFINE OFF;
Insert into DEPARTMENT_DENORMS(DEPARTMENT_ID, MAX_SALARY,
EMPLOYEE_NAMES, EMPLOYEE_IDS)
Values (30, 11000,
"UC_EMPLOYEE_NAMES_VAT"('DEN RAPHAELY','ALEXANDER
KHOO','SHELLI BAIDA','SIGAL TOBIAS','GUY HIMURO','KAREN
COLMENARES',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL),
"EMPLOYEE_IDS_NTT"(114,115,116,117,118,119));
SET DEFINE OFF;
Insert into DEPARTMENT_DENORMS(DEPARTMENT_ID, MAX_SALARY,
EMPLOYEE_NAMES, EMPLOYEE_IDS)
Values (50, 8000,
"UC_EMPLOYEE_NAMES_VAT"('MATTHEW
WEISS',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL),
"EMPLOYEE_IDS_NTT"(120));
SET DEFINE OFF;
Insert into DEPARTMENT_DENORMS(DEPARTMENT_ID, MAX_SALARY,
EMPLOYEE_NAMES, EMPLOYEE_IDS)
Values (60, 9000,
"UC_EMPLOYEE_NAMES_VAT"('ALEXANDER HUNOLD','BRUCE
ERNST','DAVID AUSTIN','VALLI PATABALLA','DIANA
LORENTZ',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL),
"EMPLOYEE_IDS_NTT"(103,104,105,106,107));
SET DEFINE OFF;
Insert into DEPARTMENT_DENORMS(DEPARTMENT_ID, MAX_SALARY,
EMPLOYEE_NAMES, EMPLOYEE_IDS)
Values (90, 24000,
"UC_EMPLOYEE_NAMES_VAT"('STEVEN KING','NEENA
KOCHHAR','LEX DE
HAAN',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL),
"EMPLOYEE_IDS_NTT"(100,101,102));
SET DEFINE OFF;
Insert into DEPARTMENT_DENORMS(DEPARTMENT_ID, MAX_SALARY,
EMPLOYEE_NAMES, EMPLOYEE_IDS)
Values (100, 12000,
"UC_EMPLOYEE_NAMES_VAT"('NANCY GREENBERG','DANIEL
FAVIET','JOHN CHEN','ISMAEL SCIARRA','JOSE MANUEL URMAN','LUIS
POPP',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL),
"EMPLOYEE_IDS_NTT"(108,109,110,111,112,113));
COMMIT;
07-1: Work with two dimensional
arrays
1. Define a two dimensional array of strings and perform the following
operations using "direct access" to the array structure:
a. Assign "abc" to the cell (100, 300).
b. Display the value in cell (5, 100), if it exists.
If not (and it doesn't exist, does it?), display "Undefined cell:
(5,100)".
c. Assign "def" to the cell (-15, 1000).
2. (optional) Build a small package API that lets me get and set cell
values, as well as determine if a cell is defined, via procedures and
functions. Then perform steps 1.a-1.c using the API.
Solution: for 07-1: Work with two dimensional arrays
DECLARE
/*
Define a two dimensional array of strings and perform the following operations:
a. Assign "abc" to the cell (100, 300).
b. Assign "def" to the cell (-15, 1000).
c. Write code to check to see if the cell at (5, 100) exists.
*/
-- First with direct access....
TYPE dim1_aat IS TABLE OF VARCHAR2(32767) INDEX BY PLS_INTEGER;
TYPE dim2_aat IS TABLE OF dim1_aat INDEX BY PLS_INTEGER;
twodim_array dim2_aat;
BEGIN
twodim_array ( 300 ) ( 100 ) := 'abc';
BEGIN
DBMS_OUTPUT.put_line ( twodim_array ( 1000 ) ( -15 ));
EXCEPTION
WHEN NO_DATA_FOUND
THEN
DBMS_OUTPUT.put_line ( 'No cell defined at (-15,1000)' );
END;
twodim_array ( 1000 ) ( -15 ) := 'def';
END;
/
-- Now the API....
CREATE OR REPLACE PACKAGE twodim
IS
TYPE dim1_aat IS TABLE OF VARCHAR2(32767) INDEX BY PLS_INTEGER;
TYPE dim2_aat IS TABLE OF dim1_aat INDEX BY PLS_INTEGER;
PROCEDURE setcell (array_in IN OUT dim2_aat ,
dim1_in PLS_INTEGER,
dim2_in PLS_INTEGER,
value_in IN VARCHAR2);
FUNCTION getcell (array_in IN dim2_aat,
dim1_in PLS_INTEGER,
dim2_in PLS_INTEGER) RETURN VARCHAR2;
FUNCTION EXISTS (array_in IN dim2_aat,
dim1_in PLS_INTEGER,
dim2_in PLS_INTEGER) RETURN BOOLEAN;
END twodim;
/
CREATE OR REPLACE PACKAGE BODY twodim
IS
PROCEDURE setcell (array_in IN OUT dim2_aat ,
dim1_in PLS_INTEGER,
dim2_in PLS_INTEGER,
value_in IN VARCHAR2)
IS
BEGIN
array_in ( dim2_in ) ( dim1_in ) := value_in;
END;
FUNCTION getcell (array_in IN dim2_aat,
dim1_in PLS_INTEGER,
dim2_in PLS_INTEGER) RETURN VARCHAR2
IS
BEGIN
RETURN array_in ( dim2_in ) ( dim1_in );
END;
FUNCTION EXISTS (array_in IN dim2_aat,
dim1_in PLS_INTEGER,
dim2_in PLS_INTEGER) RETURN BOOLEAN
IS
l_value VARCHAR2 ( 32767 );
BEGIN
l_value := array_in ( dim2_in ) ( dim1_in );
RETURN TRUE;
EXCEPTION
WHEN NO_DATA_FOUND OR VALUE_ERROR
THEN
RETURN FALSE;
END;
END twodim;
/
-- And a rewrite of the original anonymous block using the API...
DECLARE
twodim_array twodim.dim2_aat;
BEGIN
twodim.setcell ( twodim_array, 100, 300, 'abc' );
IF twodim.EXISTS ( twodim_array, -15, 1000 )
THEN
DBMS_OUTPUT.put_line ( twodim.getcell ( twodim_array, -15, 1000 ));
ELSE
DBMS_OUTPUT.put_line ( 'No cell defined at (5,1000)' );
END IF;
twodim.setcell ( twodim_array, 5, 100, 'def' );
END;
/
07-2: Multi-level collections
and string-based indexes
Overview: use PL/SQL to organize all the employee ID and last name of
the employee, sorted by the first character in their name, into a
collection.
In other words, the data is organized like this:
A
1051, Albert
1445, Anderson
B
7060, Brumbaugh
8422, Bugsby
etc.
1. Build the following packaged API to satisfy this requirement:
a. Define a collection of collections of records
(CCR), so that for a given letter (that is, indexed by the letter), I
can obtain the list of all the employee IDs and last names whose last
names start with that letter.
b. A function that accepts a department ID and
returns in the CCR all of the employee IDs and last names in that
department as, sorted into separate lists for each distinct starting
character. If department ID is null, then return the information for
all employees.
c. A procedure that displays the contents of this
CCR.
2. Write an anonymous block that retrieves and displays this sorted
employee information for departments 50, 100 and all departments.
Solution: for 07-2: Multi-level collections and string-based indexes
CREATE OR REPLACE PACKAGE first_char
IS
-- Record containing employee ID and last name.
TYPE emp_info_rt IS RECORD (
employee_id employees.employee_id%TYPE
, last_name employees.last_name%TYPE
);
-- Collection/list of these records, indexed sequentially
TYPE employees_by_letter_aat IS TABLE OF emp_info_rt INDEX BY PLS_INTEGER;
-- A collection of employee-lastname lists, indexed by a single character string
TYPE alpha_names_aat IS TABLE OF employees_by_letter_aat INDEX BY VARCHAR2(1);
-- For a given department ID, return the collection of employee-lastname lists.
FUNCTION emps_in_dept ( department_id_in IN employees.department_id%TYPE ) RETURN alpha_names_aat;
-- Display the contents of such a list.
PROCEDURE show_emps ( title_in IN VARCHAR2, list_in IN alpha_names_aat );
END first_char;
/
CREATE OR REPLACE PACKAGE BODY first_char
IS
FUNCTION emps_in_dept ( department_id_in IN employees.department_id%TYPE )
RETURN alpha_names_aat
IS
l_temp_emps employees_by_letter_aat;
l_sorted_emps alpha_names_aat;
l_letter VARCHAR2(1);
l_new_row PLS_INTEGER;
BEGIN
-- Move all of the employee IDs and last names into a temporary collection.
SELECT employee_id, last_name BULK COLLECT INTO l_temp_emps
FROM employees
WHERE department_id = department_id_in
OR department_id_in IS NULL;
-- Sort by the first letter. If we haven't yet assigned the first row for
-- a given letter, then assign it to index 1. This will avoid a NO_DATA_FOUND exception.
FOR indx IN 1 .. l_temp_emps.COUNT
LOOP
l_letter := SUBSTR(l_temp_emps(indx).last_name, 1, 1 );
IF l_sorted_emps.EXISTS(l_letter) THEN
-- Add to the end of the list. I assign the expression to a local variable to improve readability.
l_new_row := l_sorted_emps(l_letter).COUNT + 1;
l_sorted_emps(l_letter)(l_new_row) := l_temp_emps(indx);
ELSE
l_sorted_emps(l_letter)(1) := l_temp_emps(indx);
END IF;
END LOOP;
RETURN l_sorted_emps;
END emps_in_dept;
PROCEDURE show_emps ( title_in IN VARCHAR2, list_in IN alpha_names_aat )
IS
-- Display the contents, iterating by each of the letters in alphabetical order.
l_letter VARCHAR2 ( 1 );
BEGIN
DBMS_OUTPUT.put_line ( title_in );
--
-- My outer loop goes through a string-indexed collection, so I cannot use a FOR loop.
-- My inner loop is a sequentially filled, integer-indexed list so I *CAN* in this case use a FOR loop.
l_letter := list_in.FIRST;
WHILE (l_letter IS NOT NULL)
LOOP
DBMS_OUTPUT.put_line( ' Employees whose last names start with "' || l_letter || '"' );
-- Sequentially filled, so we can use a FOR loop.
FOR indx IN 1 .. list_in ( l_letter ).COUNT
LOOP
DBMS_OUTPUT.put_line ( ' ' || list_in(l_letter)(indx).employee_id || ', ' || list_in(l_letter)(indx).last_name );
END LOOP;
-- Move on to the next letter that has last names in the employees table.
l_letter := list_in.NEXT(l_letter);
END LOOP;
DBMS_OUTPUT.put_line ( '' );
END show_emps;
END first_char;
/
DECLARE
l_emps first_char.alpha_names_aat;
BEGIN
l_emps := first_char.emps_in_dept ( 100 );
first_char.show_emps ( '*** Employees in department 100', l_emps );
l_emps := first_char.emps_in_dept ( 50 );
first_char.show_emps ( '*** Employees in department 50', l_emps );
l_emps := first_char.emps_in_dept ( NULL );
first_char.show_emps ( '*** All Employees', l_emps );
END;
/
07-3: Extend string usage
package to maintain multiple lists
Build on your solution in "06-2: Use string-indexed collection to keep
track of usages" to allow a user to keep track of various named lists
of strings, simultaenously.
Write a script demonstrating its functionality.
Solution: for 07-3: Extend string usage package to maintain
multiple lists
CREATE OR REPLACE PACKAGE string_tracker2
IS
SUBTYPE maxvarchar2_t IS VARCHAR2(32767);
PROCEDURE clear_all_lists;
PROCEDURE clear_used_list ( list_in IN maxvarchar2_t );
FUNCTION string_in_use ( list_in IN maxvarchar2_t, value_in IN maxvarchar2_t ) RETURN BOOLEAN;
PROCEDURE mark_as_used ( list_in IN maxvarchar2_t, value_in IN maxvarchar2_t );
END string_tracker2;
/
CREATE OR REPLACE PACKAGE BODY string_tracker2
IS
TYPE used_list_aat IS TABLE OF BOOLEAN INDEX BY maxvarchar2_t;
TYPE list_of_lists_aat IS TABLE OF used_list_aat INDEX BY maxvarchar2_t;
g_list_of_lists list_of_lists_aat;
PROCEDURE clear_all_lists
IS
BEGIN
g_list_of_lists.DELETE;
END clear_all_lists;
PROCEDURE clear_used_list ( list_in IN maxvarchar2_t )
IS
BEGIN
-- Remove the list like it never existed...
g_list_of_lists.DELETE(list_in);
-- Or remove all the elements in the list.
-- g_list_of_lists( list_in ).DELETE;
END clear_used_list;
FUNCTION string_in_use ( list_in IN maxvarchar2_t, value_in IN maxvarchar2_t )
RETURN BOOLEAN
IS
BEGIN
-- Assumes that mark_as_used always set row to TRUE.
RETURN g_list_of_lists(list_in)(value_in);
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN FALSE;
END string_in_use;
PROCEDURE mark_as_used ( list_in IN maxvarchar2_t, value_in IN maxvarchar2_t )
IS
BEGIN
g_list_of_lists(list_in) (value_in) := TRUE;
END mark_as_used;
END string_tracker2;
/
08. Working with collections in
SQL statements
You can manipulate and query the contents of nested tables and varrays
that are defined as columns in relational tables.
You can also query the contents of nested tables and varrays with the
TABLE operator.
For these exercises, you will be working with the departments_denorm
table, which contains denoramlized data from the employees table,
namely for each department...
* the maximum salary in that department.
* the upper-cased full names of all employees in that department
(varray)
* the id of all employees in that department (nested table)
Here are the DDL statements defining these elements:
SET DEFINE OFF;
CREATE OR REPLACE TYPE
uc_employee_names_vat IS VARRAY(1000) OF VARCHAR2(4000);
/
CREATE OR REPLACE TYPE
employee_ids_ntt IS TABLE OF INTEGER;
/
CREATE TABLE DEPARTMENT_DENORMS
(
DEPARTMENT_ID
NUMBER(6),
MAX_SALARY NUMBER,
EMPLOYEE_NAMES
UC_EMPLOYEE_NAMES_VAT,
EMPLOYEE_IDS EMPLOYEE_IDS_NTT
)
NESTED TABLE EMPLOYEE_IDS STORE
AS NESTED_ID_TABLE;Solution: for 12-2: Use subset-related
operators with nested tables
SET DEFINE OFF;
Insert into
DEPARTMENT_DENORMS(DEPARTMENT_ID, MAX_SALARY, EMPLOYEE_NAMES,
EMPLOYEE_IDS)
Values (30, 11000,
"UC_EMPLOYEE_NAMES_VAT"('DEN RAPHAELY','ALEXANDER KHOO','SHELLI
BAIDA','SIGAL TOBIAS','GUY HIMURO','KAREN
COLMENARES',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL),
"EMPLOYEE_IDS_NTT"(114,115,116,117,118,119));
SET DEFINE OFF;
Insert into
DEPARTMENT_DENORMS(DEPARTMENT_ID, MAX_SALARY, EMPLOYEE_NAMES,
EMPLOYEE_IDS)
Values (50, 8000,
"UC_EMPLOYEE_NAMES_VAT"('MATTHEW
WEISS',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL),
"EMPLOYEE_IDS_NTT"(120));
SET DEFINE OFF;
Insert into
DEPARTMENT_DENORMS(DEPARTMENT_ID, MAX_SALARY, EMPLOYEE_NAMES,
EMPLOYEE_IDS)
Values (60, 9000,
"UC_EMPLOYEE_NAMES_VAT"('ALEXANDER HUNOLD','BRUCE ERNST','DAVID
AUSTIN','VALLI PATABALLA','DIANA
LORENTZ',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL),
"EMPLOYEE_IDS_NTT"(103,104,105,106,107));
SET DEFINE OFF;
Insert into
DEPARTMENT_DENORMS(DEPARTMENT_ID, MAX_SALARY, EMPLOYEE_NAMES,
EMPLOYEE_IDS)
Values (90, 24000,
"UC_EMPLOYEE_NAMES_VAT"('STEVEN KING','NEENA KOCHHAR','LEX DE
HAAN',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL),
"EMPLOYEE_IDS_NTT"(100,101,102));
SET DEFINE OFF;
Insert into
DEPARTMENT_DENORMS(DEPARTMENT_ID, MAX_SALARY, EMPLOYEE_NAMES,
EMPLOYEE_IDS)
Values (100, 12000,
"UC_EMPLOYEE_NAMES_VAT"('NANCY GREENBERG','DANIEL FAVIET','JOHN
CHEN','ISMAEL SCIARRA','JOSE MANUEL URMAN','LUIS
POPP',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL),
"EMPLOYEE_IDS_NTT"(108,109,110,111,112,113));
COMMIT;
08-1: Query contents of PL/SQL
variable nested tables and varrays
A. Create a schema level nested table type that holds strings of up to
100 characters in length.
B. Create a schema level varray type (maximum 500 elements) based on an
object type that has the same structure (one attribute for each column)
as the JOBS table:
CREATE TABLE jobs (
job_id VARCHAR2(10),
job_title VARCHAR2(35),
min_salary NUMBER(6),
max_salary NUMBER(6)
);
C. Write an anonymous block that declares a local variable based on
each of the above types. Populate the nested table with your choice
(and number) of strings.
Populate the varray with at least 3 objects.
D. After populating the collections, write cursor FOR loops based on
SELECT statements that retrieve the information from these collections
and display them using DBMS_OUTPUT.PUT_LINE.
Display the job information ordered by the minimum salary.
Solution: for 08-1: Query contents of PL/SQL variable nested tables
and varrays
/* A. Create a schema level nested table type that holds strings of up to 100 characters in length. */
CREATE OR REPLACE TYPE string_ntt IS TABLE OF VARCHAR2(100);
/
/* B. Create a schema level varray type (maximum 500 elements) based
on an object type that has the same structure (one attribute for each column) as the JOBS table:
CREATE TABLE jobs (
job_id VARCHAR2(10),
job_title VARCHAR2(35),
min_salary NUMBER(6),
max_salary NUMBER(6)
);
*/
CREATE TYPE jobs_ot IS OBJECT (
job_id VARCHAR2 ( 10 )
, job_title VARCHAR2 ( 35 )
, min_salary NUMBER ( 6 )
, max_salary NUMBER ( 6 )
)
/
CREATE OR REPLACE TYPE jobs_vat IS VARRAY(500) OF jobs_ot;
/
/* C. Write an anonymous block that declares a local variable based on
each of the above types. Populate the nested table with your choice
(and number) of strings. Populate the varray with at least 3 objects.*/
DECLARE
l_strings string_ntt := string_ntt();
l_jobs jobs_vat := jobs_vat();
BEGIN
l_strings.EXTEND(100);
FOR indx IN 1 .. 100
LOOP
l_strings(indx) := 'PL/SQL is ' || indx || 'X better than Java!';
END LOOP;
l_jobs.EXTEND ( 3 );
--
-- Assign values in constructor...
l_jobs(1) := jobs_ot( '100', 'Toothbrush Scrubber', 1000, 10000 );
--
-- Assign values explicitly after constructor call...
l_jobs(2) := jobs_ot ( NULL, NULL, NULL, NULL );
l_jobs(2).job_id := '200';
l_jobs(2).job_title := 'Salamander Companion';
l_jobs(2).min_salary := 54000;
l_jobs(2).min_salary := 70990;
--
l_jobs(3) := jobs_ot ( '300', 'Spoon Concavity QA', 120000, 475000 );
/*
D. After populating the collections, write cursor FOR loops based on
SELECT statements that retrieve the information from these collections
and display them using DBMS_OUTPUT.PUT_LINE. */
FOR rec IN ( SELECT COLUMN_VALUE FROM TABLE(l_strings)
ORDER BY COLUMN_VALUE )
LOOP
DBMS_OUTPUT.put_line(rec.COLUMN_VALUE);
END LOOP;
--
FOR rec IN ( SELECT * FROM TABLE(l_jobs)
ORDER BY min_salary)
LOOP
DBMS_OUTPUT.put_line(rec.job_title);
END LOOP;
END;
/
08-2: Query contents of nested
table and varray columns
A. Write a query that displays the department ID and each of the
upper-cased, full names of the employees in that department, querying
only from the department_denorms table.
B. Show only those employees whose names contain an "E".
Solution: for 08-2s: Query contents of nested table and varray
columns
-- A. Write a query that displays the department ID and each of the
-- upper-cased, full names of the employees in that department,
-- querying only from the department_denorms table.
SELECT department_id, COLUMN_VALUE
FROM department_denorms, TABLE(employee_names)
/
-- B. Show only those employees whose names contain an "E".
SELECT dd.department_id, nms.COLUMN_VALUE
FROM department_denorms dd, TABLE(dd.employee_names) nms
WHERE nms.COLUMN_VALUE LIKE '%E%'
/
08-3: Update nested table and
varray columns
A. Set all the employee IDs in department_denorms table for department
50 to their negative (that is, 500 -> -500).
B. Show the IDs only of those employees in department 50.
Solution: for 08-3: Update nested table and varray columns
-- A. Set all the employee IDs in department_denorms table for department 50 to their negative (that is, 500 -> -500).
UPDATE TABLE (SELECT employee_ids
FROM department_denorms
WHERE department_id = 50 )
SET COLUMN_VALUE = -1 * COLUMN_VALUE
/
-- B. Show the IDs only of those employees in department 50.
SELECT ids.COLUMN_VALUE
FROM TABLE ( SELECT employee_ids
FROM department_denorms
WHERE department_id = 50 ) ids
/
08-4: Populate collection
columns with FORALL
As a special exercise, write the code needed to populate the
denormalization table, department_denorms, from the employees table.
See "08. Working with collections in SQL statements" for the DDL that
creates these objects.
Solution: for 08-4: Populate collection columns with FORALL
DECLARE
TYPE employees_aat IS TABLE OF employees%ROWTYPE INDEX BY PLS_INTEGER;
l_employees employees_aat;
TYPE denorms_aat IS TABLE OF department_denorms%ROWTYPE INDEX BY PLS_INTEGER;
l_denorms denorms_aat;
l_index department_denorms.department_id%TYPE;
BEGIN
SELECT * BULK COLLECT INTO l_employees
FROM employees;
FOR indx IN 1 .. l_employees.COUNT
LOOP
-- Initialize the row if necessary.
l_index := l_employees(indx).department_id;
IF NOT l_denorms.EXISTS() THEN
l_denorms(l_index).department_id := l_index;
l_denorms(l_index).max_salary := 0;
l_denorms(l_index).employee_ids := employee_ids_ntt();
l_denorms(l_index).employee_names := uc_employee_names_vat();
END IF;
-- Set the maximum salary.
l_denorms ( l_index ).max_salary := GREATEST(l_denorms(l_index).max_salary, l_employees(indx).salary);
-- Add the upper cased name.
l_denorms(l_index).employee_names.EXTEND;
l_denorms(l_index).employee_names( l_denorms(l_index).employee_names.COUNT) := UPPER(l_employees(indx).first_name|| ' ' || l_employees(indx).last_name);
-- Add the employee id.
l_denorms(l_index).employee_ids.EXTEND;
l_denorms(l_index).employee_ids(l_denorms(l_index).employee_ids.COUNT) := l_employees(indx).employee_id;
END LOOP;
-- Push the data into the denorm table.
FORALL indx IN INDICES OF l_denorms
INSERT INTO department_denorms
VALUES l_denorms(indx);
COMMIT;
END;
/
09. BULK COLLECT for high
performance querying
This section covers BULK COLLECT in more detail and with more of its
features, in particular the LIMIT clause.
09-1: Use BULK COLLECT to query
limited number of rows from table
Write an anonymous block that retrieves up to 10 rows at a time from
employees table and moves them to a nested table of records based on
the employees%ROWTYPE.
Display the last name for each row retrieved, then exit from the loop
when all rows have been queried.
Solution: for 09-1: Use BULK COLLECT to query limited number of
rows from table
DECLARE
CURSOR allrows_cur IS SELECT *
FROM employees;
TYPE employees_ntt IS TABLE OF employees%ROWTYPE;
l_employees employees_ntt;
l_row PLS_INTEGER;
BEGIN
OPEN allrows_cur;
LOOP
FETCH allrows_cur
BULK COLLECT INTO l_employees LIMIT 10;
-- Remember: BULK COLLECT will NOT raise NO_DATA_FOUND if no rows are queried.
-- Instead, check the contents of the collection to see if you have anything left to process.
EXIT WHEN l_employees.COUNT = 0;
-- Process the data, if any.
l_row := l_employees.FIRST;
DBMS_OUTPUT.put_line ( '--------------------' );
WHILE ( l_row IS NOT NULL )
LOOP
DBMS_OUTPUT.put_line ( l_employees ( l_row ).last_name );
l_row := l_employees.NEXT ( l_row );
END LOOP;
END LOOP;
-- Clean up when done: close the cursor and delete everything
-- in the collection.
CLOSE allrows_cur;
l_employees.DELETE;
END;
/
09-2: Use BULK COLLECT With
EXECUTE IMMEDIATE
Write a procedure that accepts as its arguments the following:
a. the name of the table to be queried
b. the name of the string column to be queried
c. optional "after from" clause: SQL text that can follow the FROM
clause of a query
Then use BULK COLLECT with EXECUTE IMMEDIATE to retrieve the values
from the specified column.
You can use the DBMS_SQL.VARCHAR2s collection type, or define your own.
After querying the data, write a loop that iterates through the
contents of the collection and displays the string values.
Then write an anonymous block to exercise this procedure.
Solution: for 09-2: Use BULK COLLECT With EXECUTE IMMEDIATE
CREATE OR REPLACE PROCEDURE show_string_values (
table_in IN VARCHAR2
, column_in IN VARCHAR2
, after_from_in IN VARCHAR2 DEFAULT NULL
)
/*
Write a procedure that accepts as its arguments the following:
a. the name of the table to be queried
b. the name of the string column to be queried
c. optional "after from" clause: SQL text that can follow the
FROM clause of a query
Then use BULK COLLECT with EXECUTE IMMEDIATE to retrieve the values
from the specified column.
You can use the DBMS_SQL.vARCHAR2s collection type, or define your own.
After querying the data, write a loop that iterates through the
contents of the collection and displays the string values.
*/
IS
l_string_values DBMS_SQL.varchar2s;
BEGIN
EXECUTE IMMEDIATE 'select '
|| column_in
|| ' FROM '
|| table_in
|| ' '
|| after_from_in
BULK COLLECT INTO l_string_values;
FOR indx IN 1 .. l_string_values.COUNT
LOOP
DBMS_OUTPUT.put_line(l_string_values(indx ));
END LOOP;
END show_string_values;
/
BEGIN
show_string_values ( 'employees', 'last_name', 'where department_id = 50' );
DBMS_OUTPUT.put_line ( '' );
show_string_values ( 'employees'
, 'first_name'
, 'where department_id = 100'
);
END;
/
10.
FORALL for high performance DML
This section covers FORALL in more detail and with more of its
features, including:
SAVE EXCEPTIONS
The RETURNING clause
FORALL and collections of records
10-1: Working with SAVE
EXCEPTIONS
The following procedure executes a FORALL update of the employees
table. The values in the array will cause errors in the update.
CREATE OR REPLACE PROCEDURE
BULK_EXCEPTIONS IS
TYPE namelist_t IS TABLE
OF VARCHAR2 ( 1000 );
-- employee.last_name%TYPE;
enames_with_errors
namelist_t := namelist_t (
'ABC', 'DEF', NULL, 'LITTLE', RPAD ( 'BIGBIGGERBIGGEST', 250,
'ABC' ), 'SMITHIE');
BEGIN
FORALL indx IN
enames_with_errors.FIRST .. enames_with_errors.LAST
UPDATE
employees SET last_name =
enames_with_errors(indx);
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.put_line ( 'Updated ' || SQL%ROWCOUNT || ' rows.' );
DBMS_OUTPUT.put_line ( DBMS_UTILITY.format_error_stack );
END;
/
Add a SAVE EXCEPTIONS clause to the FORALL statement and an exception
section to the procedure that traps the bulk error exception, -24381,
and iterates through the contents of the pseudo-collection,
SQL%BULK_EXCEPTIONS, displaying the error code and the value in the
binding collection that caused the error.
Furthermore, if the error is "value too long for column", then issue
the UPDATE statement for that row using SUBSTR to ensure the value is
not too long.
After performing the update (and in the exception sections), issue a
ROLLBACK so that the table's data is not changed.
Solution: for 10-1: Working with SAVE EXCEPTIONS
CREATE OR REPLACE PROCEDURE BULK_EXCEPTIONS
IS
TYPE namelist_t IS TABLE OF VARCHAR2 ( 1000 );
-- employee.last_name%TYPE;
enames_with_errors namelist_t := namelist_t ( 'ABC', 'DEF', NULL, 'LITTLE', RPAD ( 'BIGBIGGERBIGGEST', 250, 'ABC' ), 'SMITHIE');
l_name employees.last_name%TYPE;
e_bulk_errors EXCEPTION;
PRAGMA EXCEPTION_INIT ( e_bulk_errors, -24381 );
BEGIN
FORALL indx IN enames_with_errors.FIRST .. enames_with_errors.LAST SAVE EXCEPTIONS
UPDATE employees SET last_name = enames_with_errors(indx);
ROLLBACK;
EXCEPTION
/* You really DON'T want to be writing code like this....
WHEN OTHERS THEN
IF SQLCODE = -24381 */
WHEN e_bulk_errors THEN
DBMS_OUTPUT.put_line ( 'Updated ' || SQL%ROWCOUNT || ' rows.' );
FOR indx IN 1 .. SQL%BULK_EXCEPTIONS.COUNT
LOOP
DBMS_OUTPUT.put_line
( 'Error '
|| indx
|| ' occurred during '
|| 'iteration '
|| SQL%BULK_EXCEPTIONS ( indx ).ERROR_INDEX
|| ' updating name to '
|| enames_with_errors
( SQL%BULK_EXCEPTIONS ( indx ).ERROR_INDEX )
);
DBMS_OUTPUT.put_line('Oracle error is ' || SQLERRM(-1 * SQL%BULK_EXCEPTIONS ( indx ).ERROR_CODE));
IF SQL%BULK_EXCEPTIONS(indx).ERROR_CODE = 12899 THEN
DBMS_OUTPUT.put_line('Correcting for too long name of "' || enames_with_errors(SQL%BULK_EXCEPTIONS(indx).ERROR_INDEX) || '"');
l_name :=SUBSTR(enames_with_errors(SQL%BULK_EXCEPTIONS(indx).ERROR_INDEX ), 1, 25);
UPDATE employees SET last_name = l_name;
END IF;
END LOOP;
-- Avoid saving all those last names!
ROLLBACK;
WHEN OTHERS THEN
DBMS_OUTPUT.put_line ( 'Updated ' || SQL%ROWCOUNT || ' rows.' );
DBMS_OUTPUT.put_line ( DBMS_UTILITY.format_error_stack );
-- Avoid saving all those last names!
ROLLBACK;
END;
/
10-2: Use FORALL with a
RETURNING clause
1. Declare a collection of employee last names.
2. Populate that collection with these three values:
S%
E%
%A%
3. Use FORALL to double the salaries of all employees whose last names
are LIKE the values in each of the rows in that collection. As a part
of that FORALL, retrieve the employee IDs of each person who received
the salary increase.
4. Display those employee IDs.
5. Issue a ROLLBACK so that the table's data is not changed.
Solution: for 10-2: Use FORALL with a RETURNING clause
DECLARE
-- 1. Declare a collection of employee last names.
TYPE names_aat IS TABLE OF employees.last_name%TYPE INDEX BY PLS_INTEGER;
l_filters names_aat;
l_lucky_few names_aat;
--
TYPE ids_aat IS TABLE OF employees.employee_id%TYPE INDEX BY PLS_INTEGER;
l_employee_ids ids_aat;
BEGIN
-- 2. Populate that collection with these three values:
l_filters ( 1 ) := 'S%';
l_filters ( 2 ) := 'E%';
l_filters ( 3 ) := '%A%';
/*
3. Use FORALL to double the salaries of all employees whose last names
are LIKE the values in each of the rows in that collection. As a part
of that FORALL, retrieve the employee ID and last name of each person
who received the salary increase.
*/
FORALL indx IN 1 .. l_filters.COUNT
UPDATE employees SET salary = salary * 2
WHERE last_name LIKE l_filters ( indx )
RETURNING employee_id, last_name BULK COLLECT INTO l_employee_ids, l_lucky_few;
-- 4. Display those employee IDs.
FOR indx IN 1 .. l_employee_ids.COUNT
LOOP
DBMS_OUTPUT.put_line (l_employee_ids(indx) || ', ' || l_lucky_few(indx));
END LOOP;
ROLLBACK;
END;
/
10-3: Use FORALL with EXECUTE
IMMEDIATE
Write a procedure that accepts as its arguments the following:
a. the name of the table to be queried
b. the name of the string column to be updated
c. a collection of type DBMS_SQL.VARCHAR2s containing filters (eg,
"%S%")
d. maximum length of new string value to be placed back in the
specified column
Then use FORALL with EXECUTE IMMEDIATE to update all the rows that
match each/any of the filters in the collection, setting the string
column value to a concatenation of the string to itself.
After performing the update...
a. Display the total number of rows modified, as well as the number of
rows modified by each filter.
b. Issue a ROLLBACK so that the table's data is not changed.
Then write an anonymous block to exercise this procedure.
Solution: for 10-3: Use FORALL with EXECUTE IMMEDIATE
CREATE OR REPLACE PROCEDURE change_string_values (
table_in IN VARCHAR2
, column_in IN VARCHAR2
, name_filters_in IN DBMS_SQL.varchar2s
, max_length_in IN PLS_INTEGER
)
/*
Write a procedure that accepts as its arguments the following:
a. the name of the table to be queried
b. the name of the string column to be updated
c. a collection of type DBMS_SQL.VARCHAR2s containing filters (eg, "%S%")
d. maximum length of new string value to be placed back in the specified column
Then use FORALL with EXECUTE IMMEDIATE to update all the rows specified in the table,
setting the string column value to a concatenation of the string to itself.
You can use the DBMS_SQL.VARCHAR2s collection type, or define your own.
After performing the update, issue a ROLLBACK so that the table's data is not changed.
Then write an anonymous block to exercise this procedure.
*/
IS
BEGIN
-- That's a PRETTY CRAZY dynamic SQL statement!
FORALL indx IN 1 .. name_filters_in.COUNT
EXECUTE IMMEDIATE 'UPDATE '
|| table_in
|| ' SET '
|| column_in
|| ' = SUBSTR ('
|| column_in
|| ' || '
|| column_in
|| ', 1, '
|| max_length_in
|| ') WHERE '
|| column_in
|| ' LIKE :colval '
USING name_filters_in ( indx );
DBMS_OUTPUT.put_line ( SQL%ROWCOUNT );
FOR indx IN 1 .. name_filters_in.COUNT
LOOP
DBMS_OUTPUT.put_line ('Filter ' || name_filters_in(indx) || ' affected ' || SQL%BULK_ROWCOUNT(indx) || ' rows');
END LOOP;
ROLLBACK;
END change_string_values;
/
DECLARE
l_filters DBMS_SQL.varchar2s;
BEGIN
l_filters(1) := '%E';
l_filters(2) := '%S%';
change_string_values('employees', 'last_name', l_filters , 25);
END;
/
10-4: Use FORALL with a
collection of records
A. Write an anonymous block that:
1. Use BULK COLLECT to populate a collection of records based on the
employees table with all the employees in department 50.
2. Iterate through all the elements in the collection, and double the
salaries.
3. Use FORALL to update those same rows back in the database, using the
SET ROW = syntax available in Oracle Database 10g.
4. Rollback your changes.
Solution: for 10-4: Use FORALL with a collection of records
DECLARE
/*
Write an anonymous block that:
1. Use BULK COLLECT to populate a collection of records based on the
employees table with all the employees in department 50.
2. Iterate through all the rows, and double the salaries.
3. Use FORALL to update those same rows back in the database, using
the SET ROW = syntax available in Oracle Database 10g.
4. Rollback your changes.
*/
TYPE employees_ntt IS TABLE OF employees%ROWTYPE INDEX BY PLS_INTEGER;
TYPE employee_ids_ntt IS TABLE OF employees.employee_id%TYPE INDEX BY PLS_INTEGER;
l_employees employees_ntt;
l_employee_ids employee_ids_ntt;
BEGIN
SELECT * BULK COLLECT INTO l_employees
FROM employees
WHERE department_id = 50;
FOR indx IN 1 .. l_employees.COUNT
LOOP
l_employees(indx).salary := l_employees(indx).salary * 2;
l_employee_ids(indx) := l_employees(indx).employee_id;
END LOOP;
FORALL indx IN 1 .. l_employees.COUNT
UPDATE employees SET ROW = l_employees(indx)
WHERE employee_id = l_employee_ids(indx);
ROLLBACK;
END;
/
10-5: Incremental commit
processing with FORALL
Suppose that you need to update each of the rows of the employee table
after some complex transformation of the data in the table.
The dataset is too large to process in a single transaction; you
receive the "Rollback segment too small" error if you attempt this. So
you must shift to incremental commit processing, but you still would
like to use FORALL.
Modify the procedure provided below to implement incremental commit
processing with the FORALL.
CREATE OR REPLACE PROCEDURE
raise_across_dept (
dept_in IN
employees.department_id%TYPE
, raise_in IN
employees.salary%TYPE
)
IS
l_counter PLS_INTEGER;
TYPE employee_tt IS TABLE
OF employees.employee_id%TYPE BY PLS_INTEGER;
employee_ids employee_tt;
TYPE salary_tt IS TABLE OF
employees.salary%TYPE BY PLS_INTEGER;
salaries salary_tt;
BEGIN
SELECT employee_id, salary BULK COLLECT INTO employee_ids,
salaries
FROM employees
WHERE
department_id = dept_in;
-- Some manipulation of
the data here, justifying
-- the use of PL/SQL...and
then the update:
FORALL indx IN 1 ..
employee_ids.COUNT
UPDATE
employees SET salary =
salaries(indx)
WHERE employee_id = employee_ids(indx);
ROLLBACK;
END raise_across_dept;
/
Solution: for 10-5: Incremental commit processing with FORALL
CREATE OR REPLACE PROCEDURE raise_across_dept (
dept_in IN employees.department_id%TYPE
, raise_in IN employees.salary%TYPE
, commit_after_in IN PLS_INTEGER
)
IS
l_last PLS_INTEGER;
l_start PLS_INTEGER;
l_end PLS_INTEGER;
TYPE employee_aat IS TABLE OF employees.employee_id%TYPE INDEX BY PLS_INTEGER;
employee_ids employee_aat;
TYPE salary_aat IS TABLE OF employees.salary%TYPE INDEX BY PLS_INTEGER;
salaries salary_aat;
BEGIN
SELECT employee_id, salary BULK COLLECT INTO employee_ids, salaries
FROM employees
WHERE department_id = dept_in;
l_last := employee_ids.LAST;
l_start := employee_ids.FIRST;
LOOP
EXIT WHEN l_start > l_last;
l_end := LEAST(l_start + commit_after_in, l_last);
--
FORALL indx IN l_start .. l_end
UPDATE employees SET salary = salaries(indx)
WHERE employee_id = employee_ids(indx);
--
-- Do the COMMIT; here, but we won't to preserve the info in the table....
-- COMMIT;
l_start := l_end + 1;
END LOOP;
COMMIT;
END raise_across_dept;
/
10-6: Use FORALL with
sparsely-filled collections
A. Write an anonymous block that:
1. Use BULK COLLECT to populate a collection of records based on the
employees table with all the employees in department 50.
2. Iterate through all the elements in the collection, and delete any
element (record) with a salary > 3500. If salary <= 3500, add
$1000 to the salary.
3. Use FORALL to update the remaining rows back in the database, using
the SET ROW = syntax available in Oracle Database 10g. Note that the
collection of records is now likely to be sparse.
4. Rollback your changes.
(Note: you should be able to easily adapt your solution in "10-4e: Use
FORALL with a collection of records" to give you a head-start on
completing this one.)
B. Write an anonymous block that:
1. Use BULK COLLECT to populate two collection of scalars, holding the
employee IDs (COE) and salaries (COS) for all the employees in
department 50.
2. Iterate through all the elements in the COS and populate
sequentially a collection of "flags" (COF) with the index value of the
COS if that record has a salary <= 3500. Also add $1000 to the
salary of those elements.
3. Use FORALL to update the salaries only for those elements in the COR
that are "marked" for update in the COI.
4. Rollback your changes.
(Note: you should be able to easily adapt your solution in A to give
you a head-start on completing B.)
Solution: for 10-6s: Use FORALL with sparsely-filled collections
-- A. Use of INDICES OF to leverage sparse collection
DECLARE
TYPE employees_ntt IS TABLE OF employees%ROWTYPE INDEX BY PLS_INTEGER;
TYPE employee_ids_ntt IS TABLE OF employees.employee_id%TYPE INDEX BY PLS_INTEGER;
l_employees employees_ntt;
l_employee_ids employee_ids_ntt;
BEGIN
SELECT * BULK COLLECT INTO l_employees
FROM employees
WHERE department_id = 50;
FOR indx IN 1 .. l_employees.COUNT
LOOP
IF l_employees(indx).salary > 3500 THEN
l_employees.DELETE(indx);
ELSE
-- Change the salary and set the employee Id for use in the update.
l_employees(indx).salary := l_employees(indx).salary + 1000;
l_employee_ids(indx) := l_employees(indx).employee_id;
END IF;
END LOOP;
FORALL indx IN INDICES OF l_employees
UPDATE employees SET ROW = l_employees(indx)
WHERE employee_id = l_employee_ids(indx);
ROLLBACK;
END;
/
-- B. Use of VALUES OF to leverage an "indirect" collection
DECLARE
/*
Write an anonymous block that:
1. Use BULK COLLECT to populate a collection of records based on the
employees table with all the employees in department 50.
2. Iterate through all the rows, and double the salaries.
3. Use FORALL to update those same rows back in the database, using
the SET ROW = syntax available in Oracle Database 10g.
4. Rollback your changes.
*/
TYPE employee_ids_aat IS TABLE OF employees.employee_id%TYPE INDEX BY PLS_INTEGER;
TYPE salaries_aat IS TABLE OF employees.salary%TYPE INDEX BY PLS_INTEGER;
TYPE update_indexes_aat IS TABLE OF PLS_INTEGER INDEX BY PLS_INTEGER;
l_employee_ids employee_ids_aat;
l_salaries salaries_aat;
l_selected update_indexes_aat;
BEGIN
SELECT employee_id, salary BULK COLLECT INTO l_employee_ids, l_salaries
FROM employees
WHERE department_id = 50;
FOR indx IN 1 .. l_employee_ids.COUNT
LOOP
IF l_salaries(indx) <= 3500 THEN
l_selected(l_selected.COUNT + 1) := indx;
l_salaries(indx) := l_salaries(indx) + 1000;
END IF;
END LOOP;
FORALL indx IN VALUES OF l_selected
UPDATE employees SET salary = l_salaries(indx)
WHERE employee_id = l_employee_ids(indx);
ROLLBACK;
END;
/
11.
Table Functions
Table functions are functions that return a collection and can be
called from within the FROM clause of a query using the TABLE operator.
11-1: Simple table functions
returning scalars
Suppose I need to write a query that returns N numbers (1 though 10, 16
though 75, etc.).
Write a function that returns a nested table of numbers, based on start
and end values, and then call that function within a query to display
the desired values.
Solution: for 11-1: Simple table functions returning scalars
DROP TYPE names_nt;
CREATE TYPE names_nt IS TABLE OF NUMBER;
/
CREATE OR REPLACE FUNCTION lotsa_numbers (count_in IN INTEGER)
RETURN names_nt
IS
retval names_nt := names_nt();
BEGIN
retval.EXTEND(count_in);
FOR indx IN 1 .. count_in
LOOP
retval(indx) := indx;
END LOOP;
RETURN retval;
END lotsa_numbers;
/
REM Call the table function within a SELECT staement.
SELECT column_value
FROM TABLE (lotsa_numbers (100)) names;
11-2: Streaming table functions
The task at hand:
Transform data in this table:
CREATE TABLE stocktable (
ticker VARCHAR2(20),
trade_date DATE,
open_price NUMBER,
close_price NUMBER
);
to this table:
CREATE TABLE tickertable (
ticker VARCHAR2(20),
pricedate DATE,
pricetype VARCHAR2(1),
price NUMBER
);
In other words, one row in stocktable becomes two rows in tickertable.
Pretend that this is a very complicated transformation and requires the
"intervention" of a PL/SQL function.
Perform this transformation entirely within a single SQL INSERT
statement, using a streaming table function: a function that returns a
nested table of objects with the same structure as the tickertable.
Use BULK COLLECT wherever possible to speed up processing.
Solution: for 11-2: Streaming table functions
CREATE TABLE stocktable (
ticker VARCHAR2(20),
trade_date DATE,
open_price NUMBER,
close_price NUMBER
);
BEGIN
-- Populate the table.
INSERT INTO stocktable VALUES ( 'ORCL', SYSDATE, 12.5, 12 );
INSERT INTO stocktable VALUES ( 'QSFT', SYSDATE, 13.2, 13.8 );
INSERT INTO stocktable VALUES ( 'MSFT', SYSDATE, 27, 27.04 );
FOR indx IN 1 .. 100000
LOOP
-- Might as well be optimistic!
INSERT INTO stocktable VALUES ( 'STK' || indx, SYSDATE, indx, indx + 15 );
END LOOP;
COMMIT;
END;
/
CREATE TABLE tickertable
(
ticker VARCHAR2(20),
pricedate DATE,
pricetype VARCHAR2(1),
price NUMBER
)
/
/*
Note: Must use a nested table or varray of objects
for the return type of a pipelined function
*/
CREATE TYPE tickertype AS OBJECT (
ticker VARCHAR2 ( 20 )
, pricedate DATE
, pricetype VARCHAR2 ( 1 )
, price NUMBER
);
/
CREATE TYPE tickertypeset AS TABLE OF tickertype;
/
CREATE OR REPLACE PACKAGE refcur_pkg
IS
TYPE refcur_t IS REF CURSOR RETURN stocktable%ROWTYPE;
END refcur_pkg;
/
CREATE OR REPLACE FUNCTION stockpivot ( dataset refcur_pkg.refcur_t )
RETURN tickertypeset
IS
out_obj tickertype := tickertype ( NULL, NULL, NULL, NULL );
--
TYPE dataset_tt IS TABLE OF stocktable%ROWTYPE INDEX BY PLS_INTEGER;
l_dataset dataset_tt;
retval tickertypeset := tickertypeset();
l_row PLS_INTEGER;
BEGIN
FETCH dataset BULK COLLECT INTO l_dataset;
l_row := l_dataset.FIRST;
WHILE ( l_row IS NOT NULL )
LOOP
out_obj.ticker := l_dataset(l_row).ticker;
out_obj.pricetype := 'O';
out_obj.price := l_dataset(l_row).open_price;
out_obj.pricedate := l_dataset(l_row).trade_date;
retval.EXTEND;
retval(retval.LAST) := out_obj;
--
out_obj.pricetype := 'C';
out_obj.price := l_dataset(l_row).close_price;
out_obj.pricedate := l_dataset(l_row).trade_date;
retval.EXTEND;
retval(retval.LAST) := out_obj;
--
l_row := l_dataset.NEXT(l_row);
END LOOP;
CLOSE dataset;
RETURN retval;
END;
/
BEGIN
INSERT INTO tickertable
SELECT *
FROM TABLE (stockpivot(CURSOR(SELECT *
FROM stocktable )));
END;
/
11-3: Pipelined table functions
Change the implementation of the function in 11-2 to be a pipelined
function.
Solution: for 11-3: Pipelined table functions
CREATE OR REPLACE FUNCTION stockpivot_pl (dataset refcur_pkg.refcur_t)
RETURN tickertypeset PIPELINED
IS
l_row_as_object tickertype := tickertype (NULL, NULL, NULL, NULL);
TYPE dataset_tt IS TABLE OF dataset%ROWTYPE INDEX BY PLS_INTEGER;
l_dataset dataset_tt;
retval tickertypeset := tickertypeset();
l_row pls_integer;
BEGIN
FETCH dataset BULK COLLECT INTO l_dataset;
CLOSE dataset;
l_row := l_dataset.FIRST;
WHILE (l_row IS NOT NULL)
LOOP
-- first row
l_row_as_object.ticker := l_dataset(l_row).ticker;
l_row_as_object.pricetype := 'O';
l_row_as_object.price := l_dataset(l_row).open_price;
l_row_as_object.pricedate := l_dataset(l_row).trade_date;
PIPE ROW (l_row_as_object);
-- second row
l_row_as_object.pricetype := 'C';
l_row_as_object.price := l_dataset(l_row).close_price;
l_row_as_object.pricedate := l_dataset(l_row).trade_date;
PIPE ROW (l_row_as_object);
l_row := l_dataset.NEXT (l_row);
END LOOP;
RETURN;
END;
/
12.
Multiset operations on nested tables
12-1: Check for equality
between nested tables
Write an anonymous block to populate three different nested tables of
strings as follows:
1, the last names of all employees in department 50 order by last name
descending (I will refer to this as LN50D)
2. the last names of all employees in department 50 order by last name
descending (I will refer to this as LN50A)
3. the last names of all employees in department 100 order by last name
descending (I will refer to this as LN100)
Write code to show whether or not:
ch1. LN50D is equal to LN50A
ch2. LN50D is equal to LN100
Add a NULL to the end of LN50D, and the letter "X" to the end of LN50A,
and perform the same two checks again (ch1 and ch2), displaying the
results.
Solution: for 12-1: Check for equality between nested tables
DECLARE
TYPE name_ntt IS TABLE OF employees.last_name%TYPE;
-- I initialize these two lists to demonstrate some odd
-- behavior regarding NULL values in these structures.
ln50d name_ntt := name_ntt('a', 'b', NULL );
ln50a name_ntt := name_ntt('b', 'a', NULL );
ln100 name_ntt := name_ntt();
BEGIN
IF ln50a = ln50d THEN
DBMS_OUTPUT.put_line('LN50A and LN50D are equal');
ELSIF ln50a != ln50d THEN
DBMS_OUTPUT.put_line('LN50A and LN50D are not equal');
ELSE
DBMS_OUTPUT.put_line('NULL');
END IF;
SELECT last_name BULK COLLECT INTO ln50d
FROM employees
WHERE department_id = 50
ORDER BY last_name DESC;
SELECT last_name BULK COLLECT INTO ln50a
FROM employees
WHERE department_id = 50
ORDER BY last_name ASC;
SELECT last_name BULK COLLECT INTO ln100
FROM employees
WHERE department_id = 100;
IF ln50a = ln50d THEN
DBMS_OUTPUT.put_line('LN50A and LN50D are equal');
ELSIF ln50a != ln50d THEN
DBMS_OUTPUT.put_line('LN50A and LN50D are not equal');
ELSE
DBMS_OUTPUT.put_line('NULL');
END IF;
IF ln100 = ln50d THEN
DBMS_OUTPUT.put_line('LN100 and LN50D are equal');
ELSIF ln100 != ln50d THEN
DBMS_OUTPUT.put_line('LN100 and LN50D are not equal');
ELSE
DBMS_OUTPUT.put_line('NULL');
END IF;
ln50d.EXTEND;
ln50d(ln50d.LAST) := NULL;
ln50a.EXTEND;
ln50a(ln50a.LAST) := 'X';
IF ln50a = ln50d THEN
DBMS_OUTPUT.put_line('LN50A and LN50D are equal');
ELSIF ln50a != ln50d THEN
DBMS_OUTPUT.put_line('LN50A and LN50D are not equal');
ELSE
DBMS_OUTPUT.put_line('NULL');
END IF;
IF ln100 = ln50d THEN
DBMS_OUTPUT.put_line('LN100 and LN50D are equal');
ELSIF ln100 != ln50d THEN
DBMS_OUTPUT.put_line('LN100 and LN50D are not equal');
ELSE
DBMS_OUTPUT.put_line('NULL');
END IF;
END;
/
12-2: Use set operators with
nested tables
Write an anonymous block to populate three different nested tables of
strings as follows:
1, the last names of all employees in department 50 (I will refer to
this as LN50)
2. the last names of all employees in department 100 (I will refer to
this as LN100)
3. the last names of all employees whose last names contain an "e" (LNE)
Write code to show the contents of the collections that result from the
following operations:
LN50 unioned with LN100
LN50 unioned with LNE, including duplicates
LN50 unioned with LNE, excluding duplicates
LNE minus LN50
LNE minus LN100
LNE intersected with LN100
Solution: for 12-2: Use set operators with nested tables
DECLARE
/*
Write an anonymous block to populate three different nested tables of strings as follows:
1, the last names of all employees in department 50 (I will refer to this as LN50)
2. the last names of all employees in department 100 (I will refer to this as LN100)
3. the last names of all employees whose last names contain an "e" (LNE)
Write code to show the contents of the collections that result from the following operations:
LN50 unioned with LN100
LN50 unioned with LNE, including duplicates
LN50 unioned with LNE, excluding duplicates
LNE minus LN50
LNE minus LN100
*/
TYPE name_ntt IS TABLE OF employees.last_name%TYPE;
-- I initialize these two lists to demonstrate some odd
-- behavior regarding NULL values in these structures.
ln50 name_ntt := name_ntt();
ln100 name_ntt := name_ntt();
lne name_ntt := name_ntt();
PROCEDURE show_name_ntt ( title_in IN VARCHAR2, list_in IN name_ntt )
IS
BEGIN
DBMS_OUTPUT.put_line (title_in || ' has ' || list_in.COUNT || ' elements' );
FOR indx IN 1 .. list_in.COUNT
LOOP
DBMS_OUTPUT.put_line( ' ' || list_in(indx));
END LOOP;
END show_name_ntt;
BEGIN
SELECT last_name BULK COLLECT INTO ln50
FROM employees
WHERE department_id = 50;
SELECT last_name BULK COLLECT INTO ln100
FROM employees
WHERE department_id = 100;
SELECT last_name BULK COLLECT INTO lne
FROM employees
WHERE UPPER ( last_name ) LIKE '%E%';
show_name_ntt('LN50', ln50);
show_name_ntt('LN100', ln100);
show_name_ntt('LNE', lne);
show_name_ntt('LN50 unioned with LN100', lne multiset union ln100);
show_name_ntt('LN50 unioned with LNE with duplicates', lne multiset union lne);
show_name_ntt('LN50 unioned with LNE without duplicates', lne multiset union distinct lne);
show_name_ntt('LNe minus LN50', lne multiset except ln50);
show_name_ntt('LNe minus LN100', lne multiset except ln100);
END;
/
A.
Collection utilities and applications
This section offers exercises to build utilities that help you get
information about or manipulate the contents of exercises.
A-1e: Is the value found in the
collection?
Write a function that you can use to find and return the index in a
collection based on the DBMS_SQL.
VARCHAR2S collection type that contains a value matching the specified
input value, between the given start and end values (default -> all
rows).
Solution: for A-1: Is the value found in the collection?
CREATE OR REPLACE FUNCTION element_found (
-- Replace with your own type here, as needed.
collection_in IN DBMS_SQL.VARCHAR2S
,value_in IN VARCHAR2
,start_index_in IN BINARY_INTEGER DEFAULT NULL
,end_index_in IN BINARY_INTEGER DEFAULT NULL
,nulls_eq_in IN BOOLEAN DEFAULT TRUE
)
RETURN BOOLEAN
/*
Program name: matching_row
Overview:
Returns TRUE if the value is found in the collection between
the specified index values.
Parameters:
collection_in
The collection to be searched for the specified value.
value_in
The value to be checked for in the collection.
start_index_in - starting index for search; default is first index in
collection as returned by the FIRST method.
end_index_in - ending index for search; default is first index in
collection as returned by the FIRST method.
nulls_eq_in - if TRUE, then if you pass NULL for value_in and
an element in the collection is NULL, this function
will return TRUE. That is, NULL = NULL in this case.
Dependencies/Restrictions:
It checks for equality of collection elements with an = operator.
The datatype of the collection must, therefore, support that
syntax. This will be true for scalars, for example, but not
for records, object types, etc.
Exceptions raised:
Modification History:
*/
IS
-- Replace with your own type here, as needed.
/* Set start and end indexes of search. Do not all these values to fall
outside the first and last indexes in the collection. */
l_start BINARY_INTEGER := GREATEST (NVL (start_index_in, collection_in.FIRST),collection_in.FIRST);
l_end BINARY_INTEGER := LEAST (NVL (end_index_in, collection_in.LAST), collection_in.LAST);
--
l_index BINARY_INTEGER := l_start;
l_element_found BOOLEAN DEFAULT FALSE;
BEGIN
-- If value is NULL, then return NULL unless NULL=NULL.
IF value_in IS NULL AND NOT (NVL (nulls_eq_in, FALSE)) THEN
l_element_found := NULL;
-- If nothing in collection, the element cannot be found.
ELSIF l_start IS NULL OR l_end IS NULL THEN
l_element_found := FALSE;
ELSE
-- Scan through the contents of the collection until we are out
-- of elements or we found a match.
WHILE ( l_index IS NOT NULL
AND l_index BETWEEN l_start AND l_end
AND NOT l_element_found)
LOOP
/* Do we have a match?
Note: we do not assign this expression directly to the
Boolean variable, because it might evaluate to NULL, and
we don't want that outcome passed on to l_element_found. */
IF collection_in(l_index) = value_in
OR (nulls_eq_in AND collection_in (l_index) IS NULL AND value_in IS NULL) THEN
l_element_found := TRUE;
END IF;
IF NOT l_element_found THEN
-- Go to the next element.
l_index := collection_in.NEXT (l_index);
END IF;
END LOOP;
END IF;
RETURN l_element_found;
END element_found;
/
A-2: Move sequentially filled
collection to a primary key-indexed collection
Write an anonymous block that uses BULK COLLECT to fill up a local
collection with all the rows from the employees table.
Then move the contents of that collection to two other local
collections: one that is indexed by the primary key of the employees
table, and another that is indexed by the first and last names of the
employee.
Solution: for A-2: Move sequentially filled collection to a primary
key-indexed collection
DECLARE
SUBTYPE full_name_t IS VARCHAR2(32767);
TYPE employee_tt IS TABLE OF employees%ROWTYPE INDEX BY PLS_INTEGER;
TYPE employees_by_name_tt IS TABLE OF employees%ROWTYPE INDEX BY full_name_t;
temp_employee_cache employee_tt;
employee_cache employee_tt;
employee_by_names_cache employees_by_name_tt;
BEGIN
SELECT * BULK COLLECT INTO temp_employee_cache
FROM employees;
FOR indx IN 1 .. temp_employee_cache.FIRST
LOOP
employee_cache(temp_employee_cache(indx).employee_id) := temp_employee_cache(indx);
-- Ensure uniqueness with a delimiter.
employee_by_names_cache(temp_employee_cache(indx).last_name || '^' || temp_employee_cache(indx).first_name) := temp_employee_cache(indx);
END LOOP;
temp_employee_cache.DELETE;
END;
A-3: Remove gaps from a
sparsely-filled collection (make it dense).
Write a procedure will take a collection of type DBMS_SQL.VARCHAR2S and
remove any "gaps" in the defined rows, transforming the collection into
a densely-filled collection.
Nice to have: Allow the user to specify if you want the new starting
row to match the previous starting row (default true), or provide an
alternative starting row value (default of 1) -- this is really only
relevant if you are densify-ing an associative array.
Solution: for A-3: Remove gaps from a sparsely-filled collection
(make it dense).
CREATE OR REPLACE PROCEDURE densify (
collection_inout IN OUT DBMS_SQL.VARCHAR2S
,same_start_in IN BOOLEAN DEFAULT TRUE
,start_at_in IN PLS_INTEGER DEFAULT 1
)
/*
Program name: densify
Overview:
Remove any gaps from the collection (applies only to associative arrays
and nested tables, since varrays cannot have gaps).
Parameters:
collection_inout
The collection from which gaps are to be removed.
same_start_in
TRUE if the densified collection should have the same starting row.
start_at_in
The starting row of the densified collection. Only used if the
same_start_in argument is not set to TRUE.
Dependencies/Restrictions:
Exceptions raised:
Modification History:
*/
IS
l_start PLS_INTEGER;
l_collection DBMS_SQL.VARCHAR2S;
l_row PLS_INTEGER;
BEGIN
-- Determine starting row for compressed collection.
IF same_start_in THEN
l_start := collection_inout.FIRST;
ELSE
l_start := NVL (start_at_in, 1);
END IF;
l_row := collection_inout.FIRST;
-- For each element in the original collection, copy it to the
-- sequentially allocated position in the local version.
WHILE (l_row IS NOT NULL)
LOOP
-- Go to last defined row and add the start position to it.
l_collection (l_start + l_collection.COUNT) := collection_inout (l_row);
l_row := collection_inout.NEXT (l_row);
END LOOP;
-- Now copy the local, dense collection back to the IN OUT argument.
collection_inout := l_collection;
END densify;
/
A-4: Return the COUNT of
elements in a collection between the specified start and end indices.
Write a function to return the number of elements defined in a
collection of type DBMS_SQL.VARCHAR2S between the specified start and
end indices.
The built-in COUNT method for collections returns the number of
elements defined in a collection. In Oracle Database 10g, you can also
use the CARDINALITY operator with nested tables to get the same answer.
What if, however, you need to know the number of elements not of the
entire collection, but of a specific restricted range of elements in
the collection? This function should do the job for you.
Solution: for A-4: Return the COUNT of elements in a collection
between the specified start and end indices.
CREATE OR REPLACE FUNCTION count_between (
-- Replace with your own type here, as needed.
collection_in IN DBMS_SQL.VARCHAR2S
,start_index_in IN BINARY_INTEGER DEFAULT NULL
,end_index_in IN BINARY_INTEGER DEFAULT NULL
,inclusive_in IN BOOLEAN DEFAULT TRUE
)
RETURN PLS_INTEGER
/*
Program name: count_between
Overview:
Return the number of elements defined within the specified index range.
Parameters:
collection_in - the collection to be counted
start_index_in - starting index for count; default is first index in
collection as returned by the FIRST method.
end_index_in - ending index for count; default is first index in
collection as returned by the FIRST method.
inclusive_in - pass TRUE if you want the endpoints to be counted
in the result (only if the elements exist).
Dependencies/Restrictions:
Exceptions raised:
Modification History:
*/
IS
l_start BINARY_INTEGER := start_index_in;
l_end BINARY_INTEGER := end_index_in;
l_index BINARY_INTEGER;
l_count PLS_INTEGER DEFAULT 0;
BEGIN
IF start_index_in IS NULL THEN
l_start := collection_in.FIRST;
END IF;
IF end_index_in IS NULL THEN
l_end := collection_in.LAST;
END IF;
l_index := l_start;
WHILE (l_index <= l_end)
LOOP
IF l_index = l_end AND NOT inclusive_in THEN
NULL;
ELSIF l_index = l_start AND NOT inclusive_in THEN
NULL;
ELSIF collection_in.EXISTS(l_index) THEN
l_count := l_count + 1;
END IF;
l_index := collection_in.NEXT(l_index);
END LOOP;
RETURN l_count;
END count_between;
/