O'Reilly Network    
 Published on O'Reilly Network (http://www.oreillynet.com/)
 http://www.oreillynet.com/pub/a/network/2003/04/22/feuerstein.html
 See this if you're having trouble printing code examples


Oracle PL/SQL Programming, 3rd Edition

Using PL/SQL Records in SQL Statements

by Steven Feuerstein, coauthor of Oracle PL/SQL Programming, 3rd Edition and Bryn Llewellyn
04/22/2003

Editor's note: In Oracle 9i Release 2 Developments for PL/SQL Collections, Steven Feuerstein and Bryn Llewellyn demonstrated how to use multi-level collections. In this final installment of their series on new Oracle 9i features, the pair shows how, with Oracle 9i Release 2, you can now use the PL/SQL RECORD datatype inside SQL statements to employ records in UPDATE, INSERT, DELETE, and SELECT statements. This capability, while available for many years, was limited in its usefulness because it wasn't possible to use records inside SQL statements.

The PL/SQL RECORD Datatype

A PL/SQL RECORD is a composite datatype. In contrast to a scalar datatype like NUMBER, a record is composed of multiple pieces of information, called fields. Records can be declared using relational tables or explicit cursors as "templates" with the %ROWTYPE declaration attribute. You can also declare records based on TYPEs that you define yourself. Records are very handy constructs for PL/SQL developers.

The easiest way to define a record is by using the %ROWTYPE syntax in your declaration. For example, the following statement:

DECLARE
   bestseller books%ROWTYPE;

creates a record that has a structure corresponding to the books table; for every column in the table, there's a field in the record with the same name and datatype as the column. The %ROWTYPE keyword is especially valuable because the declaration is guaranteed to match the corresponding schema-level template and is immune to schema-level changes in definition of the shape of the table. If we change the structure of the books table, all we have to do is recompile the preceding code and bestseller will take on the new structure of that table.

Asecond way to declare a record is to define your own RECORD TYPE. One advantage of a user-defined TYPE is that you can take advantage of native PL/SQL datatypes as well as derived values in the field list, as shown here:

DECLARE
   TYPE extra_book_info_t 
      IS RECORD (
      title books.title%TYPE,
      is_bestseller BOOLEAN,
      reviewed_by names_list
   );
   first_book extra_book_info_t;

Notice that the preceding user-defined record datatype includes a field ("title") that's based on the column definition of a database table, a field ("is_bestseller") based on a scalar data type (PL/SQL Boolean flag), and a collection (list of names of people who reviewed Oracle PL/SQL Programming, 3rd Edition.

Next, we can declare a record based on this type (you don't use %ROWTYPE in this case, because you're already referencing a type to perform the declaration). Once you've declared a record, you can then manipulate the data in these fields (or the record as a whole) as you can see here:

DECLARE
   bestseller books%ROWTYPE;
   required_reading books%ROWTYPE;
BEGIN
   -- Modify a field value
   bestseller.title := 
     'ORACLE PL/SQL PROGRAMMING';

   -- Copy one record to another
   required_reading :=
      bestseller;
END;

Note that in the preceding code we've used the structure of the books table to define our PL/SQL records, but the assignment to the title field didn't in any way affect data inside that table. You should also be aware that while you can assign one record to another, you couldn't perform comparisons or computations on records. Neither of these statements will compile:

BEGIN
   IF bestseller = 
      required_reading
   THEN ...

BEGIN
   left_to_read :=
      bestseller -
      required_reading;

You can also pass records as arguments to procedures and functions. This technique allows you to shrink down the size of a parameter list (pass a single record instead of a lengthy and cumbersome list of individual values). And if you're using %ROWTYPE to declare the argument, the "shape" of the record (numbers and types of fields) will adjust automatically with changes to the underlying cursor or table. Here's an example of a function with a record in the parameter list:

CREATE OR REPLACE PROCEDURE
   calculate_royalties (
      book_in IN books%ROWTYPE,
      quarter_end_in IN DATE
   )
