结构化查询语言(SQL)是第四代编程语言的典型,这种命令式的语言更像一种指令,使用它,你只需要告诉计算机“做什么”,而不用告诉计算机“怎么做”。第四代编程语言普遍具有简单、易学、能更快的投入生产等优点,但也失去了部分第三代编程语言(C,C++,Java等)的灵活性。PL/SQL 在 SQL 的基础上,保留了部分第三代编程语言特性,它主要运行在 Oracle 数据库上,它同时兼备了第四代语言简单、易用的特点,又保留了高级程序设计语言的灵活性与完整性,这使得开发人员可以只使用 PL/SQL 就能进行复杂业务逻辑的编写。

一  PL/SQL 简介

  1,简介

  标准 SQL 提供了定义和操纵数据库对象的能力,但与传统高级编程语言相比,由于其具有更高的抽象性,所以注定缺乏诸多高级编程语言的特性,比如封装函数、流程控制、进行错误检测和处理等。

  PL/SQL 是 Oracle 在标准 SQL 的基础上进行功能扩充后的一门编程语言,这使它保留了部分第三代编程语言的部分特性,比如变量声明、流程控制、错误处理等。

  PL/SQL 的全称是 Procedural Language/SQL,即过程化结构查询语言,正如其名所示,PL/SQL 增加了过程性语言中的结构,以对标准 SQL 进行扩充。在PL/SQL 中,最基本的程序单元是语句块(block),所有的程序都应该由各种块构成,块与块之间可以相互嵌套。在块中,可以定义变量,执行条件判断,循环等。

  2,开发工具

  Oracle 官方提供了两款开发工具:SQL*Plus 和 Oracle SQL Developer。前者是一款命令行开发工具,后者则拥有方便的图形化操作界面(类似SQL Server 的 SSMS)。

  除了官方提供的两款工具外,PL/SQL Develpoer 是一款由第三方公司开发的,非常流行的 Oracle 数据库集成开发环境。除此之外,市面上还有很多其他工具也具备 Oracle 数据库开发的能力,大家可以根据需要选择合适的开发工具。

二  PL/SQL基础

  1,SQL 与 PL/SQL

  前面提到,PL/SQL 是对标准 SQL 的扩展,所以,在 PL/SQL 中不仅可以执行 SQL 语句,还支持很多增强的特性,比如在 SQL 语句中使用变量、使用 PL/SQL 定义的函数等。在 PL/SQL 语句块中,可以使用 SQL 语句操作数据库,它支持所有的 SQL 数据操作、游标和事务处理命令,支持所有的 SQL 函数、操作符,完全支持 SQL 数据类型。

  需要注意的是:在 PL/SQL 语句块中,不能直接使用 DDL 语句,这是因为 PL/SQL 引擎在编译时会检测语句块中所涉及的对象,如果其不存在,通常都会引发错误,导致 DDL 语句执行失败。

  为了解决这类绑定性错误,可以使用动态SQL,即把需要执行的 DDL 操作存储在字符串中,并通过 execute immediate 来执行这个字符串,从而达到间接执行 DDL 操作的目的。

  

  2,数据定义

  数据管理主要使用 DDL 数据定义语言:create、alter、drop。

  创建表和约束:

 --在列后添加约束
create table table_name
(
col1 type constraint,
...
)
--单独添加约束
create table table_name
(
col1 type,
....,
constration cons_name cons_type
)
--在Oracle中创建表和约束与标准SQL相同

  创建索引和视图:

 --创建索引(非唯一)
--默认系统会在具有unique和primary key的列上创建唯一约束
create index index_name on (col1...);
--当提供多个列时,即创建复合索引
--创建视图
create or replace view view_name
as
select ...;
--创建,如果已存在则修改视图
create view ...
as
...
with read only;
--创建只读的视图(推荐)

  修改表或视图:

 --为表增加新的列
alter table table_name
add col_name type constration;
--移除表中已有的列
alter table table_name
drop column col_name;

  删除数据库对象:

 --删除表
drop table table_name;
--删除视图
drop view view_name;
...

  

  3,数据查询

  A:标准查询

    Oracle 中的数据查询遵循 SQL 标准,常规查询请移步我的《SQL入门,就这么简单》

  B:dual 表

    dual 是 Oracle 系统中对所有用户可用的一个实际存在的表,它不能用来存储信息,在实际开发中只能用来执行 SELECT 语句,我们可以用它来获取系统信息,比如获取当前系统日期,或输出一些测试信息。

 --获取系统日期
select sysdate from dual;
--转换日期格式
select to_char(sysdate,'yyyy-mm-dd');
...

  C: 伪列

    常用的伪列有两个:rownum、rowid。

    在 Oracle 中没有类似 SQL Server 中 TOP 这样可以提取结果集前几条记录的关键字,但 Oracle 提供了一个更方便的方法,rownum 伪列。rownum 是一个动态的序号,从 1 开始,为所有查询到的数据编号。

 --查询员工表中前10位员工相关信息
select rownum,ename,sal from emp
where rownum<=10;
-- 测试数据库 Oracle 11g

    使用 rownum 伪列时需要注意:rownum 是在基础查询之后动态添加上去的序号,所以,如果你想通过一条查询语句实现提取结果集中间的部分记录是不能成功的,必须使用子查询,把 rownum 当做普通列才能实现。

 select row_num,empno,ename,sal from (
select rownum as row_num,empno,ename,sal from emp
)a
where row_num >5 and row_num <=10;
-- 别名是为了防止服务器把外层的rownum再次当做伪列

    同理,提取使用 order by 排序后的记录,也需要使用子查询。

    和 rownum 不同,rowid 伪列是和表中的数据一样实际存在的列,它是一种数据类型,是基于 64 位编码的 18 个字符,用来唯一的表示一条记录物理位置的一个id。我们可以通过 rowidtochar 函数把它转换成字符串进行显示,还可以通过它来删除表中重复的记录。

 --查看rowid
