使用记录和游标

此页上的脚本内容仅用于导航目的,不会以任何方式变更该内容。

可以将数据值存储在记录中,并使用光标作为指向结果集和相关处理信息的指针。

另请参见: Oracle Database PL/SQL Language Reference(了解有关记录的详细信息)

关于记录

记录是 PL/SQL 组合变量,可以存储不同类型的数据值。可以将内部组件(字段)视为标量变量。可以将整个记录作为子程序参数进行传递。记录非常适用于存储表行中的数据或表行中某些列的数据。

记录是一种 PL/SQL 复合变量,它可以存储不同类型的数据值,类似于 C、C++ 或 Java 中的结构类型。记录的内部组件被称为字段。要访问记录字段,可以使用点表示法:record_name.field_name。

您可以将记录字段视为标量变量。还可以将整个记录作为子程序参数传递。

记录非常适用于存储表行中的数据或表行中某些列的数据。每个记录字段对应于一个表列。

有三种方式可以创建记录:

另请参见:

教程:声明 RECORD 类型

以下步骤介绍如何使用 SQL Developer 的“编辑”工具声明一个 RECORD 类型 sal_info,其字段可用于保存员工的薪水信息(工作 ID、该工作 ID 的最低薪水和当前薪水以及建议加薪)。

声明 RECORD 类型 sal_info 的步骤:

  1. 在 "Connections"(连接)框架中,展开 hr_conn

    在 hr_conn 图标下,即会出现方案对象类型列表。

  2. 展开程序包

    即会出现程序包列表。

  3. 右键单击 EMP_EVAL

    此时将显示选项的列表。

  4. 选择编辑

    此时将打开 EMP_EVAL 窗格,其中显示创建该软件包的 CREATE PACKAGE 语句:

    CREATE OR REPLACE PACKAGE EMP_EVAL AS
       
    PROCEDURE eval_department(dept_id IN NUMBER);
    FUNCTION calculate_score(evaluation_id IN NUMBER
                           , performance_id IN NUMBER)
                             RETURN NUMBER;
       
    END EMP_EVAL;
    
  5. 在 EMP_EVAL 窗格,紧靠 END EMP_EVAL 之前添加如下代码:

     TYPE sal_info IS RECORD
       ( j_id     jobs.job_id%type
       , sal_min  jobs.min_salary%type
       , sal_max  jobs.max_salary%type
       , sal      employees.salary%type
       , sal_raise NUMBER(3,3) );
    

    该窗格的标题字体为斜体,这表示尚未将更改保存在数据库中。

  6. 选择编译图标。

    随即会编译更改后的程序包规范,并将其保存到数据库中。此时 EMP_EVAL 窗格的标题字体将不再是斜体。

    现在您可以声明记录类型 sal_info,如“教程:创建和调用带有记录参数的子程序”中所述。

教程:创建和调用带有记录参数的子程序

以下步骤说明如何使用 SQL Developer 工具“编辑”来创建和调用具有记录类型为 sal_info 的参数的子程序。

记录类型 sal_info 在“教程:声明 RECORD 类型”中创建。

本教程说明如何使用 SQL Developer 工具“编辑”来完成以下任务:

由于 EVAL_FREQUENCY 将调用 SALARY_SCHEDULE,因此 SALARY_SCHEDULE 声明必须位于 EVAL_FREQUENCY 声明之前(否则程序包将无法编译)。但是,您可以在程序包体中的任何位置定义 SALARY_SCHEDULE。