IS ...

Prior to Oracle 9i Release 2, it was only possible to use a record in conjunction with a SQL statement in one way: on the receiving end of a SELECT INTO or FETCH INTO statement. For example:

DECLARE
   bestseller books%ROWTYPE;
BEGIN
   SELECT * 
     INTO bestseller
     FROM books
    WHERE title = 
     'ORACLE PL/SQL PROGRAMMING';
END;

Related Reading

Oracle PL/SQL Programming

Oracle PL/SQL Programming
By Steven Feuerstein

Table of Contents
Index
Sample Chapter

Read Online--Safari Search this book on Safari:
 

Code Fragments only

This is very convenient syntax, but it unfortunately just leaves us all hungry for the full range of record-smart SQL, most importantly the ability to perform INSERT and UPDATE operations with a record (as opposed to having to "break out" all the individual fields of that record). In summary, before Oracle 9i Release 2, records offered significant advantages for developers, but also left us frustrated because of the limitations on their usage. Oracle 9i Release 2 goes a long way in relieving (but not completely curing us of) our frustrations.

Oracle 9i Release 2 Record Improvements

In response to developer requests, Oracle has now made it possible for us to do any of the following with static SQL (such as, SQL statements that are fully specified at the time your code is compiled):

Some restrictions do remain at Version 9.2.0 for records in SQL, including:

Also In This Series

Oracle 9i Release 2 Developments for PL/SQL Collections

HTTP Communication from Within the Oracle Database

Multi-Level Collections in Oracle 9i

Table Functions and Cursor Expressions

Native Compilation, CASE, and Dynamic Bulk Binding

But why dwell on the negative? Let's explore this great new functionality with a series of examples, all of which will rely on the employees table, defined in the hr schema that's installed in the seed database. The script to create this schema is demo/schema/human_resources/hr_cre.sql under the Oracle Home directory.

The samples also rely on common features such as an index-by-*_integer table, records of employees%rowtype and a procedure to show the rows of such a table. These are implemented in the Emp_Utl package.

SELECT with RECORD Bind

As we noted earlier, while it was possible before 9.2.0 to SELECT INTO a record, you couldn't BULK SELECT INTO a collection of records. The resulting code was often very tedious to write and not as efficient as would be desired. Suppose, for example, that we'd like to retrieve all employees hired before June 25, 1997, and then give them all big, fat raises. A very straightforward way to write the logic for this is shown in Example 1.

Example 1. Give raises to employees using single row fetches.

DECLARE
   v_emprec    employees%ROWTYPE;
   v_emprecs   emp_util.emprec_tab_t;

   CURSOR cur
   IS
      SELECT *
        FROM employees
       WHERE hire_date < TO_DATE(
          '25-JUN-1997', 'DD-MON-YYYY');

   i BINARY_INTEGER := 0;
BEGIN
   OPEN cur;

   LOOP
      FETCH cur INTO v_emprec;
      EXIT WHEN cur%NOTFOUND OR cur%ROWCOUNT > 10;
      i := i + 1;
      v_emprecs (i) := v_emprec;
   END LOOP;

   emp_util.give_raise (v_emprecs);
END;

There's no problem understanding this logic, but depending on the quantity of data involved, this could be a very inefficient implementation. We'd really love to take advantage of the recent (Oracle 8i) addition of the BULK COLLECT syntax (allowing us to fetch multiple rows with a single pass to the database); we might see an order of magnitude improvement.

To use BULK COLLECT with records prior to Oracle 9i Release 2, however, we'd need to select each element in the select list into its own collection; this technique is shown in Example 2. The complete code for this block may be seen in bulkcollect8i.sql and is more than 80 lines long! It's approaching what is feasible to maintain, and feels especially uncomfortable because of the artificial requirement to compromise the natural modeling approach by slicing the desired table of records vertically into N tables of scalars.

