41.6. 基本语句

41.6.1. 赋值
41.6.2. 在pl/sql中执行DML
41.6.3. 执行一个单行结果的查询
41.6.4. 执行动态命令
41.6.5. 完全不做任何操作

在本节和接下来的几节中,我们描述了所有由 pl/sql 显式理解的语句类型。任何不被识别为这些语句类型之一的内容都被认为是 SQL 命令,并被发送到主数据库引擎执行。

41.6.1. 赋值

一个给pl/sql变量赋值的语句写作如下:

variable := expression;

如之前所述,这条语句中的表达式通过发送SQL SELECT命令到主数据库引擎进行评估。表达式必须产生一个单一的值(如果变量是行或记录变量,则可能是一行值)。目标变量可以是一个简单的变量(可选地带有块名称),行或记录变量的字段,或者是一个简单变量或字段的数组元素。

示例:

tax := subtotal * 0.06;
my_record.user_id := 20;

41.6.2. 在pl/sql中执行DML

pl/sql在匿名块、函数、存储过程,包中支持DML, DML包括 INSERT, UPDATE, and DELETE 语句。如果需要使用 SELECT 语句, 必须加上into子句,否则必然报错。 例子如下:

create table dml_table(id int, name varchar2(100), job varchar2(100), age int);
insert into dml_table values (1, 'asda', 'gfdgd', 12);
insert into dml_table values (2, 'sdfsd', 'cvxvx', 14);
insert into dml_table values (3, 'uyiy', 'mmbv', 16);

create or replace function support_dml return int as
begin
update dml_table set id = 4 where name = 'uyiy';
return 12;
end;
/
select support_dml() from dual;

declare
retcode int := 1;
begin
retcode := support_dml();
dbms_output.put_line(retcode);
end;
/

41.6.3. 执行一个单行结果的查询

产生单行结果(可能是多列)的SQL命令的结果可以赋值给一个记录变量、行类型变量或标量变量列表。这可以通过编写基本SQL命令并添加一个INTO子句来完成。例如:

SELECT select_expressions INTO target FROM ...;
INSERT ... RETURNING expressions INTO target;
UPDATE ... RETURNING expressions INTO target;
DELETE ... RETURNING expressions INTO target;

其中target可以是一个记录变量,一个行变量,或者是一个逗号分隔的简单变量和记录/行字段的列表。 pl/sql变量将被替换到查询的剩余部分中,并且计划被缓存,就像上面描述的那样,对于不返回行的命令。 这适用于带有RETURNINGSELECTINSERT/UPDATE/DELETE以及返回行集结果的实用命令(例如EXPLAIN)。 除了INTO子句之外,SQL命令与在pl/sql外部编写的命令相同。

Tip

注意,这种带有INTOSELECT的解释与LightDB的常规SELECT INTO命令截然不同,其中INTO目标是一个新创建的表。如果你想从pl/sql函数内部的SELECT结果创建一个表,请使用语法CREATE TABLE ... AS SELECT

如果行或变量列表被用作目标,则查询的结果列必须与目标的结构在数量和数据类型上完全匹配,否则将发生运行时错误。当记录变量是目标时,它会自动将自己配置为查询结果列的行类型。

INTO子句可以出现在SQL命令的几乎任何位置。通常,它要么在SELECT命令中的select_expressions列表之前或之后,要么在其他命令类型的命令的末尾。建议你遵循这个惯例,以防pl/sql解析器在未来版本中变得更加严格。

如果INTO子句中未指定STRICT,则target将被设置为查询返回的第一行,如果查询未返回任何行,则设置为nulls。(请注意,第一行未定义,除非你使用了ORDER BY。)第一行后的任何结果行都将被丢弃。 您可以检查特殊的SQL%FOUND变量来确定是否返回了一行:

DROP TABLE t1;
DROP SEQUENCE t1_seq;

CREATE TABLE t1 (
  id NUMBER(10),
  description VARCHAR2(50),
  CONSTRAINT t1_pk PRIMARY KEY (id)
);

CREATE SEQUENCE t1_seq;

INSERT INTO t1 VALUES (t1_seq.nextval, 'ONE');
INSERT INTO t1 VALUES (t1_seq.nextval, 'TWO');
INSERT INTO t1 VALUES (t1_seq.nextval, 'THREE');

