sql 筆記(轉)
轉載自:https://gitee.com/Lanmengye/SQL_Learning/blob/master/PLSQL%E5%9F%BA%E7%A1%80.md#
PLSQL變數與型別
替代變數
在PLSQL塊中可以使用替代變數來提示使用者輸入引數,相當於佔位符,在程式執行過程中將直接用輸入值替換內容,當需要輸入字串型別引數時需要用單引號將替代變數引起來,數值型不需要加單引號,替代變數舉例:
select &columns from employees;
繫結變數
-- execute immediate [帶繫結變數的目標sql] using [對應繫結變數的具體輸入值];
declare
last_name employees.last_name%type;
begin
execute immediate 'select last_name from employees where employee_id = :i' into last_name using 100;
dbms_output.put_line(last_name);
end;
型別
-
屬性型別
屬性型別是一種可以直接引用資料庫中列的資料型別來描述變數型別的型別。Oracle 提供了兩種屬性型別,分別是
%TYPE
和%ROWTYPE
- %TYPE:該屬性允許在宣告中引用資料庫中的列或先前宣告的變數資料型別,而不是硬編碼型別名稱。
- %ROWTYPE:該屬性可以表示資料庫中表或遊標的行的記錄型別。
v_name employees.last_name %type; -- 動態型別,與employees的last_name欄位型別保持一致 v_emp employees%rowtype; -- 定義包含employees表中所有列的變數
-
記錄型別
記錄型別是由單行多列標量構成的複合結構。可以看做是一種使用者自定義的資料型別,提供了將一個或多個標量封裝成一個物件進行操作的能力。在使用記錄資料型別的變數時,需要在宣告部分先定義記錄的成員變數,然後在執行部分引用該記錄變數本身或其中的成員。但不可以對記錄做整體性的比較運算,如判斷記錄型別的變數是否為 NULL。
type emp_record is record( -- 定義記錄型別 v_name employees.last_name %type, v_salary employees.salary %type ); v_emp_record emp_record; -- 定義型別為 emp_record 的變數
-
集合型別 VARRAY 和 TABLE(待補充)
declare type emp_name_tab is table of employees.last_name%type index by binary_integer; -- 定義表變數型別 type emp_department_tab is table of employees.department_id%type index by binary_integer; -- 定義表變數型別 emp_name_t emp_name_tab; -- 定義表變數 emp_department_t emp_department_tab; -- 定義表變數 cursor emp_cur is select last_name, department_id from employees where employee_id <=114; i number :=0; begin for emp_record in emp_cur loop emp_name_t(i) := emp_record.last_name; -- 表變數賦值 emp_department_t(i) := emp_record.department_id; -- 表變數賦值 i := i+1; end loop; for i in 0..14 loop dbms_output.put_line('Employee Name:' || emp_name_t(i) || ' Department_id: ' || emp_department_t(i)); --使用表變數 end loop; end;
PLSQL流程控制
判斷語句
-
if...elsif... else
declare v_emp number; begin select t.employee_id into v_emp from employees t where t.employee_id=&emp; if v_emp=100 then -- if 布林表示式 then 語句1; DBMS_OUTPUT.PUT_LINE('員工ID1:'||v_emp); elsif v_emp=101 then -- elsif 布林表示式 then 語句2; DBMS_OUTPUT.PUT_LINE('員工ID2:'||v_emp); else -- else 語句3 DBMS_OUTPUT.PUT_LINE('其他情況'); end if; -- end if; end;
-
case when
-- SQL語句中的case when -- 連續值判斷: case when 布林表示式 then ...... select e.employee_id, e.salary old_salary, case when e.salary >= 15000 and e.salary < 20000 then 0.05 when e.salary >= 10000 and e.salary < 15000 then 0.08 when e.salary >= 8000 and e.salary < 10000 then 0.1 else 0.15 end raise1_percent from employees e; -- 等值(不連續值)判斷: case 列名/變數名 when 值 then ... select case employee_id when 100 then 'Steven King' when 101 then 'Neena Kochhar' when 102 then 'Lex De Haan' else 'unkown' end from employees;
-- 在儲存過程中,case when 語句最後以 end case 結尾 declare grade char(1) := 'B'; BEGIN case when grade = 'A' then dbms_output.put_line('Excellent'); when grade = 'B' then dbms_output.put_line('Very good'); when grade = 'C' then dbms_output.put_line('Well done'); when grade = 'D' then dbms_output.put_line('You passed'); when grade = 'F' then dbms_output.put_line('Better try again'); else dbms_output.put_line('No such grade'); end case; END;
-
decode函式
-- decode 函式只能用來做等值判斷 -- decode(列名/變數名,值1,結果1,值2,結果2……) select decode(employee_id, 100,'Steven King', 101,'Neena Kochhar', 102,'Lex De Haan') from employees;
迴圈語句
-
loop基本迴圈
-- loop迴圈 declare cursor employee_ids is select employee_id from employees; v_i employees.employee_id%type; begin open employee_ids; loop -- 執行迴圈 fetch employee_ids into v_i; dbms_output.put_line(v_i); exit when employee_ids%rowcount >= 10;-- 當條件成立時退出迴圈 end loop; -- 結束迴圈 end; declare v_i int := 1; -- 定義迴圈變數 v_id employees.employee_id%type; v_name employees.last_name%type; begin loop -- 開始迴圈 select tab.employee_id, tab.last_name into v_id, v_name from (select e.employee_id, e.last_name, rownum rn from employees e order by e.employee_id asc) tab where rn = v_i; dbms_output.put_line(v_id || ':' || v_name); exit when v_i >= 10; -- 條件滿足時退出迴圈 v_i := v_i + 1; -- 改變迴圈變數 end loop; -- 結束迴圈 end;
-
while迴圈
declare v_i int := 1; -- 定義迴圈變數 v_id employees.employee_id%type; v_name employees.last_name%type; begin while v_i <= 10 loop -- 滿足條件開始迴圈 select tab.employee_id, tab.last_name into v_id, v_name from (select e.employee_id, e.last_name, rownum rn from employees e order by e.employee_id asc) tab where rn = v_i; dbms_output.put_line(v_id || ':' || v_name); v_i := v_i + 1; -- 改變迴圈變數 end loop; -- 結束迴圈 end;
-
for迴圈
declare v_id employees.employee_id%type; v_name employees.last_name%type; begin for v_i in 1..10 loop -- 開始迴圈 select tab.employee_id, tab.last_name into v_id, v_name from (select e.employee_id, e.last_name, rownum rn from employees e order by e.employee_id asc) tab where rn = v_i; dbms_output.put_line(v_id || ':' || v_name); end loop; -- 結束迴圈 end;
PLSQL遊標
顯式遊標
/*
遊標使用注意事項:
(1)使用完成之後一定要關閉遊標;
(2)不能關閉已經被關閉的遊標;
(3)能不使用遊標就不要使用遊標,因為遊標效率十分低下。
*/
-- 顯式遊標
-- loop迴圈方式
declare
cursor dept_cursor is select department_id, department_name from test_cursor; --定義遊標
v_dept_id test_cursor.department_id%type;
v_dept_name test_cursor.department_name%type;
begin
open dept_cursor; -- 2.開啟遊標
loop
fetch dept_cursor into v_dept_id,v_dept_name; -- 3. 提取當前行資料
dbms_output.put_line(v_dept_id||': '||v_dept_name);
exit when dept_cursor%rowcount > 10;
end loop;
close dept_cursor; -- 4. 關閉遊標
end;
-- for迴圈方式
-- 使用子查詢形式遍歷
begin
for v_record in ( select * from employees )
loop
dbms_output.put_line(v_record.employee_id||': '||v_record.last_name||': '||v_record.salary);
end loop;
end;
-- 使用遊標方式遍歷
declare
cursor emp_cursor is select * from employees;
begin
for v_record in emp_cursor
loop
dbms_output.put_line(v_record.employee_id||': '||v_record.last_name||': '||v_record.salary);
-- exit when emp_cursor%rowcount > 10; --只打印10行
end loop;
end;
隱式遊標
-- 隱式遊標:預設名稱sql
-- 遊標的四個重要屬性:found, notfound, rowcount,isopen
-- 隱式遊標的isopen屬性永遠為false
declare
begin
update test_cursor set manager_id = 108 where department_id between 1000 and 2000;
if sql%found then
dbms_output.put_line('共更新資料'||sql%rowcount||'條');
else
dbms_output.put_line('未找到要更新的資料');
end if;
end;
帶參遊標
-- 帶參遊標:與顯示遊標的區別是可以帶引數,從而根據where條件劃定範圍
declare
cursor dep_by_id_cursor(p_id number default 10) is
select t.* from test_cursor t where t.department_id <= p_id;
begin
dbms_output.put_line('遊標不傳參測試:');
for v_dep in dep_by_id_cursor loop --不傳參,取預設值10
dbms_output.put_line(v_dep.department_id||': '||v_dep.department_name);
end loop;
dbms_output.put_line('遊標傳參測試:');
for v_dep in dep_by_id_cursor(100) loop --傳參
dbms_output.put_line(v_dep.department_id||': '||v_dep.department_name);
end loop;
end;
REF遊標
- 弱型別REF遊標
/*
REF遊標和遊標變數用於處理執行時動態執行的SQL查詢,建立遊標變數需要兩個步驟:
1)宣告REF遊標型別
2)宣告REF遊標型別變數
*/
-- REF遊標:開啟遊標時動態指定具體結果集,不能使用for迴圈遍歷
-- 弱型別REF遊標
declare
type t_ref_cursor is ref cursor; -- 宣告ref遊標型別
v_cursor t_ref_cursor; -- 宣告ref遊標型別變數
v_dept departments%rowtype;
v_emp employees%rowtype;
begin
-- 列印departments表10前行資料
dbms_output.put_line('列印departments表前10行資料:');
open v_cursor for select * from departments; --開啟遊標時指定
loop
fetch v_cursor into v_dept;
dbms_output.put_line(v_dept.department_id ||': '|| v_dept.department_name);
exit when v_cursor%rowcount > 10;
end loop;
close v_cursor;
-- 列印employees表前10行資料
dbms_output.put_line('列印employees表前10行資料:');
open v_cursor for select * from employees;
loop
fetch v_cursor into v_emp;
dbms_output.put_line(v_emp.employee_id ||': '|| v_emp.last_name);
exit when v_cursor%rowcount > 10;
end loop;
close v_cursor;
end;
- 強型別REF遊標
-- 強型別REF遊標【預先指定查詢型別與返回型別】
-- 參考 https://www.cnblogs.com/nick-huang/p/4609100.html
declare
Type ref_cur_emp IS REF CURSOR RETURN scott.emp%RowType;
cur_emp ref_cur_emp;
rec_emp cur_emp%RowType;
v_sql varchar2(100) := 'select * from scott.emp t';
begin
-- xxx Open cur_emp For v_sql;
Open cur_emp For
select * from scott.emp t;
Loop
fetch cur_emp
InTo rec_emp;
Exit When cur_emp%NotFound;
dbms_output.put_line(cur_emp%rowcount || ' -> ' || rec_emp.empno ||
' ' || rec_emp.sal);
End Loop;
Close cur_emp;
end;
知識點補充
-- 遊標補充點:更新刪除資料
select * from test_cursor for update;
select t.*, rowid from test_cursor t;
SELECT * FROM EMPLOYEES WHERE DEPARTMENT_ID = 80;
DECLARE
CURSOR emp_cursor IS
(SELECT employee_id, last_name, department_name
FROM employees, departments
WHERE employees.department_id = departments.department_id
AND employees.department_id = 80)
FOR UPDATE OF salary NOWAIT; -- 當沒有獲得資源報錯
BEGIN
for v_emp in emp_cursor loop
update employees set salary = salary * 1.1 where current of emp_cursor;
--where current of 條件指定遊標所指的當前行(多表時可能會匹配不到)
END LOOP;
END;
PLSQL塊
匿名塊
-- 匿名塊只能執行一次,不能被呼叫
declare -- 宣告變數部分
v_test employees%rowtype;
begin -- 邏輯處理主體部分
select * into v_test from employees where EMPLOYEE_ID = &empid;
dbms_output.put_line('EMPLOYEE_ID :'|| v_test.EMPLOYEE_ID);
dbms_output.put_line('LAST_NAME :' || v_test.LAST_NAME);
-- 異常處理部分
exception
when NO_DATA_FOUND then dbms_output.put_line('無效的員工ID');
end;
儲存過程
/*
一組為了完成特定功能的SQL 語句集,儲存在資料庫中,經過第一次編譯後再次呼叫不需要再次編譯,使用者通過指定儲存過程的名字並給出引數(如果該儲存過程帶有引數)來執行它。儲存過程是資料庫中的一個重要物件,任何一個設計良好的資料庫應用程式都應該用到儲存過程。
*/
-- 建立儲存過程
CREATE OR REPLACE PROCEDURE RAISE_SALARY(BASE_SALARY NUMBER)
-- 儲存過程宣告部分(包括儲存過程名[(引數1 型別1,……)])
IS
-- 變數宣告部分
BEGIN --邏輯處理主體部分
UPDATE PLSQL_EMPLOYEES SET RAISE_SALARY = SALARY * 1.1 WHERE SALARY < BASE_SALARY;
COMMIT;
--可能會有異常處理部分
END RAISE_SALARY;
-- 呼叫儲存過程
begin
RAISE_SALARY(24000);
end;
函式
/*
函式用於返回特定資料。執行時需要有一個變數接收函式的返回值。儲存在資料庫中,
經過第一次編譯後再次呼叫不需要再次編譯,使用者通過指定函式的名字並給出引數(如果該函式帶有引數)來執行它。
*/
create or replace function get_sal(p_id in employees.employee_id%type)
return number
-- 儲存函式宣告部分(包括儲存函式名[(引數1 型別1,……)] return 返回型別)
is
-- 變數宣告部分
v_salary employees.salary%type := 0;
begin --邏輯處理主體部分
select salary into v_salary from employees where employee_id = p_id;
return v_salary;
--可能會有異常處理部分
end get_sal;
知識點補充
- 過程與函式的區別
-
in、out 和 in out模式引數區別
-
in: 用於接受呼叫程式的值,不能賦值;預設的引數模式;可以是常量、變數、表示式。
-
out: 用於向呼叫程式返回值;必須顯式指定值,接收不到傳入的值;必須是變數。
-
in out: 用於接受呼叫程式的值,並向呼叫程式返回更新後的值;必須顯式指定, 必須是變數。
-
-
儲存過程和函式中不能直接使用DML語句可以轉化為
execute immediate '要執行的sql語句';
PLSQL異常
在執行程式時出現的錯誤叫做異常。發生異常後,語句將停止執行,控制權轉移到PL/SQL塊的異常處理部分。 異常有兩種型別:
- 預定義異常:當PL/SQL程式違反Oracle規則或超越系統限制時隱式引發。
- 使用者定義異常:使用者可以在PL/SQL塊的宣告部分定義異常,自定義的異常通過RAISE語句顯式引發。
預定義異常
異常 | 說明 |
---|---|
ACCESS_INTO_NULL | 在未初始化物件時出現 |
CASE_NOT_FOUND | 在case語句中的選項與使用者輸入的資料不匹配時出現 |
COLLECTION_IS_NULL | 在給尚未初始化的表或資料賦值時出現 |
CURSOR_ALREADY_OPEN | 在使用者試圖開啟已經開啟的遊標時出現。 |
INVALID_CURSOR | 執行非法遊標運算時出現 |
TOO_MANY_ROWS | 在執行select into語句後返回多行時出現。 |
ZERO_DIVIDE | 以零作除數時出現 |
VALUE_ERROR | 產生大小限制錯誤時出現。如:變數中的列值超出變數的大小。 |
NO_DATA_FOUND | 在表中不存在請求的行時出現。 |
BEGIN
. . .
EXCEPTION
WHEN NO_DATA_FOUND THEN -- 總是需要考慮的兩種異常:NO_DATA_FOUND | TOO_MANY_ROWS
statement1;
WHEN TOO_MANY_ROWS THEN
statement1;
WHEN OTHERS THEN
statement1;
statement2;
END;
使用者自定義異常
DECLARE
e_invalid_department EXCEPTION; -- 定義異常
BEGIN
UPDATE departments
SET department_name = &p_department_desc
WHERE department_id = &p_department_number;
IF SQL%NOTFOUND THEN
RAISE e_invalid_department; -- 使用raise關鍵字丟擲異常
END IF;
COMMIT;
EXCEPTION
WHEN e_invalid_department THEN -- 處理異常
DBMS_OUTPUT.PUT_LINE('No such department id.');
END;
/*
RAISE_APPLICATION_ERROR() 函式:對於使用者自定義的業務錯誤,如果覺得先定義再使用很麻煩,那麼也可以簡單的使用raise_application_error() 來簡化處理。它可以無需預先定義錯誤,而在需要丟擲錯誤的地方直接使用此函式丟擲例外,例外可以包含使用者自定義的錯誤嗎和錯誤描述。
*/
-- 執行部分
BEGIN
...
DELETE FROM employees
WHERE manager_id = v_mgr;
IF SQL%NOTFOUND THEN
RAISE_APPLICATION_ERROR(-20202,
'This is not a valid manager');
END IF;
...
-- 異常處理部分
...
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE_APPLICATION_ERROR (-20201,
'Manager is not a valid employee.');
END;
PLSQL包與JOB
包
CREATE OR REPLACE PACKAGE EMP_SALARY_ADJ IS
-- 包宣告部分(包含包中的儲存函式、儲存過程的宣告)
-- 在包宣告部分宣告的儲存函式和儲存過程必須在包主體中定義
-- 只有在包宣告部分聲明瞭的函式或過程才能在外部使用 【包名.函式名/過程名】進行呼叫
FUNCTION GET_RAISE_PER_BY_JOB_CHG(P_JOB_CHG NUMBER) RETURN NUMBER;
PROCEDURE SALARY_ADJ_BASE;
END EMP_SALARY_ADJ;
CREATE OR REPLACE PACKAGE BODY EMP_SALARY_ADJ IS
-- 包主體部分(包含包宣告的函式和過程的具體定義以及包中未宣告的函式和儲存過程)
FUNCTION GET_RAISE_PER_BY_JOB_CHG(P_JOB_CHG NUMBER) RETURN NUMBER IS
RESULT NUMBER := 0;
BEGIN
......
END GET_RAISE_PER_BY_JOB_CHG;
PROCEDURE SALARY_ADJ_BASE IS
BEGIN
......
END SALARY_ADJ_BASE;
PROCEDURE SALARY_ADJ_JOB_CHG IS
BEGIN
MERGE INTO EMP_SALARY_ADJUST S
USING (SELECT H.EMPLOYEE_ID, COUNT(1) AS JOB_CHG
FROM JOB_HISTORY H
GROUP BY H.EMPLOYEE_ID) Q
ON (S.EMPLOYEE_ID = Q.EMPLOYEE_ID)
WHEN MATCHED THEN
UPDATE
SET S.CHANGED_COUNT = Q.JOB_CHG,
S.RAISE2_PERCENT = GET_RAISE_PER_BY_JOB_CHG(Q.JOB_CHG);
COMMIT;
END SALARY_ADJ_JOB_CHG;
END EMP_SALARY_ADJ;
JOB
-- job的建立
declare
jobno number;
begin
dbms_job.submit(
jobno,
'p_dosomething', --what
to_date(), --next_date,可以不填
'Interval時間字串' --interval,關鍵設定
);
commit;
end;
declare
testjobno number;
begin
dbms_job.submit(
testjobno,
'ADD_DEPT;', --what
SYSDATE, --next_date,可以不填
interval => 'ADD_MONTHS(SYSDATE , 6)' --interval,關鍵設定
);
commit;
DBMS_JOB.RUN(jobno);
end;
PLSQL觸發器
觸發器概念
觸發器是當特定事件出現時自動執行的儲存過程,特定事件可以是執行更新的DML語句和DDL語句,觸發器不能被顯式呼叫。
觸發器的功能:
- 自動生成資料
- 自定義複雜的安全許可權
- 提供審計和日誌記錄
- 啟用複雜的業務邏輯
觸發器組成部分
觸發器型別
DML觸發器示例
CREATE TABLE test_trigger(id number(4), name varchar2(20));
-- 建立觸發器
create or replace trigger hello_trigger
after insert or delete or update on test_trigger for each row --什麼時候在對哪張表進行什麼操作時觸發
begin
IF INSERTING --執行INSERT操作時 自動執行以下語句
THEN
DBMS_OUTPUT.put_line ('I');
DBMS_OUTPUT.put_line ('insert batch_id='||:new.id);
ELSIF UPDATING --執行UPDATE操作時 自動執行以下語句
THEN
DBMS_OUTPUT.put_line ('U');
DBMS_OUTPUT.put_line ('update old last_name='||:old.name);
DBMS_OUTPUT.put_line ('update new last_name='||:new.name);
ELSIF DELETING --執行DELETE操作時 自動執行以下語句
THEN
DBMS_OUTPUT.put_line ('D');
DBMS_OUTPUT.put_line ('delete employee_id ='||:old.id);
END IF;
END;
-- 測試觸發器
insert into test_trigger(id, name) values(1111, 'aaaa');
insert into test_trigger(id, name) values(2222, 'bbbb');
update test_trigger set name = 'cccc' where id = 2222;
delete from test_trigger where id = 2222;
插入或修改資料
insert
-- insert into 後面可以接select子句,從而將某些表的資料插入到目標表中
insert into emp_salary_adjust
select s.employee_id, s.salary, raise_per_by_salary(s.salary) raise1_percent,
0 changed_count,
0 raise2_precent,
0 raise3_precent,
0 raise_salary,
0 new_salary
from employees s
where s.salary < 20000;
merge into
merge into emp_salary_adjust t1 -- 需要更新的表
using( -- 資料來源(用什麼資料去更新)
select distinct h.employee_id,
count(1) over(partition by h.employee_id) changed_count,
case when count(1) over(partition by h.employee_id)>=3 then 0.2
when count(1) over(partition by h.employee_id) = 2 then 0.1
when count(1) over(partition by h.employee_id) = 1 then 0.05 end raise2_percent
from job_history h
) t2
on (t1.employee_id = t2.employee_id) -- 根據什麼鍵去匹配更新
when matched then -- 匹配到該進行什麼操作(一般是update)
update
set t1.raise2_percent = t2.raise2_percent
when not matched then -- 匹配不到該進行什麼操作(一般是insert)
insert (t1.EMPLOYEE_ID) values(1000);
關於儲存過程或函式不能寫DDL語句的解決辦法
execute immediate
-- 在過程或函式中不能直接編寫DDL語句(create/alter/drop/truncate)
-- 可以使用execute immediate sqlstr的方式在過程或函式中執行DDL語句
execute immediate 'truncate table test';
使用trunc函式處理日期型別
select trunc(sysdate) from dual --2011-3-18 今天的日期為2011-3-18
select trunc(sysdate, 'mm') from dual --2011-3-1 返回當月第一天.
select trunc(sysdate,'yy') from dual --2011-1-1 返回當年第一天
select trunc(sysdate,'dd') from dual --2011-3-18 返回當前年月日
select trunc(sysdate,'yyyy') from dual --2011-1-1 返回當年第一天
select trunc(sysdate,'d') from dual --2011-3-13 (星期天)返回當前星期的第一天
select trunc(sysdate, 'hh') from dual --2011-3-18 14:00:00 當前時間為14:41
select trunc(sysdate, 'mi') from dual --2011-3-18 14:41:00 TRUNC()函式沒有秒的精確