select rowidtochar(rowid) ename,sal from emp;
--基于rowid删除表中形同的记录
delete from emp
where rowid not in (
select min(rowid) from emp group by empno
);

  

  4,数据操纵

  数据操纵主要包含以下操作:insert、update、delete、merge。

  A:insert 插入

 --方式一
insert into table_name(column list)--如果不提供字段列表,下面的值列表需要提供每个字段的值,即使可以为空或有默认值
values
(value list),
(value list),
....
--方式二
insert into table_name
select ...
--从其他查询获取数据,并插入表,数据必须符合表的约束

  B:update 更新

 --方式一
update table_name
set col=newValue
where ...--如果不提供过滤条件,则更新表中所有的列
--方式二
update table_name
set (column list)=
(select ...)
--通过子查询更新表,如果只更新一列,则可以省略column list 的括号,需要注意子查询的字段顺序需要和更新的字段顺序一致

  C:delete

 --方式一
delete from table_name
where ...--如果不提供过滤条件,则会删除所有记录

  

  5,序列

  Oracle 中没有 SQL Server 中 identity() 标识函数,也没有 MySQL 中 auto_increnent 这样的选项来实现自增的列。但 Oracle 提供了更有用的“序列”。类似一个封装好的函数,每次执行会返回一个按指定步长增长或减小的数字。常用来为表设置自增的主键。

 create sequence seq_name
increment by n --自增的步长,(省略该选项则)默认为1,负数表示递减
start with n --序列的初始值,默认为1
max value n | nomaxvalue --指定最大值或没有最大值(无限增长)
min value n | nominvalue --指定最小值或没有最小值(无限减小)
cycle | nocycle --规定设置的序列到达最大或最小时是否从开头循环
cache n | nocache --规定是否在内存中缓存序列值,以改善性能

  通常情况下,我们只需要指定初始值,最大值和循环三项,即可创建一个序列。

 create sequence my_seq
start with 1
nomaxvalue
nocycle;

  序列也是 Oracle 数据库对象之一,序列有两个常用的属性:nextval、currval。

 select my_seq.nextval from dual;--获取下一个序列值
select my_seq.currval from dual;--查看当前序列值
--在插入数据是使用序列
insert into table_name
values
(my_seq.nextval,...)
--使用循环批量插入时非常方便

  我们可以为每个表创建单独的序列,从而为每个表提供没有间隙(无删除数据或回滚等操作干扰)的自增字段作为主键。

  修改和删除序列:

 alter sequence seq_name
...
--为了保证主键的变化有相同的规律可循,一般不建议修改已创建的序列
drop sequence seq_name

三  Oracle 内置函数

  1,字符串函数

 --把二进制转换成字符
select CHR(0101) from dual;
--连接字符串
select concat(111,'aaa') from dual;
select 111 || 'aaa' from dual;
--首字母大写
select INITCAP('char') from dual;
--全大/小写转换
select lower('ABC'),upper('abc') from dual;
--左/右填充
select lpad('aa',5,'*'),rpad('aa',5,'*') from dual;
--删除字符串左/右指定字符(第二个参数中包含的字符都会被删除)
select ltrim('aaa123aaa','1a'),rtrim('aa123aa','a') from dual;
--删除左右空格
select trim(' aaa ') from dual;
--从左边开始删除指定字符(单个),可选参数还包括:trailing(从右边开始),both(两边一起)
select trim(leading 'a' from 'aa123aa') from dual;
--从指定位置开始截取指定长度的字符串
select substr('abcdefg',2,3) from dual;
--字符替换(第二个参数中包含的字符都会被替换)
select translate('11aa22aa11', 'a2', 'bb') from dual;
--替换 NULL 值
select nvl(NULL,'aha') from dual;

  

  2,数学函数

 --绝对值
select abs(-123) from dual;
--向上取整
select ceil(1.2),ceil(-1.2) from dual;
--向下取整
select floor(1.8),floor(-1.8) from dual;
--返回自然常数 e 的 n 次方
select exp(5) from dual;
--返回以第一个参数为底的第二个参数的对数
select log(3,10) from dual;
--求模,如果第二个参数为0,则返回第一个参数
select mod(10,3) from dual;
--返回第一个参数的第二个参数次方
select power(2,3) from dual;
--保留指定小数位,最后一位小数四舍五入得来
select round(1.2345,3) from dual;
--保留指定小数位,其余直接截断
select trunc(1.2345,3) from dual; --格式化数字(格式位数应该与数字位数相同) --用0格式化时,如果数字位数不够,结果会用0补齐位数
select to_char(123456789000,'000,000,000,000,000') from dual;
--用9格式化时,如果数字位数不够,结果会用空格补齐位数
select to_char(123456789000,'999,999,999,999,999') from dual;
--使用fm格式化小数
select to_char(123456.258,'fm999,999,999.99') from dual;
--使用 $(美元) 或 L(当地) 添加货币符号
select to_char(123.456,'L999.999') from dual;
/* 注意货币符号和小数不能一起使用 */

  3,时间和日期函数

 --返回操作系统日期