RETURNING INTO子句允许我们返回受DML语句影响的行的列值。返回的数据可以是单列。例如,当我们使用序列插入数据来生成我们的主键值时,我们可以如下返回主键值:

DECLARE
  l_id t1.id%TYPE;
BEGIN
  INSERT INTO t1 VALUES (t1_seq.nextval, 'FOUR')
  RETURNING id INTO l_id;
  COMMIT;

  DBMS_OUTPUT.put_line('ID=' || l_id);
END;
/

例如,语法也适用于更新和删除语句。
DECLARE
  l_id t1.id%TYPE;
BEGIN
  UPDATE t1
  SET    description = description
  WHERE  description = 'FOUR'
  RETURNING id INTO l_id;

  DBMS_OUTPUT.put_line('UPDATE ID=' || l_id);

  DELETE FROM t1
  WHERE  description = 'FOUR'
  RETURNING id INTO l_id;

  DBMS_OUTPUT.put_line('DELETE ID=' || l_id);

  COMMIT;
END;
/

41.6.4. 执行动态命令

通常情况下,你会想在pl/sql函数内部生成动态命令,即每次执行时都涉及不同的表或不同的数据类型。 pl/sql的正常尝试缓存命令计划在这种情况下无法工作。为了处理这种问题,提供了EXECUTE IMMEDIATE语句:

EXECUTE IMMEDIATE command-string [ [ BULK COLLECT ] INTO target ] [ USING expression [, ... ] ];

其中command-string是一个表达式,产生一个包含要执行的命令的字符串(类型为varchar)。可选的target是一个记录变量、行变量或由简单变量和记录/行字段组成的逗号分隔列表,用于存储命令的结果。可选的USING表达式提供要插入命令的值。

在计算的命令字符串上不执行pl/sql变量的替换。任何必需的变量值必须在构建命令字符串时插入;或者你可以使用下面描述的参数。

此外,通过EXECUTE IMMEDIATE执行的命令没有计划缓存。相反,每次运行语句时,命令都会被计划。因此,命令字符串可以在函数内部动态创建,以执行对不同表和列的操作。

INTO子句指定SQL命令返回的行应分配到哪里。如果提供了行或变量列表,则它必须与查询结果的结构完全匹配(当使用记录变量时,它将自动配置自身以匹配结果结构)。如果返回多行,则仅将第一行分配给INTO变量。如果没有返回行,则将NULL分配给INTO变量。如果没有指定INTO子句,则丢弃查询结果。

BULK COLLECT INTO子句指定SQL命令返回的行应分配到哪里。如果返回多行,则将全部分配给BULK COLLECT INTO变量。如果没有返回行,则将NULL分配给BULK COLLECT INTO变量。如果没有指定BULK COLLECT INTO子句,则丢弃查询结果。 例如:

declare	
  type array_number is table of number;
  type array_varchar is table of varchar2(255);
 
  a_employee_id    array_number := array_number();
  a_first_name     array_varchar := array_varchar();
 	
BEGIN
  
  dbms_output.put_line('----------multiple nested table---------');
  execute IMMEDIATE 'WITH employeetemp AS (
      SELECT employee_id AS eid, first_name AS fname FROM employees
    )
    SELECT eid, fname FROM employeetemp' bulk collect into a_employee_id, a_first_name ;
 
  for i in a_employee_id.first .. a_employee_id.last loop
 		dbms_output.put_line(a_employee_id(i)); 	
  end loop;
  
  for i in a_first_name.first .. a_first_name.last loop
 		dbms_output.put_line(a_first_name(i)); 	
  end loop; 
  
end;
/

如果提供了STRICT选项,除非查询产生恰好一行,否则将报告错误。

命令字符串可以使用参数值,在命令中引用为:inserted_by:inserted等。这些符号指的是在USING子句中提供的值。这种方法通常比将数据值作为varchar插入命令字符串更可取:它避免了将值转换为varchar并返回的运行时开销,并且由于不需要引用或转义,它更不容易受到SQL注入攻击的影响。例如:

EXECUTE IMMEDIATE 'SELECT count(*) FROM mytable WHERE inserted_by = :inserted_by AND inserted <= :inserted'
   INTO c
   USING checked_user, checked_date;

