Oracle 扫盲
最近撸了个用于管理数据库的VSCode插件,在测试Oracle类型数据库功能时,发现Oracle跟MySQL有非常的的差异,记录下来方便日后再用到。
一、先纠正几个概念
MySQL 里习惯说”连上服务器,use 一个库”。Oracle 这边,一个实例基本就对应一个数据库,平时理解的”库”,在 Oracle 里其实是”用户(schema)”。
- 每个用户就是一个 schema,用户名底下的表就是他的表
- 没有”跨库 join”这种事,要访问别的用户的表,写
用户名.表名,而且得有权限 - 连接串里不再是简单的 host:port,而是 service_name 或 SID,这块下一小节单独说
表空间(tablespace)也是 Oracle 特有的,简单理解:表的数据最终落在数据文件上,数据文件归属表空间,用户建表时默认放进自己的默认表空间。日常开发不太需要操心,知道有这个东西就行。
二、连接数据库:我第一次就被 SID 搞懵了
工具不缺:官方的 SQL Developer,老牌的 PL/SQL Developer,Navicat、DBeaver 也都支持。我第一次连的时候,IP、端口、账号密码都填好了,发现还剩一个必填项:要在 SID 和服务名之间二选一。当时完全不知道这俩是什么关系,随手选了一个,然后喜提 ORA-12505。
MySQL 的连接很直接,TCP 连上 mysqld 就完事了。Oracle 中间多了一层叫 listener(监听器) 的进程:客户端先连到它(默认端口 1521),再由它把你转交给数据库实例。
然后是 SID 和 service_name 的区别:
- SID 是实例的名字,一个实例一个,相当于”身份证号”
- service_name 是实例对外注册的”服务名”,一个实例可以注册多个;RAC 集群里一个服务名还能同时飘在多个实例上
官方推荐客户端一律连 service_name,但现实是很多老环境、老文档、老交接文档里只给你 SID。连不上就换另一个选项再试,不用纠结原理,先连上再说。
sqlplus 的两种常见连接姿势:
# 简易连接(EZConnect),//host:port/ 后面跟的是 service_name
sqlplus app_user/password@//192.168.1.100:1521/orcl
# TNS 别名:先在 tnsnames.ora 里配好,之后只写别名
sqlplus app_user/password@ORCL_DB
tnsnames.ora 长这样,文件放在 $ORACLE_HOME/network/admin(也可以用环境变量 TNS_ADMIN 指到别处):
ORCL_DB =
(DESCRIPTION =
(ADDRESS = (PROTOCOL = TCP)(HOST = 192.168.1.100)(PORT = 1521))
(CONNECT_DATA =
(SERVER = DEDICATED)
(SERVICE_NAME = orcl)
)
)
图形工具里对应的就是那个二选一:SQL Developer 新建连接时,主机名/端口下面有”服务名”和”SID”两个单选;Navicat、DBeaver 也都有同样的选择,或者直接选 TNS 别名连接。拿不准就先选服务名,报 12505 再换 SID。
连接报错速查,这几个我全遇到过:
| 报错 | 大概率原因 |
|---|---|
| ORA-12541 | 连不上监听器,端口不对或 listener 没起 |
| ORA-12505 | 你按 SID 连的,但监听器那儿没注册这个 SID |
| ORA-12514 | 你按服务名连的,但没注册这个服务名,跟 12505 反着来 |
| ORA-01017 | 用户名或密码错(偶尔其实是连错了库) |
| ORA-28000 | 账号被锁了,找 DBA 解锁 |
实在搞不清该填什么,最快的办法是找 DBA 要一份 tnsnames.ora,或者让他跑一下 lsnrctl status,监听器上注册了哪些服务名一目了然。
还有个工具层面的坎:Navicat 和 PL/SQL Developer 走的是 Oracle 客户端(OCI),第一次用会提示你指定 instant client 的位置(比如那个 oci.dll),得先去官网下一个 instant client 解压好。SQL Developer 和 DBeaver 走 JDBC,自带驱动,装上就能连,图省事可以先从这俩下手。
最后提一句工具里的自动提交开关:DBeaver 工具栏上有个 auto-commit 按钮,Navicat 的设置里也有对应项。第一次连上先看一眼这个开关,它关系到第六节要讲的坑。
三、数据类型对照
| MySQL | Oracle | 备注 |
|---|---|---|
| TINYINT / SMALLINT / INT / BIGINT | NUMBER | 全归 NUMBER,要自己定精度 |
| DECIMAL(p,s) | NUMBER(p,s) | 基本一样 |
| VARCHAR(n) | VARCHAR2(n) | 就用 VARCHAR2,别用 VARCHAR |
| TEXT | CLOB | |
| BLOB | BLOB | 名字相同 |
| DATE | 无纯日期类型 | Oracle 的 DATE 带时分秒 |
| DATETIME / TIMESTAMP | DATE / TIMESTAMP | 要毫秒用 TIMESTAMP |
| JSON | 21c 之前用 CLOB 凑合 | 21c 起才有原生 JSON 类型 |
| 无对应 | NUMBER(1) / CHAR(1) | Oracle 没有布尔类型 |
几个容易懵的点:
- Oracle 的
DATE是带时分秒的,精度到秒;MySQL 的DATE才是纯日期。查出来显示成什么样还跟会话的NLS_DATE_FORMAT有关,想稳就用TO_CHAR自己格式化。 NUMBER不指定精度默认 38 位,随手用问题不大。
四、自增主键:没有 AUTO_INCREMENT
这是从 MySQL 过来最不适应的地方之一。
12c 之前的标准做法是建一个 SEQUENCE,插入时自己取下一个值:
CREATE SEQUENCE seq_user START WITH 1 INCREMENT BY 1;
INSERT INTO t_user (id, name) VALUES (seq_user.NEXTVAL, '张三');
老项目里还有种”全自动”变体:序列配触发器,插入前自动填 id,SQL 里就不用管它了:
CREATE OR REPLACE TRIGGER trg_user_id
BEFORE INSERT ON t_user
FOR EACH ROW
BEGIN
IF :new.id IS NULL THEN
SELECT seq_user.NEXTVAL INTO :new.id FROM dual;
END IF;
END;
/
12c 之后可以用 IDENTITY 列,体验接近 MySQL:
CREATE TABLE t_user (
id NUMBER GENERATED ALWAYS AS IDENTITY,
name VARCHAR2(50)
);
GENERATED ALWAYS 不允许手动插 id;想保留手动插值的余地就用 GENERATED BY DEFAULT AS IDENTITY。
两个小提醒:seq.CURRVAL 只有在同一会话里取过 NEXTVAL 之后才有效,直接查会报错;序列默认带缓存(一般缓存 20 个号),实例重启会跳号——不过 MySQL 的自增值重启后照样跳,所以不用大惊小怪。
五、最大的坑:空字符串就是 NULL
Oracle 里 '' 就是 NULL,不存在”空串”和 NULL 的区别:
INSERT INTO t (name) VALUES (''); -- 这两句效果完全一样
INSERT INTO t (name) VALUES (NULL);
SELECT * FROM t WHERE name = ''; -- 永远查不到,等价于 name = NULL
SELECT * FROM t WHERE name IS NULL; -- 空串也能被这句查出来
所以在 MySQL 里用 WHERE col <> '' 过滤空串的习惯,到 Oracle 直接失效(NULL 参与比较,结果还是 NULL),IN ('') 也永远匹配不到。这个坑第一天准踩,提前打好预防针。
新表设计时建议想清楚:字段”没有值”就统一用 NULL 表达,别像 MySQL 那样有时存空串有时存 NULL,到 Oracle 里这俩已经合并了。
六、事务:默认不自动提交
第二节提过,执行完 INSERT 会提示 “1 row created”,但没提交。MySQL 默认 autocommit,一条 SQL 一个事务;Oracle 反过来,DML 执行完必须自己 COMMIT,否则改动只在你自己的会话里可见,别人看不到,你断开连接还会被回滚。
更刺激的是:DDL 会自动提交。表结构改到一半,前面没提交的 DML 会跟着一起提交掉。习惯了 MySQL 的人很容易在这里翻车。
好在事务控制的零件两边基本同名:SAVEPOINT sp1 打标记,ROLLBACK TO sp1 回到标记处;SELECT ... FOR UPDATE 锁行,两边写法一样,Oracle 里也是查数改数的老搭档。
顺带说一句,Oracle 的读一致性做得很硬,写不阻塞读,这块体感上比 MySQL 更”严”。
七、SQL 写法对照
直接上表,都是高频写法:
| 需求 | MySQL | Oracle |
|---|---|---|
| 当前时间 | NOW() | SYSDATE / SYSTIMESTAMP |
| 分页 | LIMIT 10 OFFSET 20 | OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY(12c+),老版本玩 ROWNUM |
| 判空 | IFNULL(a, b) | NVL(a, b) |
| 条件取值 | IF(c, a, b) / CASE | DECODE(c, 1, ‘a’, 2, ‘b’) 或 CASE |
| 字符串转日期 | STR_TO_DATE | TO_DATE(‘2026-01-01’, ‘YYYY-MM-DD’) |
| 日期转字符串 | DATE_FORMAT | TO_CHAR(sysdate, ‘YYYY-MM-DD’) |
| 日期加减 | DATE_ADD(NOW(), INTERVAL 1 DAY) | SYSDATE + 1 |
| 加减月份 | INTERVAL 1 MONTH | ADD_MONTHS(SYSDATE, 1) |
| 日期差几个月 | TIMESTAMPDIFF(MONTH, a, b) | MONTHS_BETWEEN(b, a) |
| 取零点(去掉时间) | DATE(NOW()) | TRUNC(SYSDATE) |
| 分组拼接 | GROUP_CONCAT(name) | LISTAGG(name, ‘,’) WITHIN GROUP (ORDER BY id) |
| 拼接 | CONCAT(a, b) | a || b |
日期运算值得单独夸一下:Oracle 里日期本质上就是个数,直接加减。SYSDATE + 1 是明天,SYSDATE + 3/24 是三小时后,SYSDATE - 1/1440 是一分钟前(一天 1440 分钟)。写顺手了比 DATE_ADD 省事。
老版本的分页,ROWNUM 是这么个套娃写法(取第 21~30 条):
SELECT * FROM (
SELECT t.*, ROWNUM rn
FROM (SELECT * FROM t_user ORDER BY id) t
WHERE ROWNUM <= 30
)
WHERE rn > 20;
多表关联更新也变了:MySQL 里顺手的 UPDATE a JOIN b SET ...,Oracle 不支持,要么写相关子查询,要么用 MERGE:
MERGE INTO t_target a
USING (SELECT id, qty FROM t_source) b
ON (a.id = b.id)
WHEN MATCHED THEN UPDATE SET a.qty = b.qty
WHEN NOT MATCHED THEN INSERT (a.id, a.qty) VALUES (b.id, b.qty);
顺便,MERGE 就是 upsert,对应 MySQL 的 INSERT ... ON DUPLICATE KEY UPDATE。
零散的几个:
- 查询必须有 FROM。
SELECT 1不行,要写SELECT 1 FROM DUAL。DUAL 是系统自带的单行表,新版本才允许省略,老环境别指望。 - GROUP BY 很严格:SELECT 里的非聚合列必须全部出现在 GROUP BY 里,没有 MySQL 那种宽松模式。
- 多行 VALUES 插入是 23ai 才有的新特性,之前要写
INSERT ALL ... SELECT 1 FROM DUAL,老代码里很常见。 - 老代码里可能见到
WHERE a.id = b.id(+),这是 Oracle 自家的老式外连接写法,等价于 LEFT JOIN,认一下就行。 - TO_DATE 一定要显式给格式串,不然走会话参数,换个环境就报 ORA-01861。
- MySQL 里 UPDATE/DELETE 可以接 ORDER BY 和 LIMIT,Oracle 没这待遇,要限量只能拿 ROWNUM 包一层。
八、大小写:都按大写存
Oracle 的标识符(表名、列名)默认全按大写存储,不带双引号的名字都会被转成大写。想建小写表名得用双引号包起来,然后从此每次查询都得带双引号——千万别这么干,新表老老实实用大写或者干脆不写引号。
MySQL 的表名大小写则跟操作系统和 lower_case_table_names 配置有关,两边的习惯不能直接搬。
九、看表结构、查元数据
MySQL 里 show tables、show create table、information_schema 一把梭。Oracle 换成查数据字典视图,常用命令对号入座:
SHOW TABLES→SELECT table_name FROM user_tables;SHOW DATABASES→ 没有对应物,凑合用SELECT username FROM all_users;SHOW CREATE TABLE→SELECT DBMS_METADATA.GET_DDL('TABLE', 'T_USER') FROM dual;(sqlplus 里先SET LONG 200000,不然输出被截断)DESC t_user→ sqlplus 里原样能用
注释也要单独说:MySQL 的列注释写在建表语句里,Oracle 的注释是独立的 DDL,用 COMMENT ON 加,查注释去 comments 表:
COMMENT ON COLUMN t_user.name IS '姓名';
SELECT column_name, comments
FROM user_col_comments
WHERE table_name = 'T_USER';
其他的按需查:
SELECT index_name FROM user_indexes WHERE table_name = 'T_USER'; -- 索引
SELECT column_name, comments FROM user_tab_comments; -- 表注释
SELECT num_rows FROM user_tables WHERE table_name = 'T_USER'; -- 行数(靠统计信息,不是实时值)
SELECT user FROM dual; -- 当前登录用户
注意 NUM_ROWS 来自统计信息,刚插入完的数据它不知道,要精确就老老实实 COUNT(*)。
前缀分三档:USER_ 是自己的,ALL_ 是有权限看的,DBA_ 是全库的(要 DBA 权限)。
十、查锁、杀会话
排障场景对号入座:MySQL 里 SHOW PROCESSLIST、翻 information_schema 的锁表。Oracle 对应的是 v$ 系列视图:
-- 谁连着、在干嘛(对应 SHOW PROCESSLIST)
SELECT sid, serial#, username, status, machine, sql_id
FROM v$session
WHERE username = 'APP_USER';
-- 谁锁着对象
SELECT * FROM v$locked_object;
干掉某个会话:
ALTER SYSTEM KILL SESSION '1234, 5678'; -- 前面查到的 sid, serial#
v$ 视图要权限,开发账号多半得找 DBA 代劳。图形工具的话,SQL Developer 和 Navicat 都有”会话”面板,能看到的东西一样,还更直观。
十一、执行计划
MySQL 的 EXPLAIN SELECT ... 在 Oracle 里是两步:
EXPLAIN PLAN FOR SELECT * FROM t_user WHERE id = 1;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
sqlplus 里还有个偷懒开关:SET AUTOTRACE ON,之后每条 SQL 顺手把计划和执行统计打出来。
想看真实执行过的计划和实际行数(相当于看真正的执行过程而不是预估):
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL, NULL, 'ALLSTATS LAST'));
统计信息要自己动手时用这个:
EXEC DBMS_STATS.GATHER_TABLE_STATS('APP_USER', 'T_USER');
另外,MySQL 里 hint 多半是应急手段,Oracle 里 /*+ ... */ 是正统优化手段,老系统的 SQL 里到处都是,看到别当成垃圾注释给删了。
十二、导数据:mysqldump 的平替没那么顺手
MySQL 一条 mysqldump 走天下。Oracle 对应的是数据泵 expdp / impdp:
expdp app_user/password@orcl schemas=app_user directory=DATA_PUMP_DIR dumpfile=app.dmp logfile=app.log
impdp app_user/password@orcl schemas=app_user directory=DATA_PUMP_DIR dumpfile=app.dmp
体感差异很大:数据泵是在数据库服务端跑的,那个 directory 是服务端的一个目录对象(一般要 DBA 帮忙建),导出的文件落在数据库服务器上而不是你本机。
想把数据弄到本机,就用 SQL Developer / Navicat 的导出功能(INSERT 脚本或 CSV),小数据量完全够用。第一次找 expdp 的 dump 文件找半天的人不止我一个。
十三、PL/SQL 随手记
Oracle 的 PL/SQL 比 MySQL 的存储过程强一大截,也复杂一大截:DECLARE - BEGIN - END 块结构、强类型、游标、异常处理,还有包(package)这一整套。业务逻辑下沉到数据库的团队,PL/SQL 是绕不过去的。
一个匿名块感受下画风:
DECLARE
v_count NUMBER;
BEGIN
SELECT COUNT(*) INTO v_count FROM t_user;
DBMS_OUTPUT.PUT_LINE('count = ' || v_count);
END;
/
(sqlplus 里要先 SET SERVEROUTPUT ON,不然 PUT_LINE 的输出被吞掉,这个也是必踩项。)
最后碎碎念
- 中文乱码:sqlplus 里查出来中文是问号或乱码,先查服务端字符集
SELECT * FROM nls_database_parameters WHERE parameter = 'NLS_CHARACTERSET';,然后把客户端的 NLS_LANG 环境变量设成配套的值(比如AMERICAN_AMERICA.AL32UTF8)。 - 密码 180 天过期:Oracle 账号默认 profile 180 天过期,到期连接报 ORA-28001/28002,别以为是密码错了瞎折腾。
- RAC:Oracle 的高可用是多实例挂同一份存储,跟 MySQL 主从复制的思路完全不一样,属于运维的活儿,知道有这么个东西就行。
总体感受:Oracle 比 MySQL 严格得多,也”重”得多。SQL 写得不规范直接报错不给过,好处是模糊地带少,坏处是刚上手天天被报错教育。
先记这么多,后面踩到新坑再来补。