创建 SALARY_SCHEDULE 并更改 EVAL_FREQUENCY 的步骤:

  1. 在 "Connections"(连接)框架中,展开 hr_conn

  2. 在方案对象类型的列表中,展开程序包

  3. 在软件包列表中,展开 EMP_EVAL

  4. 在选项列表中,右键单击 EMP_EVAL Body

  5. 在选项列表中,选择编辑

    即会出现“EMP_EVAL 主体”窗格,其中显示用于该程序包体的代码。

  6. 在“EMP_EVAL 主体”窗格中的,紧靠 END EMP_EVAL 之前添加以下 SALARY_SCHEDULE 过程定义:

     PROCEDURE salary_schedule (emp IN sal_info) AS
       accumulating_sal  NUMBER;
     BEGIN
       DBMS_OUTPUT.PUT_LINE('If salary ' || emp.sal ||
         ' increases by ' || ROUND((emp.sal_raise * 100),0) ||
    
         '% each year, it will be:');
    
       accumulating_sal := emp.sal;
    
       WHILE accumulating_sal <= emp.sal_max LOOP
         accumulating_sal := accumulating_sal * (1 + emp.sal_raise);
         DBMS_OUTPUT.PUT_LINE(ROUND(accumulating_sal,2) ||', ');
    
       END LOOP;
     END salary_schedule;
    

    该窗格的标题字体为斜体,这表示尚未将更改保存在数据库中。

  7. 在 EMP_EVAL Body 窗格中,在以下位置输入 eval_frequency 函数以及 salary_schedule 和 add_eval 过程:

     CREATE OR REPLACE
     PACKAGE BODY EMP_EVAL AS
    
     FUNCTION eval_frequency (emp_id EMPLOYEES.EMPLOYEE_ID%TYPE)
    
       RETURN PLS_INTEGER;
    
     PROCEDURE salary_schedule(emp IN sal_info);
    
     PROCEDURE add_eval(employee_id IN employees.employee_id%type, today IN DATE);
    
     PROCEDURE eval_department (dept_id IN NUMBER) AS
    
  8. 编辑 EVAL_FREQUENCY 函数,进行以下更改:

     FUNCTION eval_frequency (emp_id EMPLOYEES.EMPLOYEE_ID%TYPE)
       RETURN PLS_INTEGER
     AS
       h_date     EMPLOYEES.HIRE_DATE%TYPE;
       today      EMPLOYEES.HIRE_DATE%TYPE;
       eval_freq  PLS_INTEGER;
    
       emp_sal    SAL_INFO;  -- replaces sal, sal_raise, and sal_max
    
     BEGIN
       SELECT SYSDATE INTO today FROM DUAL;
    
       SELECT HIRE_DATE INTO h_date
       FROM EMPLOYEES
       WHERE EMPLOYEE_ID = eval_frequency.emp_id;
    
       IF ((h_date + (INTERVAL '120' MONTH)) < today) THEN
         eval_freq := 1;
    
         /* populate emp_sal */
    
         SELECT j.JOB_ID, j.MIN_SALARY, j.MAX_SALARY, e.SALARY
    
         INTO emp_sal.j_id, emp_sal.sal_min, emp_sal.sal_max, emp_sal.sal
    
         FROM EMPLOYEES e, JOBS j
    
         WHERE e.EMPLOYEE_ID = eval_frequency.emp_id
    
         AND j.JOB_ID = eval_frequency.emp_id;
    
         emp_sal.sal_raise := 0;  -- default
    
         CASE emp_sal.j_id
           WHEN 'PU_CLERK' THEN emp_sal.sal_raise := 0.08;
           WHEN 'SH_CLERK' THEN emp_sal.sal_raise := 0.07;
           WHEN 'ST_CLERK' THEN emp_sal.sal_raise := 0.06;
           WHEN 'HR_REP' THEN emp_sal.sal_raise := 0.05;
           WHEN 'PR_REP' THEN emp_sal.sal_raise := 0.05;
           WHEN 'MK_REP' THEN emp_sal.sal_raise := 0.04;
           ELSE NULL;
         END CASE;
    
         IF (emp_sal.sal_raise != 0) THEN
    
           salary_schedule(emp_sal);
         END IF;
       ELSE
         eval_freq := 2;
       END IF;
    
       RETURN eval_freq;
     END eval_frequency;
    
  9. 选择编译

关于游标

Oracle Database 运行 SQL 语句时,它将结果集和处理信息存储在未命名的专用 SQL 区域中。指向此未命名区域的指针称为游标,使用游标可以从结果集中一次检索一行。游标属性可返回有关游标状态的信息。