select sysdate from dual;
--返回日期部分
select current_date from dual;
--返回日期+时间
select current_timestamp from dual;
--返回操作系统日期—+时间(包含时区信息)
select systimestamp from dual;
--按格式化日期为字符串
select to_char(sysdate,'YYYY-MM-DD HH:MM:SS') from dual;
--把字符串表示的日期转换成日期类型的值返回(前后格式需保持一致)
select to_date('2020-05-28 17:02:00','YYYY-MM-DD HH24:MI:SS') from dual;
--把字符串表示的日期转换成日期 + 时间类型的值返回(前后格式需保持一致)
select to_timestamp('2020-05-28 17:02:00','YYYY-MM-DD HH24:MI:SS') from dual;
--返回指定日期后几个月的日期
select add_months(sysdate,1) from dual;
--返回两个日期间间隔月数(注意正负)
select months_between(sysdate,to_date('2020-07-01','YYYY-MM-DD')) from dual;
--把日期按指定精度截断,可选参数有yyyy(精确到年,返回当年的第一天的日期),mm(精确到月,返回当月第一天的日期),rr(精确到日,返回当天的日期)
select trunc(sysdate,'mm') from dual; /* ----------------------日期可选格式--------------------- */
TO_CHAR(sysdate, 'DD-MON-YYYY HH24:MI:SS')
TO_CHAR(sysdate, 'DD-MON-YYYY HH12:MI:SS PM')
TO_CHAR(systimestamp, 'DD-MON-YYYY HH24:MI:SS.FF')
TO_CHAR(sysdate, 'DY, DD-MON-YYYY')
TO_CHAR(sysdate,'Month DDth, YYYY')
TO_CHAR(systimestamp, 'DD-MON-YYYY HH24:MI:SS TZH:TZM')
TO_CHAR(sysdate, 'MM/DD/YYYY HH24:MI:SS')
TO_CHAR(sysdate, 'MM/DD/YY HH24:MI:SS')
TO_CHAR(sysdate, 'MM/DD/RRRR HH12:MI:SS PM')
TO_CHAR(sysdate, 'MM/DD/RR HH12:MI:SS PM')

  4,聚合函数

 --计算行数(不计算空值)
select count(*) from emp;--根据所有列计算
select count(comm) from emp;--根据某一列计算(注意该列是否有空值)
select count(distinct deptno) from emp;--计算deptno中不同值的个数
--计算列的最大/小值
select max(sal),min(sal) from emp;
--返回中间值
select median(sal) from emp;
--返回标准差
select stddev(sal) from emp;
--求和
select sum(sal) from emp;
--计算方差
select variance(sal) from emp;
--伪列 rownum,每条数据的序号
select rownum,empno,ename,sal from emp;

四  变量和类型

  1,PL/SQL 基础

  如果想通过 PL/SQL 程序输出内容,需要先执行以下命令,以打开输出功能,否则即使 PL/SQL 程序正常执行,也不会有任何信息输出。

 set serveroutput on--可以不需要语句结束标记';',这是开发工具的命令
dbms_output.enable;--这是 Pl/SQL 提供的

  PL/SQL 程序由不同的 block(程序块)组成,块是 PL/SQL 程序的基本组成单位,块又可以分为匿名块和命名块。

  一个完整的 PL/SQL 程序一般包含 3 部分:declare(声明),execution code(执行代码,即业务逻辑代码),exception(异常处理),声明和异常处理不是必须的。

 declare
--... 包括变量、游标等
begin
--... 业务代码
exception
--... 异常处理
end;

  让我们来看一个最简单的 PL/SQL 程序:

 --注意,PL/SQL业务代码必须运行在 begin...end 中
begin
dbms_output.put_line('hello world');
end;
--没有声明和异常部分

  块与块之间可以相互嵌套,PL/SQL 中程序块可以限制变量的作用域(变量的作用域问题稍后的章节将会详细讲解),另外,使用<<name>>为块命名可以让整个程序可读性更好:

 <<outer>>--oracle 11g 不允许给最外层块命名
begin
dbms_output.put_line('outer block');
<<inner>>
begin
dbms_output.put_line('inner block');
end;
end;

  

  2,变量

  PL/SQL 中的变量在 declare 区域声明,不需要额外的标识符,只需要提供变量名和值类型即可。

 declare
v_name emp.ename%type;--通过动态获取表中列的数据类型,来确定变量的数据类型
v_job varchar(50);--直接指定具体的数据类型
begin
v_name:='&name';--通过:=为变量赋值
end;

  &name,这种形式是 SQL Developer 工具提供的一种变量形式:替换变量,在执行程序时,你可以手动指定变量的值,提升程序的交互性,测试程序时非常有用。需要注意的是,它并不是 PL/SQL 提供的功能,当使用 & 标识变量时,每次执行该程序都需要提供值,如果使用 && 标识,则只需要在第一次执行时提供,后续执行都默认为第一次提供的值。

  给变量赋值除了通过 := 的方式,还可以使用 select...into 的方式,直接从查询中获取值并赋给变量。

 declare
v_job emp.job%type;
begin
select job into v_job from emp where ename=v_name;--通过select...into 为变量赋值
dbms_output.put_line(v_job);--输出变量值
end;

  

  3,记录类型

  当有多个逻辑相关的变量需要声明时,我们可以使用记录类型来封装他们,封装好这个东西就是记录类型(record)。

 declare
type emp_record is record(--这里相当于定义了一种新的数据类型,类型名称是emp_record,和varcahr,int等类型一样
v_name emp.ename%type,
v_job emp.job%type,
v_sal emp.sal%type
);
--记录类型类似其他编程语言中的类
v_emp_record emp_record;--声明一个emp_record类型的变量,相当于创建一个类的实例
begin
select ename,job,sal into v_emp_record from emp where ename='ALLEN';--注意查询的顺序必须和记录类型中定义的顺序一致
dbms_output.put_line(v_emp_record.v_name||' '||v_emp_record.v_job||' '||v_emp_record.v_sal);
--通过实例访问相关属性
end;

  %rowtype:

 declare
v_emp_record emp%rowtype;--声明一个包含指定表中所有列的rowtype变量,使用上和记录类型完全一致,但它本质上并不是记录类型
begin
select * into v_emp_record from emp where ename='ALLEN';--把所有的列都查询出来赋值给该变量
dbms_output.put_line(v_emp_record.ename||' '||v_emp_record.sal);
--该变量中的属性和表的列名完全一致,可以根据需要,只使用部分数据
end;

  

  4,集合

  集合类似其他编程语言中的数组,也可以通过下标来访问数据。

  如果把它和记录类型、变量相比教,你会发现,标量标量是用来处理单行单列数据的,记录类型适合处理单行多列的数据,而集合则是用来处理单列多行数据的。

  Oracle 提供了三种类型的集合:索引表(又称关联数组)、嵌套表、可变长度数组。

  索引表可以通过数字或字符串来作为下标存储数据,下标可以不连续,索引表的容量即是数字的最大值,但它只能存储在内存中。

 declare
