Oracle数据恢复进阶实战:LogMiner日志挖掘与TRUNCATE恢复全攻略
大家好,我是睿。
上一期我们聊了Oracle闪回技术全家桶,从闪回查询到闪回数据库,把6种闪回方式全部实操了一遍。文章最后我留了个预告------当闪回技术搞不定的时候,该怎么办?
现实中,DBA经常遇到这些棘手场景:
• DML误操作后,redo日志已经被归档,在线日志里找不到
• 需要精确审计"谁在什么时间改了什么数据"
• TRUNCATE误删了核心业务表,闪回技术无能为力
• 没有开归档,闪回区也过期了
今天这篇文章,我们就来掌握几把终极武器:LogMiner日志挖掘和TRUNCATE后的数据恢复方案。这些是闪回之外的最后防线,每一个DBA都应该烂熟于心。
一、LogMiner日志挖掘
1.1 LogMiner能做什么
LogMiner是Oracle自带的redo日志分析工具,它能读取redo log和archive log,把二进制日志翻译成可读的SQL语句。核心能力:
• 查看DML操作的SQL_REDO(重放)和SQL_UNDO(回滚)
• 精确审计:谁、什么时间、对哪张表、做了什么操作
• 追踪数据变更历史,辅助故障定位
• 不需要额外license,标准版即可使用
1.2 前置条件与配置
开启补充日志(必须):
-- 开启最小补充日志(LogMiner的前提)
ALTER DATABASE ADD SUPPLEMENTAL LOG DATA;
-- 验证
SELECT supplemental_log_data_min FROM v$database;
-- SUPPLEMENTAL_LOG_DATA_MIN
-- YES
全库级别补充日志(可选,获取完整SQL_UNDO):
-- 全列补充日志(生成的SQL_UNDO更完整)
ALTER DATABASE ADD SUPPLEMENTAL LOG DATA (ALL) COLUMNS;
-- 或者针对特定表
ALTER TABLE mary.test_logminer ADD SUPPLEMENTAL LOG DATA (ALL) COLUMNS;
1.3 实战:挖掘在线redo日志
下面用一个完整的实验来演示整个流程。
Step 1:准备测试数据
-- 1.1 创建mary用户并授权(如已有可跳过)
CREATE USER mary IDENTIFIED BY mary DEFAULT TABLESPACE mary;
GRANT CONNECT, RESOURCE TO mary;
GRANT EXECUTE_CATALOG_ROLE TO mary; -- LogMiner需要此权限
-- 1.2 以mary用户创建测试表
CONN mary/mary
CREATE TABLE mary.test_logminer (
id NUMBER PRIMARY KEY,
name VARCHAR2(50),
salary NUMBER
);
-- 1.3 插入测试数据
INSERT INTO mary.test_logminer VALUES (1, 'Alice', 15000);
INSERT INTO mary.test_logminer VALUES (2, 'Bob', 18000);
INSERT INTO mary.test_logminer VALUES (3, 'Charlie', 20000);
COMMIT;
-- 1.4 验证数据
SELECT * FROM mary.test_logminer;
ID NAME SALARY
1 Alice 15000
2 Bob 18000
3 Charlie 20000
Step 2:执行DML操作(模拟误操作)
-- 2.1 模拟误操作:UPDATE + DELETE
UPDATE mary.test_logminer SET salary = 99999 WHERE id = 1;
1 row updated.
DELETE FROM mary.test_logminer WHERE id = 2;
1 row deleted.
COMMIT;
-- 2.2 查看误操作后的数据
SELECT * FROM mary.test_logminer;
ID NAME SALARY
1 Alice 99999 -- 被改了
-- Bob被删了
3 Charlie 20000
Step 3:添加日志文件到LogMiner
-- 查看当前在线redo日志
set linesize 500
set pagesize 100
col member for a40
col status for a10
SELECT a.THREAD#, a.GROUP#, b.MEMBER, a.BYTES / 1024 / 1024 AS SIZE_MB ,a.status
FROM VLOG a JOIN VLOGFILE b
ON a.GROUP# = b.GROUP# ORDER BY a.THREAD#, a.GROUP#;
THREAD# GROUP# MEMBER SIZE_MB STATUS
1 1 /u01/app/oracle/oradata/NOCDB/onlinelog/ 200 INACTIVE
o1_mf_1_nkf4jk6f_.log
1 1 /u01/app/oracle/fast_recovery_area/NOCDB 200 INACTIVE
/onlinelog/o1_mf_1_nkf4jmq9_.log
1 2 /u01/app/oracle/oradata/NOCDB/onlinelog/ 200 INACTIVE
o1_mf_2_nkf4jk8k_.log
1 2 /u01/app/oracle/fast_recovery_area/NOCDB 200 INACTIVE
/onlinelog/o1_mf_2_nkf4jsps_.log
1 3 /u01/app/oracle/oradata/NOCDB/onlinelog/ 200 CURRENT
o1_mf_3_nkf4jk99_.log
1 3 /u01/app/oracle/fast_recovery_area/NOCDB 200 CURRENT
/onlinelog/o1_mf_3_nkf4jz1z_.log
6 rows selected.
-- 添加要分析的日志文件
EXEC DBMS_LOGMNR.ADD_LOGFILE(LOGFILENAME => '/u01/app/oracle/fast_recovery_area/NOCDB/onlinelog/o1_mf_3_nkf4jz1z_.log',OPTIONS => DBMS_LOGMNR.NEW);
Step 4:启动LogMiner(使用在线数据字典)
EXEC DBMS_LOGMNR.START_LOGMNR(Options => DBMS_LOGMNR.DICT_FROM_ONLINE_CATALOG);
Step 5:查询分析结果
-- 查看所有DML操作
SELECT timestamp, sql_redo, sql_undo
FROM v$logmnr_contents
WHERE seg_name = 'TEST_LOGMINER'
ORDER BY timestamp;
TIMESTAMP SQL_REDO SQL_UNDO
01-DEC-22
insert into "MARY"."TEST_LOGMINER"("ID","NAME","SALARY") values ('1','Alice','15000');
delete from "MARY"."TEST_LOGMINER" where "ID" = '1' and "NAME" = 'Alice' and "SALARY" = '15000' and ROWID = 'AAAbZOAAFAAAAA7AAA';
01-DEC-22
insert into "MARY"."TEST_LOGMINER"("ID","NAME","SALARY") values ('2','Bob','18000');
delete from "MARY"."TEST_LOGMINER" where "ID" = '2' and "NAME" = 'Bob' and "SALARY" = '18000' and ROWID = 'AAAbZOAAFAAAAA7AAB';
01-DEC-22
insert into "MARY"."TEST_LOGMINER"("ID","NAME","SALARY") values ('3','Charlie','20000');
delete from "MARY"."TEST_LOGMINER" where "ID" = '3' and "NAME" = 'Charlie' and "SALARY" = '20000' and ROWID = 'AAAbZOAAFAAAAA7AAC';
01-DEC-22
update "MARY"."TEST_LOGMINER" set "SALARY" = '99999' where "SALARY" = '15000' and ROWID = 'AAAbZOAAFAAAAA7AAA';
update "MARY"."TEST_LOGMINER" set "SALARY" = '15000' where "SALARY" = '99999' and ROWID = 'AAAbZOAAFAAAAA7AAA';
01-DEC-22
delete from "MARY"."TEST_LOGMINER" where "ID" = '2' and "NAME" = 'Bob' and "SALARY" = '18000' and ROWID = 'AAAbZOAAFAAAAA7AAB';
insert into "MARY"."TEST_LOGMINER"("ID","NAME","SALARY") values ('2','Bob','18000');
看到了吗?SQL_REDO是实际执行的操作,SQL_UNDO是回滚操作------这就是恢复数据的关键。
Step 6:结束分析
EXEC DBMS_LOGMNR.END_LOGMNR;
1.4 实战技巧:精准定位目标操作
生产环境的日志量动辄几十上百个G,直接全量查询等于大海捞针。分享几个我常用的精准过滤技巧:
-- 按时间范围过滤
SELECT timestamp, sql_redo, sql_undo
FROM v$logmnr_contents
WHERE timestamp BETWEEN
TO_DATE('2026-06-30 10:00:00', 'YYYY-MM-DD HH24:MI:SS')
AND TO_DATE('2026-06-30 11:00:00', 'YYYY-MM-DD HH24:MI:SS')
AND seg_owner = 'MARY'
AND operation = 'DELETE';
-- 按SCN范围过滤(更精确)
SELECT timestamp, sql_redo, sql_undo
FROM v$logmnr_contents
WHERE scn BETWEEN 59447386 AND 59449766
AND seg_name LIKE 'TEST%';
1.5 分析归档日志
实际场景中,redo日志往往已经被归档。这时候同样可以分析归档日志:
-- 查看归档日志列表
SELECT name, sequence#, first_time, next_time
FROM v$archived_log
WHERE dest_id = 1
ORDER BY sequence#;
-- 添加多个归档日志
EXEC DBMS_LOGMNR.ADD_LOGFILE( LogFileName =>'/u01/app/oracle/fast_recovery_area/NOCDB/archivelog/2026_07_01/o1_mf_1_1222_krh55h1c_.arc',Options => DBMS_LOGMNR.NEW);
EXEC DBMS_LOGMNR.ADD_LOGFILE( LogFileName =>'/u01/app/oracle/fast_recovery_area/NOCDB/archivelog/2026_07_01/o1_mf_1_1221_krh3gm1w_.arc',Options => DBMS_LOGMNR.NEW);
-- 跨库分析时,在源库(被分析的库)上执行
BEGIN DBMS_LOGMNR_D.BUILD( dictionary_filename => 'dict.ora', dictionary_location => '/u01/logmnr/', options => DBMS_LOGMNR_D.STORE_IN_FLAT_FILE ); END; /
-- 在第三方数据库做日志分析,使用字典文件启动
EXEC DBMS_LOGMNR.START_LOGMNR(DictFileName => '/u01/logmnr/dict.ora', Options => DBMS_LOGMNR.DICT_FROM_REDO_LOGS);
1.6 LogMiner的局限性与注意事项
- TRUNCATE无法恢复:TRUNCATE是DDL操作,只修改segment header,
不产生逐行的redo/undo日志。LogMiner只能看到"TRUNCATE TABLE xxx"
的记录,但没有SQL_UNDO,无法通过重放来恢复数据。
恢复TRUNCATE需要其他方案,见第二章。
-
日志保留时间:在线redo日志会被循环覆盖,归档日志也被手动或自动清理。不能依赖"总能找到日志"。
-
性能影响:LogMiner分析大量日志时消耗CPU和内存,
建议在测试环境执行。
- LOB和LONG类型:对大字段的支持有限,SQL_UNDO可能不完整。
二、TRUNCATE后的数据恢复
在动手恢复之前,先搞清楚TRUNCATE的本质:
• TRUNCATE是DDL操作,会修改segment header中的data_object_id
• 重置High Water Mark(HWM)和extent map
• 不会逐行删除数据块中的内容
• 不产生数据级别的redo/undo日志
• 默认情况下会释放存储空间
关键原则:只要数据块没有被覆盖,就有机会恢复!
公共测试环境准备:
先创建测试表并批量插入1000条数据,后续各方案的演示均基于这张表。
-- 1. 创建测试表
sqlplus mary/mary
CREATE TABLE mary.employees (
emp_id NUMBER PRIMARY KEY,
emp_name VARCHAR2(50),
dept VARCHAR2(30),
salary NUMBER
);
-- 2. 批量插入1000条测试数据
BEGIN
FOR i IN 1..1000 LOOP
INSERT INTO mary.employees VALUES (
i,
'EMP_' || LPAD(i, 4, '0'),
CASE MOD(i, 4)
WHEN 0 THEN 'IT'
WHEN 1 THEN 'Sales'
WHEN 2 THEN 'HR'
WHEN 3 THEN 'Finance'
END,
10000 + MOD(i, 500) * 30
);
END LOOP;
COMMIT;
END;
/
-- 3. 验证数据量
SELECT COUNT(*) FROM mary.employees;
COUNT(*)
1000
-- 4. 查看前几条数据样例
SELECT * FROM mary.employees WHERE ROWNUM <= 5 ORDER BY emp_id;
EMP_ID EMP_NAME DEPT SALARY
1 EMP_0001 Sales 10030
2 EMP_0002 HR 10060
3 EMP_0003 Finance 10090
4 EMP_0004 IT 10120
5 EMP_0005 Sales 10150
2.1 方案一:容灾库闪回数据库恢复
闪回数据库是恢复TRUNCATE数据最快的方案。但在生产库上直接做FLASHBACK DATABASE影响面过大,绝对不可行。正确的做法是:在容灾库上做闪回,以read only模式打开,再通过dblink把数据反向插回生产库。
前提条件:容灾库(Physical Standby)长期开启Flashback Database。
操作步骤:
-- ===== 以下步骤在容灾库(备库)执行 =====
-- 1. 暂停容灾库的redo apply
ALTER DATABASE RECOVER MANAGED STANDBY DATABASE CANCEL;
-- 2. 在容灾库执行闪回到TRUNCATE之前的时间点
FLASHBACK DATABASE TO TIMESTAMP
TO_TIMESTAMP('2026-06-30 10:00:00', 'YYYY-MM-DD HH24:MI:SS');
-- 3. 以read only方式打开容灾库
ALTER DATABASE OPEN READ ONLY;
-- 4. 验证数据已恢复
SELECT COUNT(*) FROM mary.employees;
COUNT(*)
1000
通过dblink将数据反向回传到生产库:
-- ===== 在生产库执行 =====
-- 5. 在生产库创建到容灾库的dblink
CREATE DATABASE LINK to_standby
CONNECT TO mary IDENTIFIED BY oracle
USING '(DESCRIPTION =
(ADDRESS_LIST =
(ADDRESS = (PROTOCOL = TCP)(HOST = 192.168.110.101)(PORT = 1521))
)
(CONNECT_DATA =
(SERVICE_NAME = nocdb)
)
)';
-- 6. 从容灾库拉回数据
INSERT INTO mary.employees
SELECT * FROM mary.employees@to_standby;
commit;
-- 7. 验证
SELECT COUNT(*) FROM mary.employees;
COUNT(*)
1000
恢复完成后,容灾库重新同步:
-- ===== 在容灾库执行 =====
-- 8. 容灾库重新flashback到最新状态,恢复同步
ALTER DATABASE RECOVER MANAGED STANDBY DATABASE
USING CURRENT LOGFILE DISCONNECT;
这个方案的核心优势在于:不影响生产库正常运行,不需要RESETLOGS,数据通过dblink精准回传。唯一前提是容灾库必须提前开启Flashback Database------这也是我一直强调容灾库要开flashback的根本原因。
2.2 方案二:RMAN备份异机恢复
如果容灾库没有开Flashback,或者手头有RMAN备份,可以通过异机恢复来抢救数据。核心原则不变:所有操作在异机(辅助实例)上完成,绝不动生产库。
方法A:异机全库恢复(最有效,但最耗时、耗资源)
-- 在异机(辅助实例)上执行
-- 1. 在异机准备参数文件、创建目录结构
-- 2. 启动辅助实例到nomount
STARTUP NOMOUNT PFILE='/tmp/pfile.ora';
-- 3. 全库恢复到TRUNCATE之前的SCN
RMAN> RUN {
SET UNTIL SCN 59464891;
RESTORE DATABASE;
RECOVER DATABASE;
ALTER DATABASE OPEN RESETLOGS;
}
-- 4. 验证数据存在
SELECT COUNT(*) FROM mary.employees;
-- 5. 在生产库创建dblink连接到异机
CREATE DATABASE LINK to_aux
CONNECT TO mary IDENTIFIED BY mary_password
USING 'aux_tns';
-- 6. 从异机拉回数据
INSERT INTO mary.employees
SELECT * FROM mary.employees@to_aux;
COMMIT;
-- 7. 恢复完成后关闭异机
SHUTDOWN IMMEDIATE;
方法A的优势是数据完整性100%有保证,缺点是耗时长(需要全库restore+recover),且需要一台空闲的辅助服务器,适合大型灾难场景。
方法B:异机表空间级恢复
-- 在异机执行
-- 1. 只恢复目标表空间(节省时间和空间)
准备可用的参数文件
准备创建控制文件的脚本,只保留必要的数据文件
alter database backup controlfile to trace as '/soft/controlfile_reset.sql' resetlogs;
vi controlfile_reset.sql (只保留system、sysaux、undo、temp、目标表空间)
-- 目标环境执行:
STARTUP NOMOUNT
CREATE CONTROLFILE REUSE DATABASE "LOCALDB" RESETLOGS FORCE LOGGING ARCHIVELOG
MAXLOGFILES 16
MAXLOGMEMBERS 3
MAXDATAFILES 100
MAXINSTANCES 8
MAXLOGHISTORY 292
LOGFILE
GROUP 1 '/oradata/localdb/redo01.log' SIZE 50M BLOCKSIZE 512,
GROUP 2 '/oradata/localdb/redo02.log' SIZE 50M BLOCKSIZE 512,
GROUP 3 '/oradata/localdb/redo03.log' SIZE 50M BLOCKSIZE 512
-- STANDBY LOGFILE
DATAFILE
'/oradata/localdb/system01.dbf',
'/oradata/localdb/sysaux01.dbf',
'/oradata/localdb/undotbs01.dbf',
'/oradata/localdb/mary01.dbf',
'/oradata/localdb/mary03.dbf',
'/oradata/localdb/mary02.dbf'
CHARACTER SET ZHS16GBK
;
RECOVER DATABASE USING BACKUP CONTROLFILE
ALTER TABLESPACE TEMP ADD TEMPFILE '/oradata/localdb/temp01.dbf' REUSE;
-- 启动到mount状态,恢复system、sysaux、undo、目标表空间
rman target /
RUN {
SET UNTIL SCN 5326681;
RESTORE TABLESPACE mary;
RESTORE TABLESPACE sysaux;
RESTORE TABLESPACE system;
RESTORE TABLESPACE undotbs1;
RECOVER TABLESPACE mary;
RECOVER TABLESPACE sysaux;
RECOVER TABLESPACE system;
RECOVER TABLESPACE undotbs1;
}
RMAN> recover database;
sqlplus / as sysdba
SQL> alter database open resetlogs;
SQL> select name from v$tablespace;
NAME
SYSTEM
SYSAUX
UNDOTBS1
MARY
TEMP
SQL> SELECT count(*) FROM mary.employees;
COUNT(*)
1000
-- 2. 同样通过dblink在生产库拉取数据
INSERT INTO mary.employees
SELECT * FROM mary.employees@to_aux;
COMMIT;
表空间级恢复的速度比全库恢复快得多,但需要注意:异机的文件路径、表空间名称等可能需要调整。可控性也不如全库恢复,实际使用要根据具体情况灵活判断。
2. 3 方案三:FY_RECOVER_DATA存储过程包
FY_RECOVER_DATA是一个专门用于TRUNCATE恢复的工具包,核心原理是构造傀儡表,将被TRUNCATE表的数据块内容嫁接到傀儡对象上,从而实现数据恢复。
支持范围:
• 普通堆表(Heap Table)
• 压缩表(Compression)
• 索引组织表(IOT)
• 分区表
• BLOB/CLOB Store in Row
• 离线恢复(Offline Recovery)
• Oracle 9i及以上,全平台支持
关于数据条数限制:
FY_RECOVER_DATA从数据块层面恢复数据,理论上没有硬性的条数限制。但实际恢复效果和性能受以下因素影响:
• 数据块是否已被覆盖:如果TRUNCATE后有大量写入操作,
原数据块可能已被重用,这部分数据无法恢复
• 表的大小:超大表(几十GB以上)恢复时间较长
• 表空间剩余空间:恢复过程需要在同表空间创建傀儡对象,
需要足够的空间
• 建议:TRUNCATE后尽快执行恢复,减少数据块被覆盖的概率
实战演示:
-- 1. 模拟TRUNCATE误操作
SELECT COUNT(*) FROM mary.employees;
COUNT(*)
1000
TRUNCATE TABLE mary.employees;
Table truncated.
-- 2. 确认数据已丢失
SELECT COUNT(*) FROM mary.employees;
COUNT(*)
0
-- 3. 加载FY_RECOVER_DATA包
SQL> @/soft/FY_Recover_Data.pck
Enter value for files: (这里直接回车即可)
old 30: -- 1. Temp Restore and Recover tablespace & files ---
new 30: -- 1. Temp Restore and Recover tablespace ---
Package created.
Package body created.
-- 4. 执行恢复
SQL> exec fy_recover_data.recover_truncated_table('MARY','EMPLOYEES');
PL/SQL procedure successfully completed.
-- 5. 查看恢复结果
SQL> SELECT COUNT(*) FROM mary.employees$$;
COUNT(*)
1000
-- 6. 将恢复的数据写回原表
INSERT INTO mary.employees SELECT * FROM mary.employees$$;
COMMIT;
-- 7. 验证
SELECT COUNT(*) FROM mary.employees;
COUNT(*)
1000
恢复出来的数据会存储在以原表名加$$后缀的傀儡表中,确认数据无误后再导入原表。整个过程不需要停机,对生产环境的影响极小。
恢复完成后,别忘了清理FY_RECOVER_DATA包自动创建的临时表空间:
SQL> drop tablespace FY_REC_DATA INCLUDING CONTENTS and datafiles;
Tablespace dropped.
SQL> drop tablespace FY_RST_DATA INCLUDING CONTENTS and datafiles;
Tablespace dropped.
2.4 方案四:第三方工具(ODU/DUL/PRM)
如果以上方案都不可用------没有容灾库闪回、没有RMAN备份、也没有FY_RECOVER_DATA包------还可以借助专业的数据恢复工具。需要提醒的是:ODU、DUL、PRM均为SF软件,网上能下载到的试用版通常有数据条数限制,生产环境使用需要购买正式授权。
|----------------------------|---------------------|-----------------|
| 工具 | 特点 | 适用场景 |
| ODU (Oracle Data Unloader) | 直接读取数据文件,不依赖实例,SF工具 | 表空间损坏、数据文件损坏 |
| DUL (Database Unloader) | 类似ODU,SF工具 | 极端场景下的数据抢救 |
| PRM (Parnassus Recovery) | 恢复工具,SF工具 | 支持ASM/RAC/加密表空间 |
本文以Windows环境下的ODU为例,简单演示恢复流程。
ODU恢复流程示例:
-- 1. 将表空间offline
ALTER TABLESPACE mary OFFLINE;
-- 2. 复制数据文件到ODU所在的Windows服务器
-- 3. 进入ODU目录,修改control.txt文件,将文件路径根据实际情况修改
示例:
#ts #fno #rfno filename block_size
5 6 6 D:\workspace\test.dbf 8192
-- 4. 使用ODU加载数据字典
.\odu.exe
ODU> unload dict
-- 5. 扫描目标表空间
ODU> scan extent tablespace 5
-- 6. 导出恢复的数据
ODU> unload table mary.employees object auto
生成MARY_EMPLOYEES.ctl、MARY_EMPLOYEES.txt两个文件
-- 7. 用sqlldr将数据导入
SQL> alter tablespace mary online;
$ sqlldr userid=mary/mary control=MARY_EMPLOYEES.ctl
-- 8. 验证恢复的数据
SELECT COUNT(*) FROM mary.employees;
三、总结与实战建议
3.1 LogMiner最佳实践
-
日常开启最小补充日志,关键时刻不掉链子
-
重要表开启全列补充日志
-
分析时先用时间/SCN/表名精准过滤,避免大量扫描
-
大量日志分析建议在异机库进行,不影响生产
-
定期清理不需要的归档日志,释放空间
3.2 预防TRUNCATE误操作的几条铁律
-
做好用户的权限管理,只授予必要的使用权限
-
容灾库务必开启Flashback Database(关键时刻能救命)
-
RMAN备份策略:全备+增量+归档日志尽量多的保留
-
重要操作前记录关键时间节点
-
生产环境尽量禁止直接TRUNCATE操作,审批 + 审核确认
3.3 数据恢复优先级决策树
遇到数据丢失时,按以下顺序选择恢复方案:
DML误操作(UPDATE/DELETE):
→ 闪回查询(Flashback Query)
→ 闪回表(Flashback Table)
→ LogMiner分析redo日志,获取SQL_UNDO
→ RMAN备份异机恢复
TRUNCATE误操作:
→ 容灾库闪回数据库 + dblink回传(首选)
→ RMAN异机恢复(有备份时)
→ FY_RECOVER_DATA存储过程包
→ 第三方工具(ODU/DUL/PRM)
DROP TABLE:
→ 闪回DROP(Recycle Bin)
→ RMAN备份恢复
→ 第三方工具
好了,今天的内容就到这里。LogMiner、闪回数据库、RMAN异机恢复、FY_RECOVER_DATA、第三方工具------这篇文章把TRUNCATE数据恢复从头到尾扒了个遍。如果你在生产环境中经历过数据恢复的实战,欢迎在评论区分享你的经验和踩过的坑。遇到问题也可以留言,我看到都会回。
我们下期见!
------ 睿 | Oracle性能优化老司机