Example 2. BULK COLLECT into separate collections.

DECLARE
   TYPE employee_ids_t IS 
      TABLE OF employees.employee_id%TYPE
      INDEX BY BINARY_INTEGER;
   ...
   v_employee_ids   employee_ids_t;
   ...
   v_emprecs emp_util.emprec_tab_t;

   CURSOR cur
   IS
      SELECT employee_id,
           ...
        FROM employees
       WHERE hire_date >= TO_DATE(
          '25-JUN-1997', 'DD-MON-YYYY');

BEGIN
   OPEN cur;
   FETCH cur BULK COLLECT 
    INTO v_employee_ids,
       ...
    LIMIT 10;
   CLOSE cur;

   FOR j IN 1 .. v_employee_ids.LAST
   LOOP
      v_emprecs (j).employee_id := 
       v_employee_ids (j);
      ...
   END LOOP;

   emp_util.give_raise (v_emprecs);
END;

Note: The clause limit 10 is equivalent to where rownum <= 10.

With Oracle 9i Release 2, our program becomes much shorter, intuitive, and maintainable. What you see here is all we need to write to take advantage of BULK COLLECT to populate a single associative array of records:

DECLARE
   v_emprecs   
     emp_util.emprec_tab_t;

   CURSOR cur
   IS
      SELECT *
        FROM employees
       WHERE hire_date < '25-JUN-97';
BEGIN
   OPEN cur;
   FETCH cur BULK COLLECT 
    INTO v_emprecs LIMIT 10;
   CLOSE cur;
   emp_util.give_raise (v_emprecs);
END;

Note: Once again, the clause limit 10 is equivalent to where rownum <= 10.

Even more wonderful, we can now combine BULK COLLECT fetches into records with native dynamic SQL. Here's an example, in which we give raises to employees for a specific schema:

CREATE OR REPLACE PROCEDURE
   give_raise (schema_in IN VARCHAR2)
IS 
   v_emprecs   
     emp_util.emprec_tab_t;

   cur SYS_REFCURSOR; 
BEGIN
   OPEN cur FOR 
     'SELECT * FROM ' ||
     schema_in || '.employees' ||
     'WHERE hire_date < :date_limit'
     USING '25-JUN-97';

   FETCH cur BULK COLLECT 
    INTO v_emprecs LIMIT 10;

   CLOSE cur;
   emp_util.give_raise (
      schema_in, v_emprecs);
END;

SYS_REFCURSOR is a pre-defined weak REF CURSOR type that was added to the PL/SQL language in Oracle 9i Release 1.

INSERT with RECORD Bind

PL/SQL developers are demanding, no doubt about that. Even though Oracle can add all sorts of cool, new functionality into PL/SQL, we'll still find something missing, something else we so dearly need. For years, one of our favorite "wish-we-had's" was the ability to insert a row into a table using a record. Prior to Oracle 9i Release 2, if we had put our data into a record, it would then be necessary to "explode" the record into its individual fields when performing the insert, as in:

DECLARE
   v_emprec employees%ROWTYPE   
      := emp_util.get_one_row;
BEGIN
   INSERT INTO employees_retired (
     employee_id, 
     last_name, 
     ...)
   VALUES (
     v_emprec.employee_id, 
     v_emprec.last_name,
     ...);
END;

This is very cumbersome coding; it certainly is something we would have liked to avoid. In Oracle 9i Release 2, we can now take advantage of simple, intuitive, and compact syntax to bind an entire record to a row in an insert. This is shown here:

DECLARE
  v_emprec employees%rowtype 
    := Emp_Util.Get_One_Row;
BEGIN
  INSERT INTO employees_retired
    VALUES v_emprec;
END;

Notice that we don't put the record inside parentheses. You are, unfortunately, not able to use this technique with Native Dynamic SQL. You can, on the other hand, insert using a record in the highly efficient FORALL statement. This technique is valuable when you're inserting a large number of rows.