type idx_table is table of varchar(20) index by pls_integer;--创建索引表类型
-- index by 后可选的参数有pls_integer、binary_integer、varcahr(size)和使用%type 指定的varchar2类型
v_idx_table idx_table;--声明索引表类型的变量
begin
v_idx_table(1):='hello';--插入值
v_idx_table(2):='world';
dbms_output.put_line(v_idx_table(1)||' '||v_idx_table(2));
end;

  嵌套表只能使用数字作为下标,数字必须是有序的,嵌套表的容量没有限制,可以保存到数据库中。

 declare
type nest_table is table of varchar(20);--创建嵌套表类型
v_nest_table nest_table:=nest_table('x');--声明嵌套表类型的变量并初始化
--未初始化的嵌套表类型实际上是一个null,如果试图为其赋值会报错。初始化就是调用一个和创建的嵌套表类型同名的函数,
--函数的参数值类型需要和嵌套表类型定义的存储值类型(of 后的类型)相同,并且参数的个数默认就是这个嵌套表类型变量的初始容量
begin
v_nest_table.extend(5);--扩充嵌套表类型变量的容量
--如果要增加嵌套表的容量,需要调用extend方法(该方法将在稍后详细说明)
v_nest_table(1):='hello';--插入值
v_nest_table(2):='world';
dbms_output.put_line(v_nest_table(1)||' '||v_nest_table(2));
dbms_output.put_line(nvl(v_nest_table(3),'it is null'));--没被使用的位置为null
end;

  可变长度数组和嵌套表类似,都只能使用有序的数字作为下标,可变数组在定义时必须指定容量,但在运行时可以手动的扩充其容量,所以,可变数组的真实容量也可以是无限的,可变数组也可以存储到数据库中。

 declare
type varr is varray(5) of int;--创建可变数组类型
v_varr varr:=varr();--声明可变数组类型的变量并初始化
--和嵌套表一样的原因,必须初始化
begin
for i in 1..5 loop--循环插入值
v_varr.extend();
v_varr(i):=i;
end loop; dbms_output.put_line(v_varr(1)||','||v_varr(2)||','||v_varr(3)||','||v_varr(4)||','||v_varr(5));
end;

  嵌套表和可变数组能存入数据库是指:他们可以和普通数据类型一样,用来定义表的列。

 --第一步,创建一个保存在数据库中的嵌套表类型
create or replace type nest is table of varchar(20);
--第二步,创建一个带有嵌套表类型列的数据表
create table x(
x_id int,
x_nest nest
)nested table x_nest store as y;--使用nest table 指定这是一个包含嵌套表类型值的数据表,并通过 store as 创建一个关联表来专门存储嵌套表
--插入一条数据(包含初始化的嵌套表类型值)
insert into x values(1,nest('x','y','z'));
--第三步,在PL/SQL中读取嵌套表类型的值(多行操作使用游标)。数据表并没有直接存储嵌套表,所以不能直接使用select查询,而应该在PL/SQL程序块中查询
declare
v_nest nest;
begin
select x_nest into v_nest from x;
for i in 1..3 loop
dbms_output.put_line(v_nest(i));
end loop;
end;

  把可变长度数组存放到数据库就不需要使用 nested table 和 store as 指定相关信息,而且可以直接使用 select 查询存储了可变长度数组的数据表。所以,通常的建议是,当只是临时使用集合,那么索引表是最好的选择,如果需要把集合存入数据库,可变数组更操作起来更简单。

  

  5,集合常用方法

  集合的方法通过“.”点的形式调用:集合.方法。

 集合.exists(n)--判断是否存在某个集合的值
集合.count--统计集合中值的个数
集合.limit--查询集合容量(长度)
集合.first/集合.last--集合中第一个/最后一个值的索引
集合.prior(n)/集合.nest(n)--指定索引位置前一个/下一个值的索引(一般用在索引表中,因为其下标可能不连续)
集合.extend/集合.extend(n)--为集合增加1个/n个容量(一般用在嵌套表和可变数组中)
集合.trim/集合.trim(n)--从集合末尾删除1个/n个元素(一般用在嵌套表和可变数组中)
集合.delete/集合.delete(n)--从集合中删除所有元素/第n个元素(一般用在索引表和嵌套表中)

  

  6,变量的作用域

 declare
v1 int default 1;--外层块变量v1
begin
dbms_output.put_line(v1);
--dbms_output.put_line(v2);error 必须声明v2
declare
v2 int default 2;---内层块变量
v1 int default 3;
begin
dbms_output.put_line(v1);--返回3
end;
end;

  变量只在声明的块中起作用,内层块可以访问外层块的变量,但外层块无法访问内层块的变量,如果内外块声明的相同的变量,那么 PL/SQL 采用就近原则。

五  流程控制

  

  1,case

  case 语句有两种语法,简单 case 语法只做等值匹配,搜索 case 语法可以做区间匹配。

  先来看简单 case 语法:

 declare
v_sal int;
begin
select sal into v_sal from emp where empno=&empno;
case v_sal
when 800 then dbms_output.put_line('太少了吧');
when 1600 then dbms_output.put_line('这还差不多');
when 3000 then dbms_output.put_line('这样更好');
when 5000 then dbms_output.put_line('这样最好');
else dbms_output.put_line('随缘吧');
end case;
end;

  搜索 case 语法:

 begin
select sal into v_sal from emp where empno=&empno;
case
when v_sal<=1000 then dbms_output.put_line('太少了吧');
when v_sal<=1600 then dbms_output.put_line('这还差不多');
when v_sal<=3000 then dbms_output.put_line('这样更好');
when v_sal<=5000 then dbms_output.put_line('这样最好');
else dbms_output.put_line('随缘吧');
end case;
end;

  请仔细观察两种语法的区别。

  

  2,if...elsif...else

 declare