每次运行 SQL DML 语句或 PL/SQL SELECT INTO 语句时,PL/SQL 都会打开一个隐式游标。您可以从游标属性中获取有关此游标的信息,但无法控制此信息。在语句运行之后,数据库会关闭游标;但是,其属性值仍将保持可用,直至另一个 DML 或 SELECT INTO 语句运行。

还可以使用 PL/SQL 声明游标。显式游标具有名称并与一个查询(SQL SELECT 语句)关联,而且这种搜索通常可以返回多行。声明光标后,必须以隐式或显式方式对其进行处理。要隐式处理游标,请使用游标 FOR LOOP。语法如下:

FOR record_name IN cursor_name LOOP
  statement
  [ statement ]...
END LOOP;

要显式处理游标,请将其打开(使用 OPEN 语句),从结果集中一次提取一行或批量提取(使用 FETCH 语句),然后关闭游标(使用 CLOSE 语句)。关闭游标后,您将无法从结果集中提取记录,也无法查看游标属性值。

隐式游标属性值的语法是 SQL%attribute(例如 SQL%FOUND)。SQL%attribute 始终引用最近运行的 DML 或 SELECT INTO 语句。

声明的游标属性值的语法是 cursor_name%attribute(例如 c1%FOUND)。表 1 列出了各种游标属性以及它们可以返回的值。(隐式游标还具有其他属性,本文不再赘述。)

表 1 游标属性值

属性 声明光标的值 隐式游标的值
已找到 %FOUND

如果游标已打开但尚未尝试进行提取(脚注 1),则为 NULL

如果最近的提取返回了一行,则为 TRUE

如果最近的提取未返回行,则为 FALSE

如果未运行任何 DML 或 SELECT INTO 语句,则为 NULL

如果最近的 DML 或 SELECT INTO 语句返回了一行,则为 TRUE

如果最近的 DML 或 SELECT INTO 语句未返回行,则为 FALSE

% 未找到

如果游标已打开但尚未尝试进行提取(脚注 1),则为 NULL

如果最近的提取返回了一行,则为 FALSE

如果最近的提取未返回行,则为 TRUE

如果未运行任何 DML 或 SELECT INTO 语句,则为 NULL

如果最近的 DML 或 SELECT INTO 语句返回了一行,则为 FALSE

如果最近的 DML 或 SELECT INTO 语句未返回行,则为 TRUE

%ROWCOUNT 如果游标已打开(脚注 1),则为大于或等于零的数字。 如果未运行任何 DML 或 SELECT INTO 语句,则为 NULL;否则,则为一个大于或等于零的数字。
打开 %S 如果游标已打开,则为 TRUE;否则为 FALSE。 始终为假。

脚注 1:如果光标未打开,则该属性会引发预定义的异常错误 INVALID_CURSOR。

另请参见:

使用声明的游标从结果集中一次检索一行

可以使用声明的游标检索一次一行的结果集行。

下列过程以最简单的形式使用每个必要的语句,但提供了有关其完整语法的参考。

使用声明的游标检索结果集行的步骤(一次一个):

  1. 在声明部分中:

    1. 声明游标:

       CURSOR cursor_name IS query;
      

      有关已申报的完整游标声明语法,请参阅 Oracle Database PL/SQL Language Reference

    2. 声明一个用于保存游标返回的行的记录:

       record_name cursor_name%ROWTYPE;
      

      有关 %ROWTYPE 的完整语法,请参阅 Oracle Database PL/SQL Language Reference

  2. 在可执行部分中:

    1. 打开游标:

       OPEN cursor_name;
      

      有关 OPEN 语句的完整语法,请参阅 Oracle Database PL/SQL Language Reference

    2. 使用 LOOP 语句从游标(结果集行)中一次提取一行,其语法与以下代码类似:

       LOOP
         FETCH cursor_name INTO record_name;
         EXIT WHEN cursor_name%NOTFOUND;
      
         -- Process row that is in record_name:
      
         statement;
         [ statement; ]...
       END LOOP;
      

      有关 FETCH 语句的完整语法,请参阅 Oracle Database PL/SQL Language Reference

    3. 关闭游标:

       CLOSE cursor_name;
      