Take a look at the example in Example 3. Table 1 explains the interesting parts of the retire_them_now procedure (written and run at a low-tech company that never went public nor saw its value crash, enabling them to now offer early, paid retirement to everyone over 40 years of age!).

Example 3. Bulk INSERTing with a record.

 1  CREATE OR REPLACE PROCEDURE retire_them_now
 2  IS
 3     bulk_errors   EXCEPTION;
 4     PRAGMA EXCEPTION_INIT (bulk_errors, -24381);
 5     TYPE employees_t IS TABLE OF employees%ROWTYPE
 6        INDEX BY PLS_INTEGER;
 7     retirees      employees_t;
 8  BEGIN
 9     FOR rec IN (SELECT *
10                   FROM employees
11                  WHERE hire_date < ADD_MONTHS (SYSDATE, -1 * 18 * 40))
12     LOOP
13        retirees (SQL%ROWCOUNT) := rec;
14     END LOOP;
15     FORALL indx IN retirees.FIRST .. retirees.LAST
16        SAVE EXCEPTIONS
17        INSERT INTO employees
18          VALUES retirees (indx);
19  EXCEPTION
20     WHEN bulk_errors
21     THEN
22        FOR j IN 1 .. SQL%BULK_EXCEPTIONS.COUNT
23        LOOP
24           DBMS_OUTPUT.PUT_LINE ( 'Error from element #' ||
25              TO_CHAR(SQL%BULK_EXCEPTIONS(j).error_index) || ': ' ||
26              SQLERRM(SQL%BULK_EXCEPTIONS(j).error_code));
27        END LOOP;
28* END;

Table 1. Description of the retire_them_now procedure.


Line(s)

Description

3-4

Declare an exception, enabling us to trap by name an error that occurs during the bulk insert.

5-7

Declare an associative array, each row of which contains a record having the same structure as the employees table.

9-14

Load up the array with the information for all employees who are over 40 years of age.

15-18

The turbo-charged insert mechanism, FORALL, that includes a clause to allow FORALL to continue past errors and references a record (the specified row in the array).

20-26

Typical code you'd write to trap any error that was raised during the bulk insert and display or deal with each error individually.

Prior to Oracle 9i Release 2, you could use the FORALL syntax, but it would have been necessary to create and populate a separate collection for each column, and then reference individual columns and collections in the INSERT statement.

UPDATE SET ROW with RECORD Bind

Oracle 9i Release 2 now gives you an easy and powerful way to update an entire row in a table from a record: the SET ROW clause. The ROW keyword is functionally equivalent to *. It's most useful when the source of the row is one table and the target is a different table with the same column specification-for example, in a scenario where rows in an application table are updated once or many times and may eventually be deleted, and where the latest state of each row (including when it's been deleted) must be reflected in an audit table. (Ideally we'd use MERGE with a RECORD bind, but this isn't supported yet.)

The new syntax for the Static SQL, single row case is obvious and compact:

DECLARE
   v_emprec employees%ROWTYPE   
     := emp_util.get_one_row;
BEGIN
   v_emprec.salary 
     := v_emprec.salary * 1.2;

   UPDATE employees_2
      SET ROW = v_emprec
    WHERE employee_id = 
          v_emprec.employee_id;
END;

Prior to Oracle 9i Release 2, this same functionality would require listing the columns explicitly, as shown in Example 4.

Example 4. Pre-Oracle9i Release 2 update of entire row.

DECLARE
   v_emprec   employees%ROWTYPE   := emp_util.get_one_row;
BEGIN
   v_emprec.salary := v_emprec.salary * 1.2;

   UPDATE employees
      SET first_name = v_emprec.first_name,
          last_name = v_emprec.last_name,
          email = v_emprec.email,
          phone_number = v_emprec.phone_number,
          hire_date = v_emprec.hire_date,
          job_id = v_emprec.job_id,
          salary = v_emprec.salary,
          commission_pct = v_emprec.commission_pct,
          manager_id = v_emprec.manager_id,
          department_id = v_emprec.department_id
    WHERE employee_id = v_emprec.employee_id;