v_sal int;
begin
select sal into v_sal from emp where empno=&empno;
if v_sal>=5000
then dbms_output.put_line('还有头发吗');
elsif v_sal>=3000
then dbms_output.put_line('还有一半吗');
else
dbms_output.put_line('好好珍惜头发啊,少年');
end if;
end;

  请注意,PL/SQL 中的多分支结构 elsif 关键字与其他语言相比,少了一个字母 e,且 els 和 if 之间没有空格。

  

  3,循环

  PL/SQL 提供了 3 种循环:loop、while、for(集合部分已经见过了)。

  在正式介绍循环之前,首先要介绍 PL/SQL 中的循环控制语句:exit,无条件结束整个循环(类似其他语言中的 break)。continue,结束本次循环,继续下一次循环。接下里让我们通过例子来详细说明每个循环的使用方法。

  loop 循环:

 declare
i int default 1;--定义,初始化循环控制变量
begin
loop
if i=5 then
i:=i+1;
continue;--当n等于5时,直接结束本次循环,不输出
end if;
dbms_output.put_line(i);
i:=i+1;--修改循环控制变量
exit when i>10;--根据循环控制比变量,判断是否退出循环
end loop;--结束循环
end;

  while 循环:

 declare
i int default 1;--定义,初始化循环控制变量
begin
while i<=10 loop--根据循环控制变量,判断是否进入循环体
dbms_output.put_line(i);--循环体
i:=i+1;--修改循环控制变量
end loop;--结束循环
end;

  for 循环:

 begin
--在for循环中,初始化循环控制变量,只需指明变量名即可,类型系统默认为数字,min..max指明控制变量的变化范围,从min开始,到max结束
for i in reverse 1..10 loop--i可以被循环体引用,但不能被赋值
dbms_output.put_line(i);--循环体
--注意,因为初始化循环变量时已经指定了变化范围,这相当于限定了循环条件,当变量从min变化到max时将自动结束循环
end loop; --结束循环
--最后说明,reverse是可选的参数,表示循环变量从max开始,到min结束
end;

  

  4,杂项

  这里要介绍两个东西,null 语句(不是null 值)和 goto 语句。null 语句表示什么也不做,goto 可以无条件跳转到程序指定位置。

 begin
if ... then
...
else
null;--什么也不做,但使整个语句块更丰满,可读性更高
end if;
end;
 declare
i int:=0;
begin
<<outer>>--定义一个标签
i:=i+1;
dbms_output.put_line(i);
if i<10 then
goto outer;--通过goto实现类似循环的结构
else
null;--通过使用null让语句块更易读
end if;
end;

  使用 goto 语句会破坏程序常规的执行流程,它是有别于顺序、分支、循坏的另一种执行流程,如无特别需求,建议不要使用。

六  异常处理

  

  1,异常简介

  无论何时何地何人,在编程的领域,总是无法避开异常。为了保证程序的健壮性,多数语言都提供了异常处理机制,PL/SQL 也不例外。

  在 PL/SQL 中,异常大致可分为两大类:

    编译时错误:程序在编写过程中的错误,例如语法错误,访问不存在的对象等,这类错误在编译时 PL/SQL 引擎就会发现,并通知用户。

    执行时错误:这类错误会顺利通过程序的编译环节,只能等到执行时才能被发现,比如除数是 0 。这类错误也是最要命的。

  

  2,异常处理语法

  我们知道,PL/SQL 程序分为三个部分:声明区,执行区,异常处理区。基本的异常处理也包含此三个步骤:

    A:在定义区,定义异常。

    B:在执行区,触发异常。

    C:只要执行区触发了异常,那么执行区后续的业务代码都会立即停止执行,执行流程跳转至异常处理区。

 declare
异常变量名 exception;
begin
...
raise 异常变量名;
...
exception
when 异常变量名
then ...
end;

  如果有多个异常,可以定义多个变量,并在合适的时候触发他们,并在异常处理区通过多个 when...then 来捕获他们,并执行特定操作。

  

  3,预定义异常

  大多数编译时的异常,Oracle 都在内部隐式的定义好了,并且不需要在执行区手动的触发,这类异常的处理最为简单:

declare
v_tmp varchar(10);
begin
v_tmp:='超过10字节的长度了';
exception
when value_error
then
dbms_output.put_line('出现value_error错误!' || '错误编号:'|| sqlcode || '错误名称' || sqlerrm);
end;

  PL/SQL 中出现的错误,都一个错误号,一个错误编码(sqlcode),一个错误名称(sqlerrm)。在错误处理区通过在 when 后面指定错误名称,既可捕获到指定错误了。常见的预定义错误如下:

错误号 异常编码 异常名称 描述
ora-01012 -1017 not_logged_on 在没有连接数据库时访问数据
ora-01403 100 no_date_found select...into没有返回值
ora-01422 -1422 too_many_rows select...into结果集超过一行
ora-01476 -1476 zero_divide 除数为0
ora-01722 -1722 invalid_number 字符串和数字相加时,字符串转换失败
ora-06502 -6502 value-error 赋值时,变量长度不足
ora-06530 -6530 access_into_null 向null值对象赋值
ora-06592 -06592 case_not_found case语句中没有任何匹配的值并且没有else选项

  更多预定义异常请查询 Oracle 11g 《Oracle 在线文档》

  

  4,自定义错误

 declare
e_nocomm exception;--定义一个异常名称
v_comm number(10,2);
begin
select comm into v_comm from emp where empno=&empno;
if v_comm is null
then raise e_nocomm;--触发自定义异常
end if;
exception
when e_nocomm--捕获自定义异常
then dbms_output.put_lne('该员工没有提成');
when others--捕获未定义的错误
then dbms_output.put_line('未知错误 !');
end;

  同一个块中不能同时声明一个异常多次,但不同的块中可以定义相同的异常,在各自的块中使用不会相互影响。