有关完整的 CLOSE 语句语法,请参阅 Oracle Database PL/SQL Language Reference

教程:使用声明的游标从结果集中一次检索一行

以下步骤说明如何实施 EMP_EVAL.EVAL_DEPARTMENT 过程,该过程使用声明的游标 emp_cursor。

实施 EMP_EVAL.EVAL_DEPARTMENT 过程的步骤:

  1. 在 EMP_EVAL 程序包规范中,按照所示更改 EVAL_DEPARTMENT 过程的声明:

     PROCEDURE eval_department(dept_id IN employees.department_id%TYPE);
    
  2. 在 EMP_EVAL 程序包体中,更改 EVAL_DEPARTMENT 过程的定义,如以下示例所示:

     PROCEDURE eval_department (dept_id IN employees.department_id%TYPE)
     AS
    
       CURSOR emp_cursor IS
    
         SELECT * FROM EMPLOYEES
    
         WHERE DEPARTMENT_ID = eval_department.dept_id;
    
       emp_record  EMPLOYEES%ROWTYPE;  -- for row returned by cursor
    
       all_evals   BOOLEAN;  -- true if all employees in dept need evaluations
    
       today       DATE;
    
     BEGIN
    
       today := SYSDATE;
    
       IF (EXTRACT(MONTH FROM today) < 6) THEN
    
         all_evals := FALSE; -- only new employees need evaluations
    
       ELSE
    
         all_evals := TRUE;  -- all employees need evaluations
    
       END IF;
    
       OPEN emp_cursor;
    
       DBMS_OUTPUT.PUT_LINE (
    
         'Determining evaluations necessary in department # ' ||
    
         dept_id );
    
       LOOP
    
         FETCH emp_cursor INTO emp_record;
    
         EXIT WHEN emp_cursor%NOTFOUND;
    
         IF all_evals THEN
    
           add_eval(emp_record.employee_id, today);
    
         ELSIF (eval_frequency(emp_record.employee_id) = 2) THEN
    
           add_eval(emp_record.employee_id, today);
    
         END IF;
    
       END LOOP;
    
       DBMS_OUTPUT.PUT_LINE('Processed ' || emp_cursor%ROWCOUNT || ' records.');
    
       CLOSE emp_cursor;
     END eval_department;
    

    (有关更改程序包体的分步示例,请参阅“教程:在子程序中声明变量和常量”。)

  3. 编译 EMP_EVAL 程序包规范。

  4. 编译 EMP_EVAL 程序包体。

关于游标变量

游标变量类似于游标,但它不局限于一个查询。您可以为一个查询打开一个游标变量,处理结果集,然后将该游标变量用于其他查询。当子程序之间传递查询结果时,游标变量将非常有用。

有关游标的信息,请参见“关于游标”。

要声明游标变量,您需要声明一个 REF CURSOR 类型,然后声明一个该类型的变量(因此,游标变量通常称为 REF CURSOR )。REF CURSOR 类型可以是强类型或弱。

强 REF CURSOR 类型指定了返回类型,它是其游标变量的 RECORD 类型。对于返回行不属于该返回类型行的查询,PL/SQL 编译器不允许使用这些强类型游标变量。强 REF CURSOR 类型比弱类型为更加不易出错,但弱类型为更加灵活。

弱 REF CURSOR 类型不指定返回类型。PL/SQL 编译器接受任何查询中的弱类型游标变量。弱 REF CURSOR 类型是可交换类型;因此,您可以使用预定义类型的弱游标类型 SYS_REFCURSOR,而不用创建弱 REF CURSOR 类型。

声明游标变量后,您必须为特定查询打开它(使用 OPEN FOR 语句),从结果集中一次提取一行(使用 FETCH 语句),然后关闭游标(使用 CLOSE 语句)或是为另一个特定查询打开它(使用 OPEN FOR 语句)。为另一个查询打开游标变量时,将会关闭前一个查询的游标变量。在关闭特定查询的游标变量后,您既不能从该查询的结果集中提取记录,也不能查看该查询的游标属性值。