END;

Now, it would certainly be nice to be able to use the SET ROW syntax in a FORALL statement, as follows:

DECLARE
   v_emprecs emp_util.emprec_tab_t 
      := emp_util.get_many_rows;
BEGIN
   -- This will not work, due to: 
   -- PLS-00436: 
   --   implementation restriction: 
   --   cannot reference fields of 
   --   BULK In-BIND table of records.
   FORALL j IN 
      v_emprecs.FIRST .. v_emprecs.LAST
      UPDATE employees
         SET ROW = v_emprecs (j)
       WHERE employee_id = 
          v_emprecs (j).employee_id;
END;

Sadly, this code fails to compile with the error: "PLS-00436: implementation restriction: cannot reference fields of BULK In-BIND table of records." Instead, we must write:

DECLARE
   v_emprecs emp_util.emprec_tab_t 
      := emp_util.get_many_rows;

   TYPE employee_id_tab_t IS 
      TABLE OF employees.employee_id%TYPE
      INDEX BY PLS_INTEGER;

   v_employee_ids   employee_id_tab_t;
BEGIN
   -- Transfer just the IDs into their own
   -- collection for use in the WHERE clause
   -- of the UPDATE statement.
   FOR j IN v_emprecs.FIRST .. v_emprecs.LAST
   LOOP
      v_employee_ids (j) :=
       v_emprecs (j).employee_id;
   END LOOP;

   FORALL j IN 
      v_emprecs.FIRST .. v_emprecs.LAST
      UPDATE employees
         SET ROW = v_emprecs (j)
       WHERE employee_id = v_employee_ids (j);
END;

DELETE and UPDATE with RETURNING with RECORD Bind

You can also take advantage of rows when using the RETURNING clause in both DELETEs and UPDATEs. The RETURNING clause allows you to retrieve and return information that's processed in the DML statement without using a separate, subsequent query. Record-based functionality for RETURNING means that you can return multiple pieces of information into a record, rather than individual variables. An example of this feature for DELETEs is shown in Example 5.

Example 5. RETURNING into a record from a DELETE statement.

DECLARE
   v_emprec   employees%ROWTYPE;
BEGIN
   DELETE FROM employees
         WHERE employee_id = 100
     RETURNING employee_id, first_name, last_name, email, phone_number,
               hire_date, job_id, salary, commission_pct, manager_id,
               department_id
          INTO v_emprec;

   emp_util.show_one (v_emprec);
END;

You can also retrieve less than a full row of information by relying on programmer-defined record types, as this next example shows:

DECLARE
   TYPE key_info_rt IS RECORD (
      id   NUMBER,
      nm   VARCHAR2 (100)
   );

   v_emprec key_info_rt;
BEGIN
   DELETE FROM employees
         WHERE employee_id = 100
     RETURNING employee_id, first_name
          INTO v_emprec;
   ...
END;

You must still list the individual columns or derived values in the RETURNING clause, making the integration a bit less than ideal (for example, Oracle could and perhaps will some day allow us to write RETURNING ROW INTO v_emprec). Nevertheless, this is a significant improvement over Version 9.0.1, where a RECORD could not be used as the target for INTO, requiring us to provide a long list of individual variables to hold the values returned from the DML statement.

Next, suppose that we execute a DELETE or UPDATE that modifies more than one row. In this case, we can use the RETURNING clause to obtain information from each of the individual rows modified by using BULK COLLECT to populate a collection of records! This technique is shown in Example 6.

Example 6. RETURNING multiple rows of information from an UPDATE statement.

DECLARE
   v_emprecs   emp_util.emprec_tab_t;