七  编程对象

  

  1,事务

  在 SQL Server 中,每一条 DML 语句都是一个隐式的事务,除非显示的开始一个事务,否则,这些语句执行完就立即向数据库提交了这些更改。而在 Oracle 中,每一条 SQL 语句开始都会自动开启一个事务,除非显示的使用 commit 提交,或退出某个开发工具而断开连接,才会提交到数据库,否则这些操作都只会保存在内存中。

 --在Oracle SQL Developer中
begin
insert into dept values(88,'开发部','cd');
savepoint a;--设置保存点a
insert into dept values(88,'设计部','cd');
exception
when dup_val_on_index then
dbms_output.put_line('插入出错');
rollback to a;--回滚到a
end;
--这里我们人为的制造了一个违反唯一约束的插入操作,在错误区捕获该错误,然后回滚到保存点a
select * from dept;--只能查询到开发部被插入
 /* 在 SQL*Plus 中 */
SQL>select * from dept;
/* 连开发部都没有被插入 */ 
1 -- 在 Oracle SQL Developer中
commit;
3 --现在插入已经被提交到数据库,在SQL*Plus 中也可以查询到了

  在多个事务并发执行时,大概率会发生:一个事务读取到另一个事务还未提交的数据(脏读);一个事务中不同时间点执行的同一个查询,由于其他事务对涉及的数据进行了修改或删除(不可重复读)或插入(幻读),而导致出现不一样的结果。

  为了解决这样的问题,Oracle 允许对事务设立隔离级别:

 begin
commit;
set transaction read only;--只读的事务
--settransaction read write;--可读写的事务
--set transaction isolation level [serializable | read commited];
--serializable:整个事务只能读到当前事务开始前就以提交的数据
--read commited:当前事务中的查询,只能读到该查询前以提交的数据(不是整个事务,而是该查询语句。这也是 Oracle 默认的隔离级别)
end;

  由于一个事务中有且只能存在一条 set transaction 语句,且必须是事务的第一条语句,所以通常先使用 commit 结束前一个事务(理论上rollback也可以),以保证该语句是事务的第一条语句。

  

  2,子程序

  Oracle 中子程序事实上就是 SQL Server 中对存储过程和用户自定义函数的总称。过程和函数本质上是一个命名块,可以被存储在数据库中,并在合适的时候调用,这样可以解决代码重用的问题,并且由于它是已编译好的代码,所以执行起来也更快。

  过程和函数相比,过程不会返回值,常用来做数据的增删改。而函数必须有返回值,通常用来向应用程序返回值。其他方面,过程和函数几无区别。

  存储过程:

 --无参过程
create or replace procedure p2 as
begin
dbms_output.put_line('hello world');
end p2;
--or replace:如果存在则替换存储过程,建议使用
--p1:过程名
--as:不能省略,也可以用is代替
--end p2:创建完成时也要跟上过程名
 --带参数的过程
create or replace procedure p2(p_deptno in int)--使用括号添加过程需要的形参
as
v_empcount number;--定义过程中需要使用的变量,只需指定数据类型,不能添加类型所占字节长度
begin
select count(ename) into v_empcount from emp where deptno=p_deptno;
if v_empcount>0 then
dbms_output.put_line('有人');
else
dbms_output.put_line('没人');
end if;
end p2;--不要忘了过程名
 --调用存储过程
begin
p2(20);--通过()传递实参
end;
--call p2(20);

  函数:

 --创建函数
create or replace function f1
return number--需要指定返回值类型,不需要长度
as
begin
return 1;--需要使用return指定返回值
end f1;
--调用函数
declare
v_f1 number(10);
begin
v_f1:=f1();--调用函数,并把返回值赋值给变量
dbms_output.put_line(v_f1);
end;

  在上面带参数存储过程中,指定形参时使用关键字 in,该关键字表示参数的模式是输入型,可选的还有 out 输出型,in out 输入输出型。

  in 模式的参数被用作输入参数,在过程内部只能被访问,不能被赋值。

  out  模式的参数被当做输出参数使用,在过程内部可以被赋值,不能访问。使用 out 类型参数时,必须在过程外部定义一个变量,用于接收过程在内部需要输出的值,然后在调用子程序时把该变量当做形参传入。待过程执行完毕,直接访问外部定义的这个变量即可得到过程希望输出的值了。

  in out 模式的参数既可以被当做输入参数,也可以被当做输出参数。使用方式和 out 型参数一致,但可以给这个变量一个初始化值,一并传入过程内部。out 型参数即使传入了初始值,也会被过程忽略。

  过程的参数模式和 MySQL 完全一致,例子可以参考我的《MySQL 编程》。函数本身就需要使用 return 返回值,所以不使用 in 或 out 指定参数模式,这样毫无意义。

  

  3,触发器

  Oracle 中的触发器本质上也是一个命名的语句块,定义的方式和 PL/SQL 语句块差不多,但它和过程或函数不同,它只能被隐式的调用。并且不能接受任何参数。

  定义触发器的语法:

 create or replace trigger trigger_name--触发器名称
[before | after | instead of]--在事件之前还是之后执行触发器中的代码
trigger_event--触发事件
[referenceing_caluse]--通过新的名称引用当前正在更新的数据
[when trigger_condition]--指定触发条件
[for each row]--指定行级触发器(每一条记录都触发一次)
trigger_body--触发体(程序块)

  一个简单例子:

 create test(--创建测试表
id int primary key,
name varchar(20)
)
create or replace trigger t_test--创建触发器
after insert or update or delete--触发操作(也可以是其中一种)
on test--在表test上
for each row--行级触发器
begin
if inserting then--在插入数据时
dbms_output.put_line('插入了数据,name:'||:new.name);
end if;
if updating then--在更新数据时
dbms_output.put_line('更新了数据,oldname:'||:old.name||',newname:'||:new.name);
end if;
if deleting then--在删除数据时
dbms_output.put_line('删除了数据,name:'||:old.name);
end if;
end;

  谓词:new 表示引用新的数据(更新后或插入的数据),:old 引用旧的数据(被删除的或更新前的数据)。可以在创建触发器时通过 referencing(操作类型之后,for each row 之前) 指定新的谓词。

 ...
