Oracle SQL与PL/SQL深度解析:从基础到高级技巧
Oracle SQL与PL/SQL深度解析:从基础到高级应用
一、SQL基础精要
DML/DDL/DCL语句详解
DML(数据操作语言)是日常开发中最常用的SQL类型,主要包括:
-- 插入数据(多行插入语法是Oracle特有)
INSERT INTO employees VALUES(1001, '张三', '开发部');
INSERT ALL
INTO employees VALUES(1002, '李四', '测试部')
INTO employees VALUES(1003, '王五', '运维部')
SELECT * FROM dual;
-- 更新数据(带子查询的更新)
UPDATE employees e
SET e.department = (SELECT department FROM new_dept WHERE emp_id = e.id)
WHERE EXISTS (SELECT 1 FROM new_dept WHERE emp_id = e.id);
-- 删除数据(使用RETURNING子句获取被删除数据)
DELETE FROM employees WHERE id = 1001
RETURNING name, department INTO v_name, v_dept;
DDL(数据定义语言)管理数据库对象:
-- 创建表(Oracle特有的组织索引表)
CREATE TABLE orders (
order_id NUMBER PRIMARY KEY,
order_date DATE DEFAULT SYSDATE,
customer_id NUMBER,
CONSTRAINT fk_customer FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
) ORGANIZATION INDEX;
-- 修改表结构(在线重定义特性)
ALTER TABLE employees MODIFY (email VARCHAR2(100) NOT NULL);
DCL(数据控制语言)管理权限:
-- 创建角色并授权
CREATE ROLE report_reader;
GRANT SELECT ON sales_data TO report_reader;
GRANT report_reader TO user1, user2;
-- 细粒度权限控制(Oracle VPD前置示例)
BEGIN
DBMS_RLS.ADD_POLICY(
object_schema => 'HR',
object_name => 'EMPLOYEES',
policy_name => 'secure_emp',
function_schema => 'sec_admin',
policy_function => 'emp_access_ctl'
);
END;
实践建议:
- 大批量DML操作使用批量绑定技术
- DDL操作尽量在非业务高峰期执行
- 权限分配遵循最小权限原则
查询优化核心技巧
执行计划分析:
-- 获取执行计划(Oracle特有提示语法)
EXPLAIN PLAN FOR
SELECT /*+ INDEX(emp emp_name_idx) */ * FROM employees emp
WHERE emp.name LIKE '张%';
-- 查看执行计划
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
索引策略:
-- 创建函数索引(Oracle特有)
CREATE INDEX emp_upper_name_idx ON employees(UPPER(name));
-- 位图索引(适合低基数列)
CREATE BITMAP INDEX emp_dept_bidx ON employees(department);
统计信息收集:
-- 收集表统计信息(Oracle特有参数)
BEGIN
DBMS_STATS.GATHER_TABLE_STATS(
ownname => 'HR',
tabname => 'EMPLOYEES',
estimate_percent => DBMS_STATS.AUTO_SAMPLE_SIZE,
method_opt => 'FOR ALL COLUMNS SIZE AUTO',
cascade => TRUE
);
END;
实践建议:
- 定期收集统计信息,特别是大数据量表
- 使用SQL Trace分析耗时查询
- 考虑使用SQL Profile固定好的执行计划
分析函数高级应用
窗口函数示例:
-- 计算移动平均(Oracle特有分析函数)
SELECT
product_id,
sale_date,
amount,
AVG(amount) OVER (
PARTITION BY product_id
ORDER BY sale_date
RANGE BETWEEN INTERVAL '7' DAY PRECEDING AND CURRENT ROW
) AS 7day_avg
FROM sales;
排名与分页:
-- 高效分页查询(Oracle 12c新语法)
SELECT * FROM (
SELECT
e.*,
ROW_NUMBER() OVER (ORDER BY hire_date) AS rn
FROM employees e
)
WHERE rn BETWEEN 11 AND 20;
实践建议:
- 分析函数能显著简化复杂报表SQL
- 注意OVER子句的性能影响
- 结合物化视图提升分析查询性能
二、PL/SQL编程实战
存储过程与函数设计
带异常处理的存储过程:
CREATE OR REPLACE PROCEDURE transfer_funds(
p_from_acct NUMBER,
p_to_acct NUMBER,
p_amount NUMBER
) AS
v_balance NUMBER;
e_insufficient_funds EXCEPTION;
PRAGMA EXCEPTION_INIT(e_insufficient_funds, -20001);
BEGIN
-- 检查余额
SELECT balance INTO v_balance FROM accounts
WHERE account_id = p_from_acct FOR UPDATE;
IF v_balance < p_amount THEN
RAISE_APPLICATION_ERROR(-20001, '余额不足');
END IF;
-- 执行转账
UPDATE accounts SET balance = balance - p_amount
WHERE account_id = p_from_acct;
UPDATE accounts SET balance = balance + p_amount
WHERE account_id = p_to_acct;
COMMIT;
EXCEPTION
WHEN e_insufficient_funds THEN
ROLLBACK;
logger.log_error('转账失败: 账户' || p_from_acct || '余额不足');
RAISE;
WHEN OTHERS THEN
ROLLBACK;
logger.log_error('转账错误: ' || SQLERRM);
RAISE;
END;
实践建议:
- 存储过程应包含完整的异常处理
- 事务控制要明确(COMMIT/ROLLBACK)
- 考虑使用自治事务记录日志
触发器开发规范
复合触发器(Oracle 11g新特性):
CREATE OR REPLACE TRIGGER trg_emp_audit
FOR INSERT OR UPDATE OR DELETE ON employees
COMPOUND TRIGGER
-- 声明部分
v_operation VARCHAR2(10);
v_user VARCHAR2(30);
BEFORE STATEMENT IS
BEGIN
v_user := USER;
END BEFORE STATEMENT;
AFTER EACH ROW IS
BEGIN
IF INSERTING THEN
v_operation := 'INSERT';
ELSIF UPDATING THEN
v_operation := 'UPDATE';
ELSE
v_operation := 'DELETE';
END IF;
INSERT INTO emp_audit VALUES(
:NEW.emp_id,
v_operation,
v_user,
SYSDATE
);
END AFTER EACH ROW;
END;
实践建议:
- 避免在触发器中执行长时间操作
- 考虑使用复合触发器减少重复代码
- 触发器逻辑应尽量简单
包的设计模式
完整的包示例:
CREATE OR REPLACE PACKAGE emp_mgmt AS
-- 公共常量
g_max_salary CONSTANT NUMBER := 100000;
-- 异常定义
e_invalid_dept EXCEPTION;
-- 员工记录类型
TYPE emp_rec IS RECORD (
id employees.emp_id%TYPE,
name employees.emp_name%TYPE,
salary employees.salary%TYPE
);
-- 员工表类型
TYPE emp_tab IS TABLE OF emp_rec INDEX BY BINARY_INTEGER;
-- 获取部门员工
FUNCTION get_dept_emps(p_dept_id NUMBER) RETURN emp_tab;
-- 调整薪资
PROCEDURE adjust_salary(
p_emp_id NUMBER,
p_percent NUMBER,
p_result OUT NUMBER
);
END emp_mgmt;
CREATE OR REPLACE PACKAGE BODY emp_mgmt AS
-- 私有函数
FUNCTION validate_salary(p_salary NUMBER) RETURN BOOLEAN IS
BEGIN
RETURN p_salary <= g_max_salary;
END;
FUNCTION get_dept_emps(p_dept_id NUMBER) RETURN emp_tab IS
v_emps emp_tab;
BEGIN
SELECT emp_id, emp_name, salary
BULK COLLECT INTO v_emps
FROM employees
WHERE dept_id = p_dept_id;
RETURN v_emps;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN NULL;
END;
PROCEDURE adjust_salary(
p_emp_id NUMBER,
p_percent NUMBER,
p_result OUT NUMBER
) IS
v_new_salary NUMBER;
BEGIN
-- 获取当前薪资
SELECT salary INTO v_new_salary
FROM employees
WHERE emp_id = p_emp_id
FOR UPDATE;
-- 计算新薪资
v_new_salary := v_new_salary * (1 + p_percent/100);
-- 验证
IF NOT validate_salary(v_new_salary) THEN
RAISE e_invalid_dept;
END IF;
-- 更新
UPDATE employees
SET salary = v_new_salary
WHERE emp_id = p_emp_id;
p_result := v_new_salary;
COMMIT;
EXCEPTION
WHEN e_invalid_dept THEN
p_result := -1;
ROLLBACK;
RAISE_APPLICATION_ERROR(-20002, '薪资超过最大值');
END;
END emp_mgmt;
实践建议:
- 包是PL/SQL的模块化单元,合理组织功能
- 公共接口要稳定,内部实现可调整
- 使用包变量谨慎,注意会话状态
三、高级特性实战
动态SQL最佳实践
使用EXECUTE IMMEDIATE:
CREATE OR REPLACE PROCEDURE dynamic_query(
p_table_name VARCHAR2,
p_where_clause VARCHAR2 DEFAULT NULL
) AS
v_sql VARCHAR2(2000);
v_count NUMBER;
v_cursor SYS_REFCURSOR;
BEGIN
-- 执行动态DML
v_sql := 'UPDATE ' || p_table_name || ' SET last_accessed = SYSDATE';
IF p_where_clause IS NOT NULL THEN
v_sql := v_sql || ' WHERE ' || p_where_clause;
END IF;
EXECUTE IMMEDIATE v_sql;
-- 动态查询返回游标
v_sql := 'SELECT COUNT(*) FROM ' || p_table_name;
IF p_where_clause IS NOT NULL THEN
v_sql := v_sql || ' WHERE ' || p_where_clause;
END IF;
EXECUTE IMMEDIATE v_sql INTO v_count;
-- 返回结果集
v_sql := 'SELECT * FROM ' || p_table_name;
IF p_where_clause IS NOT NULL THEN
v_sql := v_sql || ' WHERE ' || p_where_clause;
END IF;
OPEN v_cursor FOR v_sql;
-- 处理结果...
END;
使用DBMS_SQL(复杂动态SQL):
CREATE OR REPLACE PROCEDURE multi_column_query(
p_table VARCHAR2,
p_columns DBMS_SQL.VARCHAR2_TABLE
) AS
v_cursor INTEGER;
v_col_cnt NUMBER;
v_status INTEGER;
v_value VARCHAR2(4000);
BEGIN
v_cursor := DBMS_SQL.OPEN_CURSOR;
-- 构建动态SQL
v_sql := 'SELECT ';
FOR i IN 1..p_columns.COUNT LOOP
IF i > 1 THEN v_sql := v_sql || ','; END IF;
v_sql := v_sql || p_columns(i);
END LOOP;
v_sql := v_sql || ' FROM ' || p_table;
DBMS_SQL.PARSE(v_cursor, v_sql, DBMS_SQL.NATIVE);
-- 定义输出列
FOR i IN 1..p_columns.COUNT LOOP
DBMS_SQL.DEFINE_COLUMN(v_cursor, i, v_value, 4000);
END LOOP;
v_status := DBMS_SQL.EXECUTE(v_cursor);
-- 处理结果
WHILE DBMS_SQL.FETCH_ROWS(v_cursor) > 0 LOOP
FOR i IN 1..p_columns.COUNT LOOP
DBMS_SQL.COLUMN_VALUE(v_cursor, i, v_value);
DBMS_OUTPUT.PUT_LINE(p_columns(i) || ': ' || v_value);
END LOOP;
END LOOP;
DBMS_SQL.CLOSE_CURSOR(v_cursor);
END;
实践建议:
- 优先使用绑定变量防止SQL注入
- 复杂动态SQL考虑使用DBMS_SQL
- 动态DDL操作需要额外权限
批量绑定性能优化
FORALL语句示例:
CREATE OR REPLACE PROCEDURE bulk_update_employees(
p_emp_ids IN DBMS_SQL.NUMBER_TABLE,
p_salaries IN DBMS_SQL.NUMBER_TABLE
) AS
BEGIN
-- 批量更新
FORALL i IN 1..p_emp_ids.COUNT
UPDATE employees
SET salary = p_salaries(i)
WHERE emp_id = p_emp_ids(i);
-- 批量插入
FORALL j IN 1..p_new_emps.COUNT SAVE EXCEPTIONS
INSERT INTO employees VALUES p_new_emps(j);
EXCEPTION
WHEN ex_dml_errors THEN
FOR k IN 1..SQL%BULK_EXCEPTIONS.COUNT LOOP
DBMS_OUTPUT.PUT_LINE(
'错误 ' || k || ': 索引 ' ||
SQL%BULK_EXCEPTIONS(k).ERROR_INDEX ||
' 错误代码 ' ||
SQL%BULK_EXCEPTIONS(k).ERROR_CODE ||
' 消息 ' ||
SQLERRM(-SQL%BULK_EXCEPTIONS(k).ERROR_CODE));
END LOOP;
END;
BULK COLLECT示例:
CREATE OR REPLACE FUNCTION get_high_paid_emps(
p_dept_id NUMBER,
p_threshold NUMBER
) RETURN emp_tab AS
v_result emp_tab;
BEGIN
-- 批量获取数据
SELECT emp_id, emp_name, salary
BULK COLLECT INTO v_result
FROM employees
WHERE dept_id = p_dept_id
AND salary > p_threshold;
-- 限制返回行数
IF v_result.COUNT > 100 THEN
v_result.TRIM(v_result.COUNT - 100);
END IF;
RETURN v_result;
END;
实践建议:
- 处理超过100行数据时应使用批量绑定
- 结合LIMIT子句控制内存使用
- 使用SAVE EXCEPTIONS处理部分失败
对象类型高级应用
完整对象类型示例:
-- 创建地址对象类型
CREATE OR REPLACE TYPE address_typ AS OBJECT (
street VARCHAR2(100),
city VARCHAR2(50),
state VARCHAR2(20),
zip_code VARCHAR2(10),
-- 成员方法
MEMBER FUNCTION format_address RETURN VARCHAR2,
MEMBER PROCEDURE validate_address(SELF IN OUT NOCOPY address_typ)
);
CREATE OR REPLACE TYPE BODY address_typ AS
MEMBER FUNCTION format_address RETURN VARCHAR2 IS
BEGIN
RETURN street || ', ' || city || ', ' || state || ' ' || zip_code;
END;
MEMBER PROCEDURE validate_address(SELF IN OUT NOCOPY address_typ) IS
BEGIN
-- 简单的验证逻辑
IF zip_code IS NULL OR LENGTH(TRIM(zip_code)) = 0 THEN
RAISE_APPLICATION_ERROR(-20001, '邮编不能为空');
END IF;
END;
END;
-- 创建嵌套表类型
CREATE OR REPLACE TYPE phone_list_typ AS TABLE OF VARCHAR2(20);
-- 创建包含对象类型的表
CREATE TABLE customers (
cust_id NUMBER PRIMARY KEY,
cust_name VARCHAR2(100),
cust_address address_typ,
phone_numbers phone_list_typ
) NESTED TABLE phone_numbers STORE AS phone_ntab;
-- 对象类型表
CREATE TABLE contacts OF contact_typ (
PRIMARY KEY (contact_id)
) OBJECT IDENTIFIER IS PRIMARY KEY;
-- 使用对象类型
DECLARE
v_address address_typ := address_typ(
'123 Main St', 'Anytown', 'CA', '12345');
v_phones phone_list_typ := phone_list_typ('123-456-7890', '456-789-0123');
BEGIN
-- 调用对象方法
v_address.validate_address();
DBMS_OUTPUT.PUT_LINE(v_address.format_address());
-- 插入对象数据
INSERT INTO customers VALUES (
1001, 'Acme Corp', v_address, v_phones);
END;
实践建议:
- 对象类型适合复杂数据结构建模
- 考虑对象视图将关系数据映射为对象
- 注意对象类型在分布式环境中的使用限制
总结与最佳实践
SQL优化:
- 始终使用绑定变量
- 分析执行计划时关注逻辑读次数
- 合理使用物化视图预计算复杂查询
PL/SQL开发:
- 包是代码组织的最佳单元
- 异常处理要包含足够上下文信息
- 考虑使用自治事务处理日志记录
高级特性:
- 动态SQL必须防范注入风险
- 批量处理是性能关键
- 对象类型适合特定场景,不要过度使用
- 版本适配:
通过掌握这些核心技术和最佳实践,您可以构建高效、可靠的Oracle数据库应用。根据实际业务需求选择合适的技术组合,并持续关注Oracle新版本的特性和改进。