另请参见:

使用游标变量从结果集中一次检索一行

可以使用游标变量从结果集中一次检索一行。

下列过程以最简单的形式使用每个必要的语句,但提供了有关其完整语法的参考。

使用游标变量从结果集中一次检索一行的步骤:

  1. 在声明部分中:

    1. 声明 REF CURSOR 类型:

       TYPE cursor_type IS REF CURSOR [ RETURN return_type ];
      

      有关完整的 REF CURSOR 类型声明语法,请参阅 Oracle Database PL/SQL Language Reference

    2. 声明一个该类型的游标变量:

       cursor_variable cursor_type;
      

      有关游标变量声明的完整语法,请参阅 Oracle Database PL/SQL Language Reference

    3. 声明一个用于保存游标返回的行的记录:

       record_name return_type;
      

      有关记录声明语法完整信息,请参阅 Oracle Database PL/SQL Language Reference

  2. 在可执行部分中:

    1. 为特定查询打开游标变量:

       OPEN cursor_variable FOR query;
      

      有关 OPEN FOR 语句语法的完整信息,请参阅 Oracle Database PL/SQL Language Reference

    2. 使用 LOOP 语句从游标变量(结果集行)中一次提取一行,其语法如下:

       LOOP
         FETCH cursor_variable INTO record_name;
         EXIT WHEN cursor_variable%NOTFOUND;
      
         -- Process row that is in record_name:
      
         statement;
         [ statement; ]...
       END LOOP;
      

      有关 FETCH 语句语法的完整信息,请参阅 Oracle Database PL/SQL Language Reference

    3. 关闭游标变量:

       CLOSE cursor_variable;
      

      此外,您也可以为另一个查询打开游标变量,这将对当前查询关闭游标变量。

      有关 CLOSE 语句语法的完整信息,请参阅 Oracle Database PL/SQL Language Reference

教程:使用游标变量从结果集中一次检索一行

以下步骤说明如何更改 EMP_EVAL.EVAL_DEPARTMENT 过程,使其使用游标变量而不是声明的游标(可让其处理多个部门),以及如何提高 EMP_EVAL.EVAL_DEPARTMENT 和 EMP_EVAL.ADD_EVAL 的效率。

本教程如何提高 EMP_EVAL.EVAL_DEPARTMENT 和 EMP_EVAL.ADD_EVAL 的效率:EVAL_DEPARTMENT 不是将记录的一个字段传递给 ADD_EVAL 并让 ADD_EVAL 使用三个查询来提取相同记录的其他三个字段,而是将整个记录传递给 ADD_EVAL,然后让 ADD_EVAL 使用点表示法访问其他三个字段的值。