referencing old as test_old new as test_new
...
--下面通过:test_old 引用修改前的数据,:test_new引用修改后的数据

  测试代码:

 insert into test values(1,'r');
update test set name='e' where id=1;
delete from test where id=1;
--注意观察输出结果

  

  4,游标

  Oracle 中的游标用来处理多行多列的数据集合,包含四个步骤:定义,打开,遍历,关闭。游标的语法如下:

 cursor cursor_name [形参]--形参可以用来在where子句中限定游标记录
[return type]--可选的指定游标返回的值类型
is query--通过is指定查询(在这里使用形参)
[for update[of column_list]]--允许在游标中修改表中的数据,并在游标打开期间锁定选中的记录

  下面是一个通过游标遍历输出 dept 部门信息的例子:

 declare
deptrow dept%rowtype;--定义一个存储记录的变量
cursor dept_cur is--通过cursor定义游标,is指定需要遍历的结果集(一个查询语句)
select * from dept;
begin
open dept_cur;--打开游标
loop--通过循环遍历游标中的记录
fetch dept_cur into deptrow;--通过fetch提取游标中记录(每次一条)赋值给变量
dbms_output.put_line(deptrow.deptno||':'||deptrow.dname);
exit when dept_cur%notfound;--通过%notfound判断游标中是否还有记录
end loop;
close dept_cur;--关闭游标
end;

  游标除了 %notfound 还有以下常用的的属性:

 cursor%isopen;--检测游标是否已打开,打开返回ture,否则返回false
cursor%found;--检测是否提取到值,提取到返回true,否者返回false
cursor%notfound;--与%found相反
cursor%rowcount;--统计到目前为止已提取的记录数

  PL/SQL 中的三种循环就够都可以用来循环遍历游标中的记录,while 和 loop 相似,这里不再举例,for 循环专门对遍历游标做了强化,工作中使用最多,也最方便:

 delcare
cursor dept_cur is
select * from dept;
begin
for dept_row in dept_cur loop
dbms_output.put_line(deptrow.deptno||':'||deptrow.dname);
end loop;
end;

  dept_row 不需要显式的声明为记录类型,PL/SQL 引擎自动隐式的声明为 %rowtype。for 循环开始,自动打开游标,并自动提取记录,然后赋值给dept_row,不用显式的使用 fetch 提取记录,循环完毕自动关闭游标并退出循环。

  

  5,包

  Oracle 中包(package)是一个工程化和面向对象的概念,它就像一个容器或命名空间,把逻辑相关的变量、类型、子程序或异常等组合起来一起存放,形成一个有序的组织单元或模块,当我们编写大型的复杂的应用程序时,我们就可以通过包来方便的归类和管理各个功能模块。

  完整的包由包规范和包体组成,但 Oracle 分开编译的存储包规范和包体,这又使得我们可以脱离包体使用包规范(反向不行)。包规范中主要是一些定义信息(也可以看成是 PL/SQL 提供的 API),比如记录类型、变量、游标、异常和子程序的声明。包体则负责实现包规范中定义的子程序。

  包规范简单应用:

 create or replace package pkg1--创建包规范
as
i int := 1;--标量变量
dept_record dept%rowtype;--rowtype类型
type dept_tab is table of varchar(20) index by pls_integer;--集合类型
end pkg1; declare
mydept pkg1.dept_tab;--创建一个包中集合类型的变量(通过"包.内容"的方式访问包中的内容)
begin
select * into pkg1.dept_record from dept where deptno=10;--给包中定义的rowtype类型变量赋值
dbms_output.put_line(pkg1.dept_record.dname);--访问包中的rowtype类型变量
dbms_output.put_line('-------------------------------------------');--分割线
for deptrow in (select * from dept) loop--使用游标给包中的集合赋值
mydept(pkg1.i) := deptrow.dname;
pkg1.i := pkg1.i+1;--修改包中的标量变量
end loop;
for j in 1..mydept.count loop--使用循环访问集合
dbms_output.put_line(mydept(j));
end loop;
pkg1.i := 1;--初始化包中的标量变量(防止下一次游标读取不到数据)
end;

  在这个例子中,我们只创建了包规范,没有包体,并且在包中定义了标量变量,rowtype类型(记录类型同理),集合这些基本的数据类型,然后在 PL/SQL 程序块中使用了他们。

  包规范中只有声明,没有具体的实现,事实上,包规范中的声明的内容是公共的,对于一个方案来说,相当于一个全局的对象,在包内任何地方都能访问他们。包规范和包体分别进行独立的编译和存储,所以没有包体,上诉例子任然能正常运行。

  另一个例子:

 create or replace package pkg2--创建包规范
as
cursor dept_cur return dept%rowtype;--定义游标类型
procedure dept_ins(p_deptno int,p_dname varchar);--定义存储过程
function f2 return varchar;--定义函数
end pkg2; create or replace package body pkg2--创建包体
as
cursor dept_cur return dept%rowtype--创建游标
is
select * from dept;
procedure dept_ins(p_deptno in int,p_dname in varchar)--创建存储过程
as
begin
insert into dept(deptno,dname) values(p_deptno,p_dname);
dbms_output.put_line('新增了部门:'|| p_deptno||','||p_dname);
end dept_ins;
function f2 return varchar--创建函数
is
begin
return '这是个函数';
end f2;
end pkg2; begin
for deptrow in pkg2.dept_cur loop--读取游标
dbms_output.put_line(deptrow.dname);
end loop;
pkg2.dept_ins(99,'TI');--执行存储过程
dbms_output.put_line(pkg2.f2());--执行函数
end;

  上面的例子在包体中定义了游标,存储过程和函数,并且在包规范中也声明了他们,这时候,存储过程和函数、游标都是公开的了,如果在包体中创建的内容并未在包规范中定义,那么我们说,这些内容是包私有的,不能在其他地方调用,而只能在包体内部使用。

  合理的使用包,有助于我们进行模块化的程序开发;把逻辑相关的东西放在一个包中进行开发和管理,是我们的程序更加规范化;把一些重要的东西定义成包的私有内容,可以大大加强数据的安全性;另外,由于在使用包时, PL/SQL 会把整个包都加载到内存中,所以还可以提高程序运行效率。

