1. 内容整体设计与核心知识梳理
1.1 这个练习到底在练什么
我收到“ORACLE 练习1”这个标题,第一反应是:这大概率是某个数据库初学者或者刚转岗做DBA的朋友,想要系统地把Oracle的基础操作过一遍。但标题太简略了,既没有说清练的是SQL还是PL/SQL,也没提是安装部署还是日常运维。所以我干脆把它拆成一个相对完整的入门实战方案,覆盖从环境搭建到日常开发再到运维管理的全链路基础操作。
在正式开始之前,建议先把Oracle数据库的体系结构搞清楚。很多人一上来就敲SQL,遇到性能问题完全不知道从哪里下手。Oracle的核心架构说白了就是三块:内存结构(SGA和PGA)、进程结构(后台进程)、存储结构(数据文件、控制文件、重做日志文件)。SGA是所有会话共享的内存区域,主要缓存数据块和SQL语句;PGA是每个会话私有的内存区域,用于排序和哈希操作。理解了这个,后面看执行计划、调优的时候才不会一头雾水。
1.2 这个练完能解决什么问题
这套练习覆盖的热搜词特别典型,基本就是大家在日常开发和运维中遇到的高频场景。比如Oracle分页查询怎么做、存储过程怎么写、connect by怎么用、trunc(sysdate)到底返回什么、not exists和not in有什么区别、冷迁移怎么操作、等保检查要跑哪些命令。每一条都是实打实的工作内容。
我举一个实际场景:某天凌晨两点,业务方打电话说报表查询特别慢,你登录数据库一看,某个分页SQL每次要扫全表,耗时8秒。这时候如果你会固定执行计划、知道怎么查看执行计划、懂得用not exists改写逻辑,问题基本二十分钟内就能定位并解决。这套练习的目的就是把这些高频问题的解法变成你的肌肉记忆。
1.3 适合谁来练
如果你是刚入行的开发人员,这套练习能帮你快速上手Oracle日常开发,从最基本的增删改查到存储过程编写,再到常见性能问题的排查思路。如果你是刚转岗的DBA,这套练习涉及的安装部署、冷迁移、等保命令等内容,正好是你上岗头三个月最需要掌握的基础技能。如果你是有几年经验的技术老手,也可以拿这套练习做一次系统复盘,查漏补缺。
练习整体节奏适合每天抽一到两小时,分一周左右完成。基础好的可以跳着看,基础弱的建议按照章节顺序循序渐进。每个章节我都配了实操思路,不光是理论,要真的动手去敲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心SQL技巧实操:那些高频查询写法
2.1 分页查询:三种方案的取舍
Oracle分页绝对是开发者问得最多的问题,没有之一。热搜词里赫然写着oracle分页,确实,太多人在Oracle上套用MySQL的limit语法,结果直接报ORA-00933。Oracle分页经典的做法有三套。
最传统的写法是用ROWNUM嵌套两层子查询:
sql复制SELECT * FROM (
SELECT a.*, ROWNUM rn
FROM (
SELECT emp_id, emp_name, salary
FROM emp
ORDER BY salary DESC
) a
WHERE ROWNUM <= 20
)
WHERE rn >= 11;
这套写法背后的逻辑是:最内层先做排序,中层用ROWNUM截断到第20行,外层再过滤出第11到20行。为什么要套两层?因为ROWNUM是在WHERE子句执行时生成的,它生成于排序之前,如果直接在排序结果上限制ROWNUM范围,拿到的是排序前的行号,结果就错了。
12c以后推荐的写法是FETCH FIRST语句:
sql复制SELECT emp_id, emp_name, salary
FROM emp
ORDER BY salary DESC
OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY;
这个写法更接近其他数据库的习惯,但要注意:OFFSET子句在Oracle内部也是扫描后丢弃,数据量大时一样有性能问题。
还有一种用分析函数ROW_NUMBER()的写法:
sql复制SELECT * FROM (
SELECT emp_id, emp_name, salary,
ROW_NUMBER() OVER (ORDER BY salary DESC) rn
FROM emp
)
WHERE rn BETWEEN 11 AND 20;
个人建议:10万行以内随意,上了百万级,老老实实用ROWNUM嵌套写法,并且一定要在排序列上建立索引。另外,分页查询一定要有稳定的排序字段,如果排序字段有重复值,翻页过程中可能出现数据重复或遗漏。
2.2 connect by start with:树形查询的威力
connect by start with是Oracle独有的层级查询语法,热搜词里有它,说明大家确实经常用到,但用不熟练。它的应用场景非常典型:组织架构树、菜单层级、BOM物料清单、地区行政区划等。
基本语法结构:
sql复制SELECT dept_id, dept_name, parent_id, LEVEL
FROM dept
START WITH parent_id IS NULL
CONNECT BY PRIOR dept_id = parent_id;
START WITH指定根节点的条件,CONNECT BY指定父子关系。PRIOR关键字很重要,PRIOR dept_id = parent_id的含义是:当前行的dept_id作为上一行的父ID来匹配。方向搞反了,树的遍历方向就反了。
实际使用中几个实用的技巧:
使用LEVEL伪列可以控制树的深度,比如只查三级以内的组织架构,就在WHERE条件里加LEVEL <= 3。用CONNECT_BY_ROOT可以在每行中显示根节点的信息,这在做汇总报表的时候特别有用。SYS_CONNECT_BY_PATH可以拼接从根到当前节点的完整路径,比如显示完整的部门路径。
还有一个SIBLINGS关键字,配合ORDER SIBLINGS BY使用,可以在保持树形结构的同时对兄弟节点排序。这个细节经常被忽略,但实际报表中经常用到。
2.3 trunc和sysdate:日期处理的细节陷阱
oracle中的trunc(sysdate)是热搜词,这个函数用起来简单,但坑也不少。trunc(date, format)表示截断日期到指定的精度,第二个参数不写,默认截断到当天零点。
几个常用的截断粒度:
sql复制SELECT TRUNC(SYSDATE) FROM dual; -- 当天零点
SELECT TRUNC(SYSDATE, 'MM') FROM dual; -- 本月1号零点
SELECT TRUNC(SYSDATE, 'YYYY') FROM dual; -- 今年1月1号零点
SELECT TRUNC(SYSDATE, 'HH24') FROM dual; -- 当前小时整点
SELECT TRUNC(SYSDATE, 'IW') FROM dual; -- 本周周一零点
这里特别注意IW和WW的区别:IW是ISO标准周的周一,WW是当年1月1日所在的周作为第一周的起始。很多报表按月、按周统计,这两个格式用错了,统计结果就完全错了。
再补充一个很实用的场景:查某个月的所有数据。正确写法是:
sql复制SELECT * FROM orders
WHERE order_date >= TRUNC(SYSDATE, 'MM')
AND order_date < TRUNC(ADD_MONTHS(SYSDATE, 1), 'MM');
这里用区间查询而不是BETWEEN,目的就是为了让查询能用上order_date上的索引。如果对order_date做了TRUNC函数运算再比较,索引就失效了。
2.4 not exists与not in:别再踩NULL的坑
oracle not exists用法也是热搜词。先说结论:能用not exists就别用not in,除非你非常确定子查询结果中不会有NULL值。
为什么会这样?因为not in的逻辑是:只要子查询结果中的值不等于列表中的任何值,就返回TRUE。但如果子查询中有NULL值,由于NULL参与等值比较时结果是UNKNOWN,整个NOT IN条件就变成了UNKNOWN,最终一行都不返回。这个坑非常隐蔽,数据量小的时候可能侥幸没遇到NULL,上线后某天数据里混入一条NULL,SQL结果直接变成空集。
再看两个写法在实际中的区别:
sql复制-- 不推荐:子查询结果可能有 NULL 时结果异常
SELECT emp_id, emp_name
FROM emp
WHERE dept_id NOT IN (SELECT dept_id FROM dept WHERE status = 'E');
-- 推荐:NULL 不会影响判断
SELECT emp_id, emp_name
FROM emp e
WHERE NOT EXISTS (
SELECT 1 FROM dept d
WHERE d.dept_id = e.dept_id AND d.status = 'E'
);
not exists用的是关联子查询,对每一行emp数据,去dept表里找匹配记录,找不到就算满足条件。它只关心是否存在,不涉及等值比较,所以NULL不影响结果。
还有个性能方面的考量:如果子查询能返回的数据量很大,not exists配合合适的索引,往往执行计划更优。因为not in在有些版本里会对子查询做全量物化,而not exists是逐行去探测。当然也不是绝对,数据量小的时候两者差别可以忽略。
2.5 distinct与字符串判断:日常小坑
oracle distinct用在多列去重的时候,很多人以为它只对某一列去重,其实distinct是对SELECT后所有列的整行组合去重。比如SELECT DISTINCT dept_id, job_id,它去重的是(dept_id, job_id)的组合,而不是单独对dept_id去重。如果想对某一列去重并取其他列的值,应该用分析函数ROW_NUMBER()或GROUP BY,而不是distinct。
oracle判断字符是字母,搜索词里也有。这个场景在做数据校验时很常见。正确方法是用正则表达式REGEXP_LIKE,或者用TRANSLATE函数:
sql复制-- 判断是否纯字母
SELECT CASE WHEN REGEXP_LIKE(emp_name, '^[A-Za-z]+$')
THEN '是字母'
ELSE '包含非字母'
END
FROM emp;
-- 返回字符串最后一个字符
SELECT SUBSTR(emp_name, -1) FROM emp;
SUBSTR的负数偏移量是Oracle的便利特性,-1表示倒数第一个字符,不用先去求LENGTH再减一,这个细节写SQL时能省不少事。
3. 环境准备与安装部署:从零开始搭一套
3.1 在Ubuntu上安装Oracle:别再被坑了
热搜词里有ubuntu安装oracle和linux oracle部署手册单机,说明现在有越来越多人在Linux环境上学习Oracle。Ubuntu上装Oracle比CentOS麻烦,原因在于Oracle官方对Ubuntu的支持一直不积极,标准安装包主要是针对Red Hat系Linux。但这不代表装不上,只是要手动做不少准备工作。
我的推荐路径是这样的:在Ubuntu上不要直接去跑Oracle官方的rpm包,更靠谱的方式是装Docker,然后用容器跑Oracle。我实测过,用docker-compose起一个Oracle 11g或12c的测试环境,整个过程大概二十分钟,比直接在宿主机上折腾各种依赖库轻松太多。基本流程如下:
bash复制# 拉取镜像
docker pull oracleinanutshell/oracle-xe-11g
# 启动容器
docker run -d \
-p 1521:1521 \
-e ORACLE_ALLOW_REMOTE=true \
--name oracle11g \
oracleinanutshell/oracle-xe-11g
容器启动后,通过以下方式连接:
bash复制docker exec -it oracle11g sqlplus system/oracle@//localhost:1521/XE
如果你所在的环境确实需要直接在物理机或虚拟机上安装,比如生产环境规划,那还是走Oracle Linux或者CentOS更稳。毕竟是生产系统,稳定性是第一位,没必要在Ubuntu上跟Oracle较劲。
3.2 CentOS上Oracle的systemd管理
热搜词里有centos oracle systemd,这个场景在企业里确实存在。Oracle在Linux上默认用/etc/init.d/oracle脚本来管理,但在现代Linux上,用Systemd统一管理更符合规范。
给Oracle实例写一个systemd服务单元文件,核心内容大致如下:
ini复制[Unit]
Description=Oracle Database 19c
Requires=network.target
After=network.target
[Service]
Type=forking
Restart=no
ExecStart=/home/oracle/bin/dbstart
ExecStop=/home/oracle/bin/dbshut
User=oracle
Group=oinstall
TimeoutSec=600
[Install]
WantedBy=multi-user.target
写完之后执行systemctl daemon-reload,然后systemctl enable oracle。这里关键是Type=forking,因为dbstart脚本本身会启动多个后台进程然后退出,Systemd要能感知到这个行为才行。如果Type写错了,systemctl start会一直卡住或者误判启动失败。TimeoutSec设长一点也是因为Oracle实例启动确实慢,设太短会被Systemd判定超时并强制结束进程。
3.3 12c删除不干净怎么处理
oracle 12c删除不干净+oracle这个热搜我太有感触了。Windows上装12c结果失败,想卸载重装,结果发现控制面板卸载了,但安装目录还在,注册表还有残留,服务列表里也有一堆Oracle服务,下一次安装各种报错。
解决办法就是手动清理。大概分几步:
第一步,停止所有Oracle服务。打开服务管理器,把所有Oracle开头的服务逐个停止。第二步,删除服务。以管理员身份运行命令提示符,用sc delete逐个删除服务名。注意每个机器的服务名可能不同,要看清楚。
第三步,清理安装目录。默认安装路径形如C:\app\用户名\product,手动删除整个目录。实际上更彻底的做法是删除整个C:\app\用户名目录,因为里面还有Oracle的配置和日志文件。
第四步,清理注册表。在regedit里搜索Oracle关键字,会有很多注册表项,全部删除。这里要注意:有些注册表项是Oracle JDK相关,不一定是Oracle数据库的,如果机器上没有其他Oracle产品,直接全删就行。
第五步,检查环境变量。删除ORACLE_HOME、ORACLE_SID等环境变量,同时把PATH里的Oracle路径清掉。
最后,重启电脑。然后在安装前确认一下磁盘空间和内存,再重新安装。很多人装12c装不上,往往是内存不够,12c的SGA和PGA默认配置加起来很占内存,机器只有4G内存的话很容易安装失败。
3.4 用Navicat和PL/SQL Developer连数据库
工具连接也是热门话题。plsql连接oracle配置和navicat连接oracle都在热搜词里。PL/SQL Developer的连接逻辑比较老套,它本身不包含Oracle的客户端组件,需要额外装一个Instant Client或者完整的Oracle客户端,然后在Tools -> Preferences -> Oracle Connection里配置OCI库路径。
Navicat就省心一些,自带了Oracle连接引擎,基本不需要额外配置。填主机地址、端口1521、服务名或SID,用户名密码一填就能连上。Navicat有两个选项要注意:连接类型选Basic,服务类型选SID还是Service Name,取决于数据库是用服务名对外还是用SID。12c以后默认都用PDB服务名,所以连接PDB时应该选Service Name,填pdb_name而不是ORCL。
4. 运维管理实战:迁移、安全与备份
4.1 11g冷迁移的操作细节
oracle 11g数据库怎么冷迁移,这个热搜说明很多人对迁移流程不熟悉。冷迁移就是在数据库关闭状态下复制数据文件到新机器,相对逻辑迁移(expdp/impdp)来说,物理迁移速度快、保真度高,适用于同版本或跨小版本的迁移。
具体操作流程分五个阶段:
第一阶段:规划和检查。确认源库和目标库的字符集一致,可以用SQL查询:select userenv('language') from dual。确认文件路径结构,尽量减少路径不一致带来的麻烦。
第二阶段:源库记录信息。登录数据库,记录当前DBID、控制文件、数据文件、日志文件的路径。这些信息后面定位问题的时候要用。
sql复制select name from v$controlfile;
select name from v$datafile;
select member from v$logfile;
select dbid from v$database;
第三阶段:干净关闭数据库。shutdown immediate,这里注意绝不能shutdown abort。abort相当于强杀进程,数据文件可能处于不一致状态,切过去之后做恢复会很痛苦。关闭之后,确认监听也已经停了。
第四阶段:整体复制。用scp或者rsync把整个ORACLE_HOME下的oradata目录、参数文件、密码文件、监听配置文件都拷到目标机器上。文件多、体积大,建议在源端打包再传,传输过程中做个校验和检查。
第五阶段:目标库启动。目标机器上需要安装好同版本的Oracle软件,但不需要先建库。把拷贝过来的参数文件(pfile/spfile)和密码文件放到对应的dbs目录,然后直接SQL> startup,Oracle会去读取控制文件,根据控制文件里记录的数据文件路径去加载数据。如果路径不变,启动就能直接成功;如果路径变了,就要通过alter database rename file来重新映射。
冷迁移最常见的问题就是忘了拷贝密码文件,导致启动后无法远程登录,只能本机sqlplus / as sysdba去补救。另外就是忘了改监听配置,listener.ora里如果写的是旧机器的主机名,客户端就连不上。
4.2 等保命令:安全审计自查清单
oracle等保命令是最近企业环境里非常热门的话题。等保2.0检查数据库时,主要关注身份鉴别、访问控制、安全审计、入侵防范、数据完整性等方面。下面这些命令是我在等保检查中被问过无数次的。
账号安全方面:
sql复制-- 查看所有用户的账号状态
SELECT username, account_status, lock_date, expiry_date
FROM dba_users;
-- 查看密码生命周期设置
SELECT * FROM dba_profiles
WHERE resource_name LIKE 'PASSWORD%';
权限控制方面:
sql复制-- 查看用户被授予的系统权限
SELECT grantee, privilege, admin_option
FROM dba_sys_privs
WHERE grantee = 'APP_USER';
-- 查看用户被授予的对象权限
SELECT owner, table_name, privilege, grantee
FROM dba_tab_privs
WHERE grantee = 'APP_USER';
-- 查看用户拥有的角色
SELECT grantee, granted_role
FROM dba_role_privs
WHERE grantee = 'APP_USER';
-- 查看DBA角色用户,这个必须严格管控
SELECT grantee FROM dba_role_privs WHERE granted_role = 'DBA';
审计配置方面:
sql复制-- 查看审计策略
SHOW PARAMETER audit_trail;
-- 推荐值是 DB,表示审计信息写入数据库审计表
-- 如果是 NONE,则会直接不符合等保要求
-- 强制开启登录审计
AUDIT CREATE SESSION WHENEVER NOT SUCCESSFUL;
两条比较重要的等保命令实践建议:密码函数要设置:FAILED_LOGIN_ATTEMPTS建议设5次,PASSWORD_LIFE_TIME建议设90天,但要注意业务系统的实际承受能力。如果误设置了过短的密码有效期导致用户大批量密码过期,那也是运维事故。生产上调整之前先把客户端的连接池参数检查一遍,确保应用侧能自动重连或者及时更新密码。
另外,打开audit_trail之后,FGA(Fine-Grained Auditing)可以用来审计某些特定的敏感表,比如员工薪资表:
sql复制BEGIN
DBMS_FGA.ADD_POLICY(
object_schema => 'HR',
object_name => 'EMP',
policy_name => 'SALARY_POLICY',
audit_column => 'SALARY',
enable => TRUE
);
END;
这样就只对salary列被查询或修改时触发审计,普通查询不记录,避免审计表膨胀太快。
4.3 关闭密码有效期:开发库的救星
oracle关闭密码有效期也是热搜词。Oracle 11g以后默认的DEFAULT profile里设置了PASSWORD_LIFE_TIME为180天,生产环境是为了安全,但开发库里密码到期搞得开发同事天天找DBA解锁,很烦。
开发环境关闭密码有效期,最常见的做法是:
sql复制ALTER PROFILE DEFAULT LIMIT PASSWORD_LIFE_TIME UNLIMITED;
执行完之后,已经逼近过期时间的账号可能还需要手动改一次密码,才能完全避开过期状态:
sql复制ALTER USER 用户名 IDENTIFIED BY 新密码;
这里要特别提醒:生产环境的数据库不要执行这条命令。等保测评如果发现密码有效期是UNLIMITED,直接就是一个不符合项。正确做法是生产环境保留PASSWORD_LIFE_TIME,定期执行密码变更流程,或者用Oracle 12c以后的INACTIVE_ACCOUNT_TIME来自动禁用长期不用的账号。
4.4 Oracle进入ASM命令
oracle进入asm命令这个热搜词,对于接触过RAC或ASM存储的人来说一定不陌生。ASM(Automatic Storage Management)是Oracle的卷管理器和文件系统,安装Grid Infrastructure之后,ASM实例负责管理磁盘组。
以grid用户身份连接到ASM实例:
bash复制su - grid
sqlplus / as sysasm
ASM实例的环境变量里ORACLE_SID一般是+ASM,注意这个加号不能省略。连进去之后,常用命令:
sql复制-- 查看磁盘组状态
SELECT name, state, type, total_mb, free_mb
FROM v$asm_diskgroup;
-- 查看ASM磁盘
SELECT name, path, failgroup, total_mb, free_mb
FROM v$asm_disk;
-- 创建磁盘组
CREATE DISKGROUP DATA EXTERNAL REDUNDANCY
DISK '/dev/sdb1', '/dev/sdc1';
如果只想执行asmcmd命令而不进入SQL环境,可以直接:
bash复制asmcmd
ls -l
du -s data
日常巡检中,ASM最需要关注的是磁盘组的空间余量。v$asm_diskgroup的USABLE_FILE_MB字段表示扣除冗余后实际可用的文件空间(MB),这个数值如果低于TOTAL_MB的10%,就要开始考虑加盘了。
4.5 忘记密码和网络登录相关
进入ASM或数据库实例后,管理员密码遗失也是常见事故。Oracle数据文件本身不加密,可以用操作系统身份认证绕过密码登录。以数据库属主用户(通常是oracle)登录操作系统,然后:
bash复制sqlplus / as sysdba
这个利用的是操作系统级身份认证,只要操作系统用户属于dba组,就可以免密登录并重置sys密码:
sql复制ALTER USER SYS IDENTIFIED BY 新密码;
这里有一条底线:操作系统层的密码保护是最后一道防线,如果操作系统root账号也丢了,Oracle数据本身还可以通过复制数据文件到新机器上重做控制文件的方式来恢复,但那个过程复杂得多,这里就不展开说了。
5. 进阶内容:存储过程、执行计划与数据导入
5.1 存储过程的完整编写与优化
oracle存储过程和oracle存储过程优化都上了热搜,这个方向确实值得好好聊。存储过程在Oracle里写得好,能大幅减少网络开销、提升数据处理效率;写不好,各种ORA错误和性能问题能让人抓狂一周。
我建议先写一个完整的存储过程,把常用的要素都包含进去:游标、异常处理、动态SQL、事务控制。
sql复制CREATE OR REPLACE PROCEDURE sp_stat_orders (
p_start_date IN DATE,
p_end_date IN DATE,
p_result OUT NUMBER
) IS
CURSOR cur_order IS
SELECT order_id, order_amount
FROM orders
WHERE order_date BETWEEN p_start_date AND p_end_date
AND status = 'PAID';
v_total NUMBER := 0;
v_order_count NUMBER := 0;
v_sql VARCHAR2(500);
v_user_name VARCHAR2(30);
BEGIN
-- 参数合法性检查
IF p_start_date IS NULL OR p_end_date IS NULL THEN
RAISE_APPLICATION_ERROR(-20001, '日期参数不能为空');
END IF;
IF p_start_date > p_end_date THEN
RAISE_APPLICATION_ERROR(-20002, '开始日期不能晚于结束日期');
END IF;
-- 游标循环处理
FOR rec IN cur_order LOOP
v_total := v_total + rec.order_amount;
v_order_count := v_order_count + 1;
END LOOP;
-- 动态SQL示例:写入统计日志表(表名用参数化)
v_sql := 'INSERT INTO stat_log(log_table, log_date, total_amount)
VALUES(''ORDERS'', SYSDATE, :1)';
EXECUTE IMMEDIATE v_sql USING v_total;
-- 获取当前操作用户
SELECT USER INTO v_user_name FROM dual;
DBMS_OUTPUT.PUT_LINE('统计单数: ' || v_order_count);
DBMS_OUTPUT.PUT_LINE('金额合计: ' || v_total);
p_result := v_total;
COMMIT;
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('没有找到数据');
p_result := 0;
WHEN OTHERS THEN
ROLLBACK;
DBMS_OUTPUT.PUT_LINE('错误代码: ' || SQLCODE);
DBMS_OUTPUT.PUT_LINE('错误信息: ' || SQLERRM);
RAISE;
END sp_stat_orders;
写存储过程时常见的坑:
ORA-01403是NO_DATA_FOUND,SELECT INTO没取到数据就会报这个。如果你预期的场景是可能没数据,就要用游标或者先COUNT一下,不要直接SELECT INTO。
动态SQL一定要用绑定变量(上面的:1),不要字符串拼接。一方面防SQL注入,另一方面绑定变量的硬解析问题也避免掉了,性能差异在并发高的场景下可能是数量级的差别。
不要在存储过程里毫无节制地COMMIT。事务的原则是:要么一起成功,要么一起失败。在循环里每处理一行就COMMIT一次,虽然单看每条数据没问题,但一旦中间出错,前面已提交的数据无法回滚,业务数据就会出现部分完成的状态。
存储过程优化方面,第一要素是避免游标循环里的逐行SQL操作。每执行一次SQL就是一次昂贵的数据库调用,如果循环1万次,就产生了1万次SQL执行,耗时无法接受。能用集合操作的就用集合,能用一条SQL完成的就不要拆成多条。
5.2 固定执行计划:SQL性能救急方案
oracle固定执行计划是高手和普通运维的分水岭。很多时候一条SQL的统计信息不准,执行计划走错了索引,SQL就慢如蜗牛。但生产环境又不能随便改SQL(权限受限,改动风险大),这时候固定执行计划就能救急。
固定执行计划有几种方法,我推荐的是SPM(SQL Plan Management)方式。SPM是从11g开始提供的机制,它可以把一条SQL的某个执行计划固定下来,即使统计信息变化或数据库升级,也不会改变执行路径。
步骤是:先找到目标SQL的SQL_ID,用DBMS_XPLAN查看当前的执行计划,然后把执行计划捕获进SPM并固定:
sql复制-- 1. 从共享池找到 SQL_ID
SELECT sql_id, sql_text
FROM v$sql
WHERE sql_text LIKE '%特殊的关键字%';
-- 2. 查看执行计划
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('&SQL_ID', NULL, 'ALLSTATS LAST'));
-- 3. 加载执行计划到 SPM 基线
DECLARE
l_plan_name VARCHAR2(30);
BEGIN
l_plan_name := DBMS_SPM.LOAD_PLANS_FROM_CURSOR_CACHE(sql_id => '&SQL_ID');
-- 返回的 l_plan_name 就是生成的SQL Plan Baseline的名称
-- 默认情况下,新加载的计划会以“固定”状态保存
END;
/
如果想让Oracle只使用这个计划而不用别的,可以对基线设置:
sql复制SET SERVEROUTPUT ON
DECLARE
l_count NUMBER;
BEGIN
l_count := DBMS_SPM.ALTER_SQL_PLAN_BASELINE(
sql_handle => '目标SQL_Handle',
plan_name => '目标Plan_Name',
attribute_name => 'FIXED',
attribute_value => 'YES'
);
DBMS_OUTPUT.PUT_LINE('固定了 ' || l_count || ' 个计划');
END;
/
SPM的核心机制是:把SQL文本哈希成SQL_HANDLE,SQL_HANDLE下面挂多条执行计划。当优化器生成新的候选计划时,Oracle会先与基线中的计划做比较,如果性能没有显著提升,就继续沿用基线计划。
5.3 impdp日志不完整的原因与解决
oracle 19c impdp日志记录不完整是数据泵导入时的常见问题。很多人在用impdp导入数据后,想查看导入日志,发现日志文件里只有开头一小段记录,后面就断掉了。这通常和以下几个因素有关:
NOLOGGING模式的影响。数据泵在很多操作中默认用NOLOGGING或最小日志记录,尤其是直接路径导入,这样日志文件里自然不会有详细记录。
替代方案:数据泵日志不完整时,不用指望日志文件本身,应该在控制终端或后台日志里查看完整输出,或者重新执行impdp时加上LOGTIME=ALL参数:
bash复制impdp system/密码 directory=DATA_PUMP_DIR dumpfile=backup.dmp \
logfile=import_full.log \
LOGTIME=ALL \
TABLE_EXISTS_ACTION=SKIP
LOGTIME=ALL会在终端上实时打印每个对象的导入进度,这个信息比日志文件全得多。如果仍然希望日志文件完整,检查用户权限:dir对象是否授予了读写权限,同时确保磁盘空间足够,日志文件写满也会中断。
5.4 SQL*Loader快速批量导入
c# oracle sqlloader用法上了热搜,说明有人在用C#调用Oracle的SQLLoader做数据导入。SQL*Loader是Oracle自带的批量加载工具,读取文本文件到表里,速度远超逐行INSERT。
它的核心是控制文件(.ctl)。举个例子:把订单文件orders.txt导入到orders表:
sql复制LOAD DATA
INFILE 'orders.txt'
APPEND INTO TABLE orders
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
(
order_id CHAR(20),
order_date DATE 'YYYY-MM-DD HH24:MI:SS',
customer_name CHAR(100),
amount DECIMAL EXTERNAL
)
在C#里调用的思路有两种。一是用Process类启动sqlldr进程:
csharp复制Process p = new Process();
p.StartInfo.FileName = "sqlldr";
p.StartInfo.Arguments = "userid=user/pass@db control=orders.ctl log=orders.log";
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.UseShellExecute = false;
p.Start();
p.WaitForExit();
另一种是先通过Oracle的BULK COPY或数组绑定做批量插入,并发分批执行。IT团队经常用前者处理几百MB的文本,后者适合处理逻辑复杂、需要程序后处理的场景。
SQL*Loader有几个小技巧:SKIP=1跳过表头行;DIRECT=TRUE启用直接路径加载,速度提升明显,但会对表加排他锁;ROWS=5000可以控制批量提交的规模。还有个大坑:如果表上有外部索引,直接路径加载后索引会变为UNUSABLE状态,需要重建索引:
sql复制ALTER INDEX idx_orders_amount REBUILD;
6. 常见问题与排查技巧实录
6.1 高频报错速查表
练Oracle期间,大家遇到最多的错误基本就那几个,我把高频报错和处理方式整理成了一张表:
| 错误代码/现象 | 出现场景 | 解决方案 |
|---|---|---|
| ORA-12541 | 监听没启动或网络不通 | lsnrctl status检查监听;确保防火墙放行1521端口 |
| ORA-12154 | TNS别名无法解析 | 检查tnsnames.ora里的服务名是否写对,本地命名是否正确 |
| ORA-01017 | 用户名密码错误 | 确认账号密码;密码大小写敏感;也可能账号被锁 |
| ORA-28000 | 账号被锁 | 用dba账号执行alter user xx identified by xx;开发环境可设置UNLIMITED |
| ORA-00904 | 列名无效 | 检查列名拼写、大小写,双引号引起来的列名对大小写敏感 |
| ORA-00942 | 表或视图不存在 | 确认当前schema是否正确,对象是否在别的用户下,需要授权 |
| ORA-01438 | 数值溢出 | 数据超出列定义精度,调整列类型或检查数据 |
| ORA-00054 | 资源正忙,需要加锁 | 有未提交事务锁住对象,查找阻塞会话并kill |
| ORA-01555 | 快照过旧 | 数据量大且UNDO空间不够,扩大UNDO_TABLESPACE或优化查询 |
| ORA-01653 | 表空间无法扩展 | 增加数据文件或启用自动扩展 |
| ORA-02049 | 分布式事务超时 | 检查两阶段事务的协调器,等待超时后手动清理 |
还有一条ORA-01555值得多说两句:Undo保留时间不够,查询走了太长的历史块版本,而undo信息被覆盖了。解决思路一是调大UNDO_RETENTION,二是把大查询拆成小批次跑。单纯加数据文件可能治标不治本,要看清是不是SQL脚本跑太久了。
6.2 12c以后用Dragonwell还是Oracle JDK
oracle和dragonwell对比oracle这条热搜词,让我有点惊讶但也在意料之中。Oracle数据库软件本身需要JDK,许多客户为了成本,开始考虑用国内的开源JDK替代Oracle JDK。
Oracle JDK和Dragonwell JDK的主要区别在于:Oracle JDK是商业版,在部分场景下需要授权许可;Dragonwell是阿里巴巴基于OpenJDK的发行版,免费开源,针对Java大规模场景做了JIT和GC优化。如果只是运行Oracle自带的工具和SQL开发,两者在使用上基本没有差别。
| 维度 | Oracle JDK | Dragonwell |
|---|---|---|
| 来源 | Oracle官方 | Alibaba开源 |
| 授权 | 商业授权(部分版本) | 免费开源 |
| 稳定性 | 商用级,经多家厂商验证 | 针对互联网大并发场景优化 |
| 更新频率 | 跟随官方发布 | 定期更新,社区活跃 |
| Oracle配套 | 官方推荐 | 社区实践验证 |
我的建议是:测试环境用什么JDK都行,生产环境还是保守一些,优先用Oracle官方JDK。毕竟Oracle数据库是核心系统,配套组件越官方,后续遇到问题越好跟原厂沟通。真要在生产环境换Dragonwell,需要先在测试环境完整回归一遍,重点验证Oracle的DBCA、监听器、RMAN等工具是否正常。
6.3 VirtualBox虚拟机迁移和账号共享
oracle virtualbox系统可以迁移到别的硬盘吗也是热搜词。这个答案是肯定的。VirtualBox的虚拟机迁移,核心就两步:导出或复制虚拟硬盘,然后在目标机器上导入。官方推荐方式是用导出向导,文件菜单 -> 导出虚拟电脑,会生成一个OVA文件,然后在另一台机器上导入。也可以手动复制整个虚拟机的.vdi文件,新建虚拟机时选择已有磁盘文件。
这里有一个容易踩的坑:直接复制.vdi文件到新机器,如果原虚拟机的网络配置里有固定的MAC地址绑定,复制后的虚拟机网卡MAC会变,Oracle监听或者网络配置可能会失联。建议在复制前先记录原机的网络配置,复制后手动调整新机的网卡设置或Oracle监听配置。
还有一个相关热搜:oracle账号共享。这里指的可能是Oracle官网的账号共享,也可能是数据库账号共享。数据库账号共享这个问题,我还是建议尽量减少共享账号。给每个开发人员单独的数据库账号,配合角色来控制权限,后面审计的时候才能追踪到人。等保检查时共享账号也是不符合项,平时省事,检查时麻烦。
6.4 CLOB字段处理与大数据量文本
oracle clob上了热搜。CLOB字段在存储大文本(比如日志、文章正文、JSON字符串)时非常有用,但日常操作起来坑也不少。
第一个坑是CLOB不能直接用distinct、group by、order by。报错ORA-00932:不一致的数据类型。解决办法是用DBMS_LOB.SUBSTR(clob_column, 4000, 1)把CLOB截断成VARCHAR2再参与操作,但只能截断处理,不能拿到全量文本去比较。
第二个坑是长度限制。PL/SQL中CLOB变量可以很大,但VARCHAR2变量的上限是32767字节。做CLOB拼接时如果直接给CLOB变量赋值,建议都用DBMS_LOB.APPEND:
sql复制DECLARE
l_content CLOB;
l_chunk VARCHAR2(4000);
BEGIN
FOR i IN 1..100 LOOP
l_chunk := '第' || i || '段文本';
DBMS_LOB.APPEND(l_content, l_chunk);
END LOOP;
-- 再写入表
END;
第三个坑是CLOB在SQLPlus里的显示问题。直接select clob_column from table,SQLPlus默认只显示第一行开头的一部分。解决办法是用SET LONG 2000000,设置显示的字节数。PL/SQL Developer和Navicat可以直接查看CLOB全量内容,图形化工具确实省心。
还有个关于dual表的问题:oracle中dual最多存多大。这个热搜有点偏门。dual表本身只是Oracle内置的单行单列表,它的大小不是你该关心的。实际问题是,如果你select的字符串长度超过了VARCHAR2的限制,报的错就和dual无关。在SQL层,SELECT '很长的字符串' FROM dual,字符串超过4000字节就会报ORA-01704(字符串文字过长)。解决办法是把长文本拆成多段,用函数拼接,或者直接存CLOB字段。
7. 实操总结与后续扩展
我在实际练习Oracle的过程中,最深的感受是:这套东西入门不难,但每个知识点都有值得深挖的角落。分页查询、connect by、存储过程、执行计划,每一个用熟练了都能解决真实工作中的大麻烦。热搜词里那些问题,几乎都是大家在工位上真正卡过壳的地方。比如等保命令那个话题,如果不是被审计追着要过检查,谁会专门去研究dba_users和dba_role_privs的查询方式?再比如固定执行计划,如果没有真正被SQL性能折磨到半夜,也很难理解SPM的价值所在。
这套练习最后再分享一个小技巧:建议新手在练习时,所有SQL都养成用绑定变量的习惯,不要直接拼字符串。哪怕是在练习环境,一开始就养好习惯,后面迁移到生产环境就不用回炉重造。同时把常用的查询脚本攒成一个自己的工具箱,执行计划相关的、表空间占用相关的、会话锁相关的,以后不管换到哪个公司,这套脚本都能复用,效率提升立竿见影。
如果后续还想深入,建议按这几个方向扩展:RAC集群的部署与管理,Data Guard的搭建与故障切换,AWR报告的分析与调优,以及OGG(Oracle GoldenGate)的实时数据同步。OGG是热搜词里的高频词,在多个系统之间的数据实时同步场景中几乎是标配。不过那些都属于进阶内容了,先把这套基础练习吃透,后面自然水到渠成。