简单常量命令字符串和一些USING参数的EXECUTE IMMEDIATE(如上面的第一个示例)在功能上等同于直接在pl/sql中编写命令,并允许自动替换pl/sql变量。重要的区别在于,EXECUTE IMMEDIATE将在每次执行时重新计划命令,生成一个特定于当前参数值的计划;而pl/sql可能会创建一个通用计划并缓存它以供重用。在最佳计划强烈依赖于参数值的情况下,使用EXECUTE IMMEDIATE可以帮助确保不选择通用计划。

EXECUTE IMMEDIATE中目前不支持SELECT INTO;相反,执行一个简单的SELECT命令,并将INTO作为EXECUTE IMMEDIATE本身的一部分指定。

using 节点所使用的变量前面可以添加变量类型 inoutin 输入的变量类型表示不可以被更改, out 输入的变量类型表示可以被更改。 例如:

CREATE OR REPLACE PROCEDURE TargetProcedure(  
    p_input1 IN VARCHAR2,  
    p_input2 IN NUMBER,  
    p_output1 OUT VARCHAR2,  
    p_output2 OUT NUMBER  
) AS  
BEGIN   
    DBMS_Output.PUT_LINE('TargetProcedure begin-> p_input1 '||' is: '||p_input1);
    DBMS_Output.PUT_LINE('TargetProcedure begin-> p_input2 '||' is: '||p_input2);
    DBMS_Output.PUT_LINE('TargetProcedure begin-> p_output1 '||' is: '||p_output1);
    DBMS_Output.PUT_LINE('TargetProcedure begin-> p_output2 '||' is: '||p_output2);
    p_output1 := 'Response from TargetProcedure: ' || p_input1;  
    p_output2 := p_input2 * 10;  
    DBMS_Output.PUT_LINE('TargetProcedure end-> p_input1 '||' is: '||p_input1);
    DBMS_Output.PUT_LINE('TargetProcedure end-> p_input2 '||' is: '||p_input2);
    DBMS_Output.PUT_LINE('TargetProcedure end-> p_output1 '||' is: '||p_output1);
    DBMS_Output.PUT_LINE('TargetProcedure end-> p_output2 '||' is: '||p_output2);
END TargetProcedure;  
/
CREATE OR REPLACE PROCEDURE MainProcedure(  
    main_input1 IN VARCHAR2,  
    main_input2 IN NUMBER,  
    main_output1 OUT VARCHAR2,  
    main_output2 OUT NUMBER  
) AS  
BEGIN  
    main_output2 := 0;
    main_output1  := 'Operation successful';
    -- Use EXECUTE IMMEDIATE to execute dynamic SQL and pass parameters  
    DBMS_Output.PUT_LINE('MainProcedure begin-> main_input1 '||' is: '||main_input1);
    DBMS_Output.PUT_LINE('MainProcedure begin-> main_input2 '||' is: '||main_input2);
    DBMS_Output.PUT_LINE('MainProcedure begin-> main_output1 '||' is: '||main_output1);
    DBMS_Output.PUT_LINE('MainProcedure begin-> main_output2 '||' is: '||main_output2);
    EXECUTE IMMEDIATE 'BEGIN TargetProcedure(:p_input1, :p_input2, :p_output1, :p_output2); END;'  
    USING IN main_input1, IN main_input2, OUT main_output1, OUT main_output2;  
    DBMS_Output.PUT_LINE('MainProcedure end-> main_input1 '||' is: '||main_input1);
    DBMS_Output.PUT_LINE('MainProcedure end-> main_input2 '||' is: '||main_input2);
    DBMS_Output.PUT_LINE('MainProcedure end-> main_output1 '||' is: '||main_output1);
    DBMS_Output.PUT_LINE('MainProcedure end-> main_output2 '||' is: '||main_output2);
END MainProcedure;  
/
call MainProcedure('xiaoming',123,'xiaohong',234);

41.6.5. 完全不做任何操作

有时候,一个不做任何事情的占位符语句是有用的。例如,它可以表示if/then/else语句中的一个分支是故意空的。为此,使用NULL语句:

NULL;

例如,以下两个代码片段是等效的:

declare
y int := 10;
x int := 10;
BEGIN
  NULL;
END;
/