PL/SQL编程急速上手的更多相关文章

  1. ORACLE PL/SQL编程详解

    ORACLE PL/SQL编程详解 编程详解 SQL语言只是访问.操作数据库的语言,并不是一种具有流程控制的程序设计语言,而只有程序设计语言才能用于应用软件的开发.PL /SQL是一种高级数据库程序设 ...

  2. Oracle数据库编程:PL/SQL编程基础

    2.PL/SQL编程基础: PL/SQL块:        declare        定义部分        begin        执行部分        exception        异 ...

  3. pl/sql编程

    body { font-family: "Microsoft YaHei UI","Microsoft YaHei",SimSun,"Segoe UI ...

  4. [强烈推荐]ORACLE PL/SQL编程详解之七:程序包的创建与应用(聪明在于学习,天才在于积累!)

    原文:[强烈推荐]ORACLE PL/SQL编程详解之七:程序包的创建与应用(聪明在于学习,天才在于积累!) [强烈推荐]ORACLE PL/SQL编程详解之七: 程序包的创建与应用(聪明在于学习,天 ...

  5. ORACLE PL/SQL编程之八:把触发器说透

    原文:ORACLE PL/SQL编程之八:把触发器说透 ORACLE PL/SQL编程之八: 把触发器说透 大家一定要评论呀,感谢!光发表就花了我将近一个下午. 本篇主要内容如下: 8.1 触发器类型 ...

  6. [推荐]ORACLE PL/SQL编程之五:异常错误处理(知已知彼、百战不殆)

    原文:[推荐]ORACLE PL/SQL编程之五:异常错误处理(知已知彼.百战不殆) [推荐]ORACLE PL/SQL编程之五: 异常错误处理(知已知彼.百战不殆) 继上三篇:ORACLE PL/S ...

  7. ORACLE PL/SQL编程之六:把过程与函数说透(穷追猛打,把根儿都拔起!)

    原文:ORACLE PL/SQL编程之六:把过程与函数说透(穷追猛打,把根儿都拔起!) ORACLE PL/SQL编程之六: 把过程与函数说透(穷追猛打,把根儿都拔起!)   继上篇:ORACLE P ...

  8. [推荐]ORACLE PL/SQL编程详解之三:PL/SQL流程控制语句(不给规则,不成方圆)

    原文:[推荐]ORACLE PL/SQL编程详解之三:PL/SQL流程控制语句(不给规则,不成方圆) [推荐]ORACLE PL/SQL编程详解之三: PL/SQL流程控制语句(不给规则,不成方圆) ...

  9. [推荐]ORACLE PL/SQL编程之四:把游标说透(不怕做不到,只怕想不到)

    原文:[推荐]ORACLE PL/SQL编程之四:把游标说透(不怕做不到,只怕想不到) [推荐]ORACLE PL/SQL编程之四: 把游标说透(不怕做不到,只怕想不到) 继上两篇:ORACLE PL ...

随机推荐

  1. 这道LeetCode题究竟有什么坑点,让它的反对是点赞的9倍?

    本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是LeetCode专题的第38篇文章,我们一起来看看第65题,Valid Number. 曾经我们聊到过算法当中的一个类别--模拟题.所 ...

  2. 终于明白了vue使用axios发送post请求时的坑及解决原理

    前言:在做项目的时候正好同事碰到了这个问题,问为什么用axios在发送请求的时候没有成功,请求不到数据,反而是报错了,下图就是报错请求本尊 vue里代码如下: this.$http.post('/ge ...

  3. Docker的安装(Linux)

    官网下载安装说明 1.卸载旧版本 sudo yum remove docker \ docker-client \ docker-client-latest \ docker-common \ doc ...

  4. redis集群复制和故障转移

    #### 一.集群的问题- 1.当某个主节点宕机后,对应的槽位没有节点承担,整个集群处于失败状态,不可用,怎么办- 2.如何判断某个主节点是否真正的岩机?- 3.如果从某个主节点的所有从节点中选举出一 ...

  5. Jenkins-Sonar集成配置及注意点

    首先说说关于Jenkins集成Sonar的相关配置:我jenkins与Sonar不在同一个服务器上! 先现在 SonarQube Scanner 插件. SonarQube Servers:系统配置 ...

  6. 【Nginx】centos7 yum命令安装nginx

    安装nginx 首先我们需要使用root用户进行操作 第一步:添加nginx存储库 sudo yum install epel-release 出现如下图说明成功: 第二步:安装nginx sudo ...

  7. 【Mac 实用技巧】不定期更新

    Mac去掉截屏图片边框外阴影效果 一次命令行:defaults write com.apple.screencapture disable-shadow -bool true;\killall Sys ...

  8. Life In Changsha College- 第四次冲刺

    第四次冲刺任务 整体功能实现. 用户故事 用户打开“生活在长大”的界面,选择登录 已注册过则输入用户名和密码直接登录 未注册用户则可选择注册功能,注册成功后登录 登录成功则弹出提示框 进行留言 系统结 ...

  9. [PHP自动化-进阶]003.CURL处理Https请求访问

    引言:继前文<模拟登录并采集数据>,<模拟登录带有验证码的网站>,大家对CURL基本上已经有了认识,这一讲简单的说一下请求Https. 在很多的站点,如TalkingData, ...

  10. [工具-008] C#邮件发送系统

    邮件发送系统很多,但是我这边给大家展示下我最近开发的一款邮件发送系统,有参照网上的一个兄弟的界面,进行了升级,界面如下. 从界面上我们可以看到了该邮件系统有如下功能: 1)服务器的设置 2)发件人的设 ...