视图与存储过程
最后更新: 2026-04-15
作者: Oracle Team
页面目录
第六章:视图与存储过程
文档信息
- 适用版本:Oracle 12c / 18c / 19c / 21c
- 阅读时间:约 55 分钟
- 前置知识:第五章索引
6.1 视图
6.1.1 视图类型
┌─────────────────────────────────────────────────────────────────┐
│ Oracle 视图类型 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 简单视图 复杂视图 │
│ ───────── ───────── │
│ • 基于单表 • 基于多表 │
│ • 不含函数 • 含聚合函数 │
│ • 可 DML 操作 • 通常只读 │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ SELECT │ │ SELECT │ │
│ │ ... │ │ d.dept, │ │
│ │ FROM emp │ │ COUNT(*), │ │
│ └─────────────┘ │ SUM(sal) │ │
│ │ FROM emp │ │
│ │ GROUP BY │ │
│ └─────────────┘ │
│ │
│ 特殊视图 │
│ ───────── │
│ • 物化视图 - 存储实际数据 │
│ • 对象视图 - 基于对象类型 │
│ • 嵌套视图 - 视图嵌套视图 │
│ │
└─────────────────────────────────────────────────────────────────┘
6.1.2 创建视图
-- 简单视图
CREATE OR REPLACE VIEW v_emp_it AS
SELECT
employee_id,
first_name,
last_name,
salary,
department_id
FROM employees
WHERE department_id = 50 -- IT 部门
WITH CHECK OPTION; -- 禁止违反 WHERE 条件的修改
-- 复杂视图(含聚合)
CREATE OR REPLACE VIEW v_dept_salary AS
SELECT
d.department_id,
d.department_name,
COUNT(e.employee_id) AS emp_count,
SUM(e.salary) AS total_salary,
AVG(e.salary) AS avg_salary,
MAX(e.salary) AS max_salary,
MIN(e.salary) AS min_salary
FROM departments d
LEFT JOIN employees e ON d.department_id = e.department_id
GROUP BY d.department_id, d.department_name;
-- 连接视图
CREATE OR REPLACE VIEW v_emp_details AS
SELECT
e.employee_id,
e.first_name || ' ' || e.last_name AS full_name,
e.job_id,
j.job_title,
d.department_name,
l.city,
l.country_id
FROM employees e
JOIN departments d ON e.department_id = d.department_id
JOIN jobs j ON e.job_id = j.job_id
JOIN locations l ON d.location_id = l.location_id;
6.1.3 视图操作
-- 查询视图
SELECT * FROM v_emp_it WHERE salary > 5000;
-- 视图 DML 操作(简单视图可更新)
INSERT INTO v_emp_it (employee_id, first_name, last_name, salary, department_id)
VALUES (300, 'Test', 'User', 3000, 50);
UPDATE v_emp_it SET salary = 4000 WHERE employee_id = 300;
DELETE FROM v_emp_it WHERE employee_id = 300;
-- WITH READ ONLY:禁止 DML
CREATE OR REPLACE VIEW v_dept_salary_readonly AS
SELECT * FROM v_dept_salary
WITH READ ONLY;
-- 查看视图定义
SELECT text FROM user_views WHERE view_name = 'V_EMP_IT';
-- 删除视图
DROP VIEW v_emp_it;
6.1.4 物化视图
-- 创建物化视图
CREATE MATERIALIZED VIEW mv_dept_salary
BUILD IMMEDIATE -- 创建时立即填充
REFRESH FAST -- 增量刷新
START WITH SYSDATE -- 立即开始
NEXT SYSDATE + 1 -- 每天刷新
AS
SELECT
department_id,
COUNT(*) AS emp_count,
SUM(salary) AS total_salary,
AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id;
-- 物化视图类型
-- BUILD IMMEDIATE: 创建时立即填充
-- BUILD DEFERRED: 创建时不填充,后续刷新
-- 刷新方式
-- FAST: 增量刷新(需要物化视图日志)
-- COMPLETE: 全量刷新
-- FORCE: 尝试 FAST,失败则 COMPLETE
-- NEVER: 从不刷新
-- 手动刷新
EXEC DBMS_MVIEW.REFRESH('MV_DEPT_SALARY', 'C');
-- 创建物化视图日志(支持增量刷新)
CREATE MATERIALIZED VIEW LOG ON employees
WITH (salary, department_id), SEQUENCE, ROWID;
-- 查看物化视图
SELECT
mview_name,
refresh_mode,
refresh_method,
build_mode,
last_refresh_type,
last_refresh_date
FROM user_mviews;
-- 删除物化视图
DROP MATERIALIZED VIEW mv_dept_salary;
6.2 PL/SQL 基础
6.2.1 PL/SQL 块结构
-- PL/SQL 匿名块
DECLARE
v_name VARCHAR2(50);
v_sal NUMBER(10,2);
BEGIN
SELECT first_name, salary
INTO v_name, v_sal
FROM employees
WHERE employee_id = 100;
DBMS_OUTPUT.PUT_LINE('Employee: ' || v_name || ', Salary: ' || v_sal);
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('Employee not found');
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('Error: ' || SQLERRM);
END;
/
-- 输出开关
SET SERVEROUTPUT ON
6.2.2 变量与数据类型
DECLARE
-- 基本类型
v_name VARCHAR2(50);
v_sal NUMBER(10,2);
v_hire DATE;
v_active BOOLEAN := TRUE;
-- 复杂类型
v_emp employees%ROWTYPE; -- 行类型
v_sal employees.salary%TYPE; -- 列类型
-- 记录类型
TYPE emp_record IS RECORD (
name VARCHAR2(100),
salary NUMBER(10,2),
dept VARCHAR2(50)
);
v_emp_rec emp_record;
-- 常量
c_pi CONSTANT NUMBER := 3.14159;
BEGIN
-- 使用 INTO 赋值
SELECT first_name, salary, hire_date
INTO v_name, v_sal, v_hire
FROM employees WHERE employee_id = 100;
-- 使用 %ROWTYPE
SELECT * INTO v_emp FROM employees WHERE employee_id = 100;
-- 记录赋值
v_emp_rec.name := v_name;
v_emp_rec.salary := v_sal;
END;
/
6.2.3 控制结构
-- IF 条件
DECLARE
v_sal NUMBER := 5000;
BEGIN
IF v_sal < 3000 THEN
DBMS_OUTPUT.PUT_LINE('Low salary');
ELSIF v_sal < 7000 THEN
DBMS_OUTPUT.PUT_LINE('Medium salary');
ELSE
DBMS_OUTPUT.PUT_LINE('High salary');
END IF;
END;
/
-- CASE 语句
DECLARE
v_grade CHAR(1) := 'B';
v_msg VARCHAR2(50);
BEGIN
v_msg := CASE v_grade
WHEN 'A' THEN 'Excellent'
WHEN 'B' THEN 'Good'
WHEN 'C' THEN 'Fair'
ELSE 'Needs Improvement'
END;
DBMS_OUTPUT.PUT_LINE(v_msg);
END;
/
-- LOOP 循环
DECLARE
v_counter NUMBER := 1;
BEGIN
LOOP
DBMS_OUTPUT.PUT_LINE('Count: ' || v_counter);
v_counter := v_counter + 1;
EXIT WHEN v_counter > 10;
END LOOP;
END;
/
-- FOR 循环
BEGIN
FOR i IN 1..10 LOOP
DBMS_OUTPUT.PUT_LINE('i = ' || i);
END LOOP;
-- 逆序
FOR i IN REVERSE 1..10 LOOP
DBMS_OUTPUT.PUT_LINE('i = ' || i);
END LOOP;
END;
/
-- WHILE 循环
DECLARE
v_counter NUMBER := 1;
BEGIN
WHILE v_counter <= 10 LOOP
DBMS_OUTPUT.PUT_LINE('Count: ' || v_counter);
v_counter := v_counter + 1;
END LOOP;
END;
/
-- CONTINUE
BEGIN
FOR i IN 1..10 LOOP
IF MOD(i, 2) = 0 THEN
CONTINUE; -- 跳过偶数
END IF;
DBMS_OUTPUT.PUT_LINE(i);
END LOOP;
END;
/
6.2.4 游标
-- 显式游标
DECLARE
CURSOR emp_cursor IS
SELECT employee_id, first_name, salary
FROM employees
WHERE department_id = 50
ORDER BY salary DESC;
v_emp emp_cursor%ROWTYPE;
BEGIN
OPEN emp_cursor;
LOOP
FETCH emp_cursor INTO v_emp;
EXIT WHEN emp_cursor%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_emp.employee_id || ': ' ||
v_emp.first_name || ' - ' || v_emp.salary);
END LOOP;
CLOSE emp_cursor;
END;
/
-- FOR 循环自动管理游标
BEGIN
FOR rec IN (SELECT employee_id, first_name, salary
FROM employees WHERE department_id = 50)
LOOP
DBMS_OUTPUT.PUT_LINE(rec.first_name || ': ' || rec.salary);
END LOOP;
END;
/
-- 带参数游标
DECLARE
CURSOR emp_cursor(p_dept_id NUMBER) IS
SELECT employee_id, first_name, salary
FROM employees
WHERE department_id = p_dept_id;
BEGIN
FOR rec IN emp_cursor(50) LOOP
DBMS_OUTPUT.PUT_LINE(rec.first_name);
END LOOP;
END;
/
-- 游标属性
-- emp_cursor%ISOPEN
-- emp_cursor%FOUND
-- emp_cursor%NOTFOUND
-- emp_cursor%ROWCOUNT
6.2.5 异常处理
DECLARE
v_emp_id NUMBER := 9999;
v_name VARCHAR2(50);
BEGIN
SELECT first_name INTO v_name
FROM employees
WHERE employee_id = v_emp_id;
DBMS_OUTPUT.PUT_LINE('Found: ' || v_name);
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('Employee ' || v_emp_id || ' not found');
WHEN TOO_MANY_ROWS THEN
DBMS_OUTPUT.PUT_LINE('Multiple employees found');
WHEN DUP_VAL_ON_INDEX THEN
DBMS_OUTPUT.PUT_LINE('Duplicate value on index');
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('Error: ' || SQLERRM);
DBMS_OUTPUT.PUT_LINE('Error Code: ' || SQLCODE);
END;
/
-- 用户自定义异常
DECLARE
e_insufficient_sal EXCEPTION;
PRAGMA EXCEPTION_INIT(e_insufficient_sal, -20001);
BEGIN
UPDATE employees SET salary = 100 WHERE employee_id = 100;
IF SQL%ROWCOUNT = 0 THEN
RAISE e_insufficient_sal;
END IF;
EXCEPTION
WHEN e_insufficient_sal THEN
DBMS_OUTPUT.PUT_LINE('Operation failed: Insufficient salary');
END;
/
-- RAISE_APPLICATION_ERROR
BEGIN
RAISE_APPLICATION_ERROR(-20001, 'Custom error message');
END;
/
6.3 存储过程
6.3.1 创建存储过程
-- 基础存储过程
CREATE OR REPLACE PROCEDURE p_update_salary(
p_emp_id IN employees.employee_id%TYPE,
p_new_sal IN employees.salary%TYPE
)
IS
v_old_sal employees.salary%TYPE;
BEGIN
-- 获取旧工资
SELECT salary INTO v_old_sal
FROM employees
WHERE employee_id = p_emp_id;
-- 更新工资
UPDATE employees
SET salary = p_new_sal
WHERE employee_id = p_emp_id;
-- 记录日志
DBMS_OUTPUT.PUT_LINE('Updated salary for emp ' || p_emp_id ||
': ' || v_old_sal || ' -> ' || p_new_sal);
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
RAISE_APPLICATION_ERROR(-20001, 'Failed to update salary: ' || SQLERRM);
END p_update_salary;
/
-- 调用存储过程
BEGIN
p_update_salary(100, 25000);
END;
/
EXEC p_update_salary(100, 25000);
CALL p_update_salary(100, 25000);
6.3.2 OUT/IN OUT 参数
-- OUT 参数:返回多个值
CREATE OR REPLACE PROCEDURE p_get_emp_info(
p_emp_id IN employees.employee_id%TYPE,
p_name OUT VARCHAR2,
p_sal OUT NUMBER,
p_dept OUT VARCHAR2
)
IS
BEGIN
SELECT e.first_name || ' ' || e.last_name,
e.salary,
d.department_name
INTO p_name, p_sal, p_dept
FROM employees e
JOIN departments d ON e.department_id = d.department_id
WHERE e.employee_id = p_emp_id;
END p_get_emp_info;
/
-- 调用
DECLARE
v_name VARCHAR2(100);
v_sal NUMBER;
v_dept VARCHAR2(100);
BEGIN
p_get_emp_info(100, v_name, v_sal, v_dept);
DBMS_OUTPUT.PUT_LINE(v_name || ' | ' || v_sal || ' | ' || v_dept);
END;
/
-- IN OUT 参数
CREATE OR REPLACE PROCEDURE p_format_phone(
p_phone IN OUT VARCHAR2
)
IS
BEGIN
-- 移除非数字字符并格式化
p_phone := REGEXP_REPLACE(p_phone, '[^0-9]', '');
IF LENGTH(p_phone) = 11 THEN
p_phone := SUBSTR(p_phone, 1, 3) || '-' ||
SUBSTR(p_phone, 4, 4) || '-' ||
SUBSTR(p_phone, 8, 4);
END IF;
END p_format_phone;
/
DECLARE
v_phone VARCHAR2(20) := '13812345678';
BEGIN
DBMS_OUTPUT.PUT_LINE('Before: ' || v_phone);
p_format_phone(v_phone);
DBMS_OUTPUT.PUT_LINE('After: ' || v_phone);
END;
/
6.3.3 存储过程管理
-- 查看存储过程
SELECT object_name, object_type, status, created
FROM user_objects
WHERE object_type IN ('PROCEDURE', 'FUNCTION');
-- 查看源代码
SELECT text
FROM user_source
WHERE name = 'P_UPDATE_SALARY'
ORDER BY line;
-- 重新编译
ALTER PROCEDURE p_update_salary COMPILE;
-- 删除
DROP PROCEDURE p_update_salary;
6.4 函数
6.4.1 创建函数
-- 计算个人所得税函数
CREATE OR REPLACE FUNCTION f_calculate_tax(
p_salary IN NUMBER
)
RETURN NUMBER
IS
v_tax NUMBER(10,2);
BEGIN
IF p_salary <= 5000 THEN
v_tax := 0;
ELSIF p_salary <= 8000 THEN
v_tax := (p_salary - 5000) * 0.03;
ELSIF p_salary <= 17000 THEN
v_tax := (p_salary - 8000) * 0.10 + 90;
ELSIF p_salary <= 30000 THEN
v_tax := (p_salary - 17000) * 0.20 + 990;
ELSE
v_tax := (p_salary - 30000) * 0.25 + 3590;
END IF;
RETURN v_tax;
END f_calculate_tax;
/
-- 调用函数
SELECT
employee_id,
first_name,
salary,
f_calculate_tax(salary) AS tax
FROM employees;
-- 在 PL/SQL 中调用
DECLARE
v_tax NUMBER;
BEGIN
v_tax := f_calculate_tax(10000);
DBMS_OUTPUT.PUT_LINE('Tax: ' || v_tax);
END;
/
6.4.2 函数与过程对比
| 特性 | 存储过程 | 函数 |
|---|---|---|
| 返回值 | 0-N 个(通过 OUT 参数) | 必须有返回值(RETURN) |
| SQL 调用 | 不能 | 可以(除特定限制) |
| DML 操作 | 可以 | 不能(除非使用自治事务) |
| 用途 | 业务逻辑、批量处理 | 计算、转换返回值 |
-- 函数用于 SQL
CREATE OR REPLACE FUNCTION f_get_dept_name(
p_dept_id IN NUMBER
) RETURN VARCHAR2
IS
v_name VARCHAR2(100);
BEGIN
SELECT department_name INTO v_name
FROM departments
WHERE department_id = p_dept_id;
RETURN v_name;
EXCEPTION
WHEN OTHERS THEN RETURN NULL;
END f_get_dept_name;
/
-- SQL 中使用
SELECT
employee_id,
first_name,
f_get_dept_name(department_id) AS dept_name
FROM employees;
6.5 包
6.5.1 包结构
-- 创建包规范(声明)
CREATE OR REPLACE PACKAGE emp_api AS
-- 常量
c_max_salary CONSTANT NUMBER := 100000;
-- 存储过程声明
PROCEDURE p_add_employee(
p_first_name IN VARCHAR2,
p_last_name IN VARCHAR2,
p_email IN VARCHAR2,
p_dept_id IN NUMBER
);
PROCEDURE p_update_salary(
p_emp_id IN NUMBER,
p_new_sal IN NUMBER
);
-- 函数声明
FUNCTION f_get_employee_count(p_dept_id IN NUMBER)
RETURN NUMBER;
FUNCTION f_validate_salary(p_sal IN NUMBER)
RETURN BOOLEAN;
END emp_api;
/
-- 创建包体(实现)
CREATE OR REPLACE PACKAGE BODY emp_api AS
-- 私有变量(包内全局)
v_emp_count NUMBER := 0;
-- 私有函数
FUNCTION f_generate_email(
p_first_name IN VARCHAR2,
p_last_name IN VARCHAR2
) RETURN VARCHAR2
IS
BEGIN
RETURN LOWER(p_first_name || '.' || p_last_name);
END f_generate_email;
-- 存储过程实现
PROCEDURE p_add_employee(
p_first_name IN VARCHAR2,
p_last_name IN VARCHAR2,
p_email IN VARCHAR2,
p_dept_id IN NUMBER
)
IS
BEGIN
INSERT INTO employees (
employee_id, first_name, last_name,
email, hire_date, department_id
) VALUES (
seq_employee_id.NEXTVAL, p_first_name, p_last_name,
p_email, SYSDATE, p_dept_id
);
COMMIT;
END p_add_employee;
PROCEDURE p_update_salary(
p_emp_id IN NUMBER,
p_new_sal IN NUMBER
)
IS
BEGIN
IF f_validate_salary(p_new_sal) THEN
UPDATE employees
SET salary = p_new_sal
WHERE employee_id = p_emp_id;
COMMIT;
ELSE
RAISE_APPLICATION_ERROR(-20001, 'Invalid salary');
END IF;
END p_update_salary;
-- 函数实现
FUNCTION f_get_employee_count(p_dept_id IN NUMBER)
RETURN NUMBER
IS
v_count NUMBER;
BEGIN
SELECT COUNT(*) INTO v_count
FROM employees
WHERE department_id = p_dept_id;
RETURN v_count;
END f_get_employee_count;
FUNCTION f_validate_salary(p_sal IN NUMBER)
RETURN BOOLEAN
IS
BEGIN
RETURN (p_sal > 0 AND p_sal <= c_max_salary);
END f_validate_salary;
END emp_api;
/
-- 调用包
BEGIN
emp_api.p_add_employee('John', 'Doe', 'john.doe', 50);
emp_api.p_update_salary(100, 8000);
DBMS_OUTPUT.PUT_LINE('Count: ' || emp_api.f_get_employee_count(50));
END;
/
6.5.2 包管理
-- 查看包信息
SELECT object_name, object_type, status
FROM user_objects
WHERE object_type IN ('PACKAGE', 'PACKAGE BODY');
-- 重新编译包
ALTER PACKAGE emp_api COMPILE PACKAGE;
ALTER PACKAGE emp_api COMPILE BODY;
-- 删除包
DROP PACKAGE emp_api;
6.6 触发器
6.6.1 触发器类型
| 类型 | 触发时机 | 说明 |
|---|---|---|
| BEFORE | 执行前 | 验证/修改数据 |
| AFTER | 执行后 | 审计/日志 |
| INSTEAD OF | 替代执行 | 用于视图 |
| STATEMENT | 语句级 | 整条语句一次触发 |
| ROW | 行级 | 每行触发一次 |
6.6.2 创建触发器
-- 行级触发器:自动记录工资变更
CREATE OR REPLACE TRIGGER trg_emp_salary_audit
BEFORE UPDATE OF salary ON employees
FOR EACH ROW
WHEN (OLD.salary != NEW.salary)
DECLARE
v_change NUMBER := 0;
BEGIN
v_change := :NEW.salary - :OLD.salary;
INSERT INTO salary_audit (
audit_id, employee_id, old_salary, new_salary,
change_amount, changed_by, changed_date
) VALUES (
seq_audit.NEXTVAL, :OLD.employee_id,
:OLD.salary, :NEW.salary, v_change,
USER, SYSDATE
);
END trg_emp_salary_audit;
/
-- 语句级触发器:禁止周末操作
CREATE OR REPLACE TRIGGER trg_no_weekend_dml
BEFORE INSERT OR UPDATE OR DELETE ON employees
DECLARE
v_day NUMBER := TO_CHAR(SYSDATE, 'D');
BEGIN
IF v_day IN (1, 7) THEN -- 周末
RAISE_APPLICATION_ERROR(-20001,
'DML operations are not allowed on weekends');
END IF;
END trg_no_weekend_dml;
/
-- 组合触发器(Oracle 11g+)
CREATE OR REPLACE TRIGGER trg_emp_compound
FOR INSERT OR UPDATE OR DELETE ON employees
COMPOUND TRIGGER
v_count NUMBER := 0;
BEFORE STATEMENT IS
BEGIN
DBMS_OUTPUT.PUT_LINE('Statement started');
END BEFORE STATEMENT;
BEFORE EACH ROW IS
BEGIN
IF INSERTING THEN
DBMS_OUTPUT.PUT_LINE('Inserting row');
ELSIF UPDATING THEN
DBMS_OUTPUT.PUT_LINE('Updating row');
ELSIF DELETING THEN
DBMS_OUTPUT.PUT_LINE('Deleting row');
END IF;
END BEFORE EACH ROW;
AFTER EACH ROW IS
BEGIN
v_count := v_count + 1;
END AFTER EACH ROW;
AFTER STATEMENT IS
BEGIN
DBMS_OUTPUT.PUT_LINE('Total rows: ' || v_count);
END AFTER STATEMENT;
END trg_emp_compound;
/
6.6.3 INSTEAD OF 触发器(视图)
-- 创建复杂视图
CREATE OR REPLACE VIEW v_emp_dept AS
SELECT
e.employee_id,
e.first_name,
e.salary,
d.department_id,
d.department_name
FROM employees e
JOIN departments d ON e.department_id = d.department_id;
-- 创建 INSTEAD OF 触发器
CREATE OR REPLACE TRIGGER trg_v_emp_dept
INSTEAD OF INSERT ON v_emp_dept
FOR EACH ROW
DECLARE
v_dept_id NUMBER;
BEGIN
-- 获取部门ID
SELECT department_id INTO v_dept_id
FROM departments
WHERE department_name = :NEW.department_name;
-- 插入员工
INSERT INTO employees (
employee_id, first_name, salary, department_id
) VALUES (
seq_employee_id.NEXTVAL, :NEW.first_name,
:NEW.salary, v_dept_id
);
END trg_v_emp_dept;
/
-- 通过视图插入
INSERT INTO v_emp_dept (first_name, salary, department_name)
VALUES ('New Employee', 5000, 'IT');
6.6.4 触发器管理
-- 查看触发器
SELECT trigger_name, trigger_type, triggering_event, status
FROM user_triggers;
-- 禁用/启用触发器
ALTER TRIGGER trg_emp_salary_audit DISABLE;
ALTER TRIGGER trg_emp_salary_audit ENABLE;
-- 禁用表的所有触发器
ALTER TABLE employees DISABLE ALL TRIGGERS;
ALTER TABLE employees ENABLE ALL TRIGGERS;
-- 删除触发器
DROP TRIGGER trg_emp_salary_audit;
6.7 本章小结
✅ 视图类型:简单视图、复杂视图、物化视图
✅ PL/SQL 块结构:DECLARE、BEGIN、EXCEPTION、END
✅ 控制结构:IF、CASE、LOOP、FOR、WHILE
✅ 游标:显式游标、隐式游标、游标 FOR 循环
✅ 存储过程:IN/OUT 参数、异常处理
✅ 函数:SQL 可调用、返回单一值
✅ 包:封装相关过程和函数
✅ 触发器:BEFORE/AFTER/INSTEAD OF、行级/语句级
📖 下章预告:触发器与事件调度器
下一步:学习 第七章:触发器与事件调度器