更改 EMP_EVAL.EVAL_DEPARTMENT 过程以使用游标变量的步骤:

  1. 在 EMP_EVAL 程序包规范中,添加过程声明和 REF CURSOR 类型定义,如以下示例所示:

     CREATE OR REPLACE
     PACKAGE emp_eval AS
    
       PROCEDURE eval_department (dept_id IN employees.department_id%TYPE);
    
       PROCEDURE eval_everyone;
    
       FUNCTION calculate_score(eval_id IN scores.evaluation_id%TYPE
                             , perf_id IN scores.performance_id%TYPE)
                               RETURN NUMBER;
       TYPE SAL_INFO IS RECORD
           ( j_id jobs.job_id%type
           , sal_min jobs.min_salary%type
           , sal_max jobs.max_salary%type
           , salary employees.salary%type
           , sal_raise NUMBER(3,3));
    
       TYPE emp_refcursor_type IS REF CURSOR RETURN employees%ROWTYPE;
     END emp_eval;
    
  2. 在 EMP_EVAL 程序包体中,为过程 EVAL_LOOP_CONTROL 添加一个前向声明,并更改过程 ADD_EVAL 的声明,如以下所示:

     CREATE OR REPLACE
     PACKAGE BODY EMP_EVAL AS
    
       FUNCTION eval_frequency (emp_id IN EMPLOYEES.EMPLOYEE_ID%TYPE)
         RETURN PLS_INTEGER;
    
       PROCEDURE salary_schedule(emp IN sal_info);
    
       PROCEDURE add_eval(emp_record IN EMPLOYEES%ROWTYPE, today IN DATE);
    
       PROCEDURE eval_loop_control(emp_cursor IN emp_refcursor_type);
     ...
    

    (有关更改程序包体的分步示例,请参阅“教程:在子程序中声明变量和常量”。)

  3. 将 EVAL_DEPARTMENT 过程更改为基于部门检索三个单独的结果集,并调用 EVAL_LOOP_CONTROL 过程,如以下示例所示:

     PROCEDURE eval_department(dept_id IN employees.department_id%TYPE) AS
    
       emp_cursor    emp_refcursor_type;
    
       current_dept  departments.department_id%TYPE;
    
     BEGIN
    
       current_dept := dept_id;
    
       FOR loop_c IN 1..3 LOOP
    
         OPEN emp_cursor FOR
    
           SELECT *
    
           FROM employees
    
           WHERE current_dept = eval_department.dept_id;
    
         DBMS_OUTPUT.PUT_LINE
           ('Determining necessary evaluations in department #' ||
    
           current_dept);
    
         eval_loop_control(emp_cursor);
    
         DBMS_OUTPUT.PUT_LINE
           ('Processed ' || emp_cursor%ROWCOUNT || ' records.');
    
         CLOSE emp_cursor;
    
         current_dept := current_dept + 10;
    
       END LOOP;
     END eval_department;
    
  4. 按如下所示更改 ADD_EVAL 过程:

     PROCEDURE add_eval(emp_record IN employees%ROWTYPE, today IN DATE)
     AS
    
     -- (Delete local variables)
     BEGIN
       INSERT INTO EVALUATIONS (
         evaluation_id,
         employee_id,
         evaluation_date,
         job_id,
         manager_id,
         department_id,
         total_score
       )
       VALUES (
         evaluations_sequence.NEXTVAL,   -- evaluation_id
    
         emp_record.employee_id,    -- employee_id
         today,                     -- evaluation_date
    
         emp_record.job_id,         -- job_id
    
         emp_record.manager_id,     -- manager_id
    
         emp_record.department_id,  -- department_id
         0                           -- total_score
     );
     END add_eval;
    
  5. END EMP_EVAL 之前,添加以下过程,该过程中将从结果集中提取各个记录并处理它们:

     PROCEDURE eval_loop_control (emp_cursor IN emp_refcursor_type) AS
       emp_record      EMPLOYEES%ROWTYPE;
       all_evals       BOOLEAN;
       today           DATE;
     BEGIN
       today := SYSDATE;
    
       IF (EXTRACT(MONTH FROM today) < 6) THEN
    
         all_evals := FALSE;
       ELSE
         all_evals := TRUE;
       END IF;
    
       LOOP
         FETCH emp_cursor INTO emp_record;
         EXIT WHEN emp_cursor%NOTFOUND;
    
         IF all_evals THEN
           add_eval(emp_record, today);
         ELSIF (eval_frequency(emp_record.employee_id) = 2) THEN
    
           add_eval(emp_record, today);
         END IF;
       END LOOP;
     END eval_loop_control;
    
  6. END EMP_EVAL 之前,添加以下过程,该过程会检索包含公司所有员工的结果集:

     PROCEDURE eval_everyone AS
       emp_cursor emp_refcursor_type;
     BEGIN
       OPEN emp_cursor FOR SELECT * FROM employees;
       DBMS_OUTPUT.PUT_LINE('Determining number of necessary evaluations.');
       eval_loop_control(emp_cursor);
       DBMS_OUTPUT.PUT_LINE('Processed ' || emp_cursor%ROWCOUNT || ' records.');
       CLOSE emp_cursor;
     END eval_everyone;
    
  7. 编译 EMP_EVAL 程序包规范。

  8. 编译 EMP_EVAL 程序包体。