BEGIN
   UPDATE    employees
         SET salary = salary * 1.1
       WHERE hire_date < = '25-JUN-97'
   RETURNING employee_id, first_name, last_name, email, phone_number,
             hire_date, job_id, salary, commission_pct, manager_id,
             department_id
        BULK COLLECT INTO v_emprecs;

   emp_util.show_all (v_emprecs);
END;

Again, this is a significant improvement over Version 9.0.1, in which you would have to declare a separate collection for each value specified in the RETURNING clause, and then populate each separately. A fragment of this approach is shown in Example 7:

Example 7. RETURNING multiple rows of data from an UPDATE statement in Version 9.0.1.

DECLARE
   TYPE employee_ids_t IS TABLE OF employees.employee_id%TYPE
      INDEX BY BINARY_INTEGER;
   ...
   v_employee_ids   employee_ids_t;
   ...
   v_emprecs        emp_util.emprec_tab_t;
BEGIN
   UPDATE    employees
         SET salary = salary * 1.1
       WHERE hire_date < = '25-JUN-97'
   RETURNING employee_id, first_name, last_name, email,
             phone_number, hire_date, job_id, salary,
             commission_pct, manager_id, department_id
       BULK COLLECT INTO v_employee_ids, v_first_names, v_last_names, v_emails,
                         v_phone_numbers, v_hire_dates, v_job_ids, v_salarys,
                         v_commission_pcts, v_manager_ids, v_department_ids;

   FOR j IN 1 .. v_employee_ids.LAST
   LOOP
      v_emprecs (j).employee_id := v_employee_ids (j);
      ...
   END LOOP;

   emp_util.show_all (v_emprecs);
END;

Performance Impact of Record Binding

There's no doubt that using records in DML statements results in greatly reduced code volume and therefore increased productivity. Is there, however, a penalty to be paid in runtime execution of this leaner code? Our tests (see Table 2) show for the most part that there's no measurable difference between field and record-based operations.

Table 2. Scripts to examine performance impact of record binding.


Script name

What is tested?

insrec1.tst

Insert with record for table with sequence-generated primary key.

insrec2.tst

Insert with record on table with non-sequence primary key.

insrec3.tst

Insert with record on table with many columns with non-sequence primary key.

insrec4.tst

Bulk insert with record on table with non-sequence primary key.

Depending on extenuating circumstances, however, you can see more of a differential.

For example, any one of the following situations could impact negatively on record-based DML processing time:

Record-based DML was added to the PL/SQL language primarily as a "usability" feature, rather than one related to performance. Part of the challenge of integrating new features into your "box of tricks" is that you need to know when not to use them. In general, if you're already working with and populating records (particularly if you're transferring data from one table to another using records), you'll find this feature to be a wonderful enhancement.

Records: The Way to Go

Records have always been a very powerful programming construct for PL/SQL developers. Use of records reduces code volume and also increases the resiliency of one's code, since a record defined using %ROWTYPE automatically (upon recompilation of the program) adapts to the current structure of the base cursor or table.

The inability to utilize records within SQL DML statements in a PL/SQL program has long been a frustration to developers. With Oracle 9i Release2, another barrier between SQL and PL/SQL has been removed, allowing for ever-smoother programming efforts, higher productivity, and more easily maintained applications.

This article was originally published in the July 2002 issue of Oracle Professional. The material in Feuerstein's articles--and those he cowrote with Bryn Llewellyn--are based on Oracle Corporation white papers originally prepared by Llewellyn for Oracle OpenWorld 2001 in San Francisco and OracleWorld Copenhagen in June 2002, and Feuerstein's book, Oracle PL/SQL Programming, 3rd Edition.

Steven Feuerstein is considered one of the world's leading experts on the Oracle PL/SQL language.

Bryn Llewellyn is PL/SQL Product Manager, Database and Application Server Technologies Development Group, at Oracle Corporation Headquarters.


O'Reilly & Associates recently released (September 2002) Oracle PL/SQL Programming, 3rd Edition.


Return to the O'Reilly Network.

Copyright © 2007 O'Reilly Media, Inc.