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;

实践建议

  1. 大批量DML操作使用批量绑定技术
  2. DDL操作尽量在非业务高峰期执行
  3. 权限分配遵循最小权限原则

查询优化核心技巧

执行计划分析

-- 获取执行计划(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;

实践建议

  1. 定期收集统计信息,特别是大数据量表
  2. 使用SQL Trace分析耗时查询
  3. 考虑使用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;

实践建议

  1. 分析函数能显著简化复杂报表SQL
  2. 注意OVER子句的性能影响
  3. 结合物化视图提升分析查询性能

二、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;

实践建议

  1. 存储过程应包含完整的异常处理
  2. 事务控制要明确(COMMIT/ROLLBACK)
  3. 考虑使用自治事务记录日志

触发器开发规范

复合触发器(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;

实践建议

  1. 避免在触发器中执行长时间操作
  2. 考虑使用复合触发器减少重复代码
  3. 触发器逻辑应尽量简单

包的设计模式

完整的包示例

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;

实践建议

  1. 包是PL/SQL的模块化单元,合理组织功能
  2. 公共接口要稳定,内部实现可调整
  3. 使用包变量谨慎,注意会话状态

三、高级特性实战

动态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;

实践建议

  1. 优先使用绑定变量防止SQL注入
  2. 复杂动态SQL考虑使用DBMS_SQL
  3. 动态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;

实践建议

  1. 处理超过100行数据时应使用批量绑定
  2. 结合LIMIT子句控制内存使用
  3. 使用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;

实践建议

  1. 对象类型适合复杂数据结构建模
  2. 考虑对象视图将关系数据映射为对象
  3. 注意对象类型在分布式环境中的使用限制

总结与最佳实践

  1. SQL优化

    • 始终使用绑定变量
    • 分析执行计划时关注逻辑读次数
    • 合理使用物化视图预计算复杂查询
  2. PL/SQL开发

    • 包是代码组织的最佳单元
    • 异常处理要包含足够上下文信息
    • 考虑使用自治事务处理日志记录
  3. 高级特性

    • 动态SQL必须防范注入风险
    • 批量处理是性能关键
    • 对象类型适合特定场景,不要过度使用
  4. 版本适配

图1

通过掌握这些核心技术和最佳实践,您可以构建高效、可靠的Oracle数据库应用。根据实际业务需求选择合适的技术组合,并持续关注Oracle新版本的特性和改进。

添加新评论