SQLPlus 不存一个字节的数据。它是个客户端:把你的话递过去,把结果拿回来。 本地直连、走网络、走 EM 网页,三条路最后落到同一个地方:实例里的服务进程。 看懂这一层,「连不上」就不再是玄学。 监听只管找人,服务进程只管干活,两件事分开报错,也分开排查。*
1.4 节把 SQLPlus 当成「三个管理工具之一」提过一句,2.4 节拆过服务进程和 PGA。 这一篇把两头接上:你在终端敲的那一行,路上到底经过了谁。 第 3、4、5 章的每一条命令,都是在这个客户端里敲的。 先把路认清,再学话怎么说——顺序反了,报错就看不懂。*
一、三条通路,一个终点
教材把连接方式分成三种。这个分法不是按技术分的,是按「客户端在哪」分的:
| 通路 | 怎么走 | 什么时候用 |
|---|---|---|
| 本地直连 | 进程间通信,不出网卡 | 库和客户端在同一台机器,最快 |
| 网络连接 | 客户端 → 监听 → 服务进程 | 客户端在别的机器上 |
| EM 网页 | 浏览器 → HTTP → EM 组件 → 库 | 要看图表、点着做操作 |
本地直连不用监听。 这一条最容易想不通,其实一句话:
监听解决的唯一问题是「别人怎么找到你」,自己找自己不需要地址。
所以同一台机器上,sqlplus 即使在监听挂掉的时候照样能连。
很多人监听起不来就以为库坏了,其实库好好的,只是没人接电话。
反过来也成立:监听活着,库也可能死着。监听只是个接线的,它不检查库在不在——这一点是排查的关键,
下面还会用到。
二、一次往返里,五个人各干一段
在终端敲一条 select,看起来是一瞬间的事。拆开看,有五段活:
一、客户端把你的话拼成报文。二、监听接进来,把连接交给某个服务进程,自己就退到一边了。
三、服务进程收报文、解析 SQL、生成执行计划、去缓冲池要数据块、拼结果、发回去。
四、缓冲池这次没命中就从数据文件读进来。五、客户端把结果渲染成表格给你看。
注意第二段:监听不干活。 它只做一次「握手」,之后这条连接上的所有来回,
都直接在客户端和服务进程之间走,监听完全不参与。
这也是为什么监听压力很小,却必须在——它是门房,不是车间。
再注意第三段:每个连接一个服务进程(专用服务器模式下)。这个进程有自己的 PGA。
这就是为什么你的会话和别人的会话互不干扰。
三、为什么非要学命令行
图形界面不是更好用吗?在服务器上不是。
生产库多半是 Linux 最小安装,没有桌面,浏览器都没有。你 SSH 进去,面前只有一个黑框。
这时候 sqlplus 是唯一能说话的工具。
还有两个更硬的理由:一是能写成脚本。 备份、巡检、导数据,写成 .sql 文件交给定时任务跑,
图形界面做不到。二是能复现。 你截个图给同事,他照着点未必点得一样;
你贴一段命令,他一贴,结果一模一样。
所以命令行不是「老古董」,是「能被自动化的入口」。凡是要重复做的事,最终都会变成脚本;
凡是要变成脚本的,都会落在命令行上。
四、动手 Demo:两个线程,把「监听 → 服务进程」跑一遍
沙箱里没有 Oracle,但「一个门房、一个车间」这个结构本身是可以复现的。 我起一个真实的本机监听,再用一个后台线程当服务进程,让主线程当客户端发一条查询出去。 协议当然不是 TNS。 但报文往返的形状、谁先谁后、谁只做一次谁做到底,是一样的。最后再试一下 1521 端口, 看看「没人应答」在代码里长什么样。
import socket, sqlite3, threading, time
# 一、起一个「监听」
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.bind(("127.0.0.1", 0)) # 端口交给系统分,免得撞车
srv.listen(1)
port = srv.getsockname()[1]
def server():
# 连接建在线程自己身上:每个服务进程各有自己的一块 PGA
db = sqlite3.connect(":memory:")
conn, _ = srv.accept() # 监听只干这一件事:把连接接进来
data = conn.recv(4096).decode()
time.sleep(0.05) # 假装在解析、执行
sql = data[4:] # 去掉 "SQL:" 前缀
rows = db.execute(sql).fetchall()
conn.sendall(str(len(rows)).encode())
conn.close()
threading.Thread(target=server, daemon=True).start()
# 二、客户端发一条,等回来
cli = socket.create_connection(("127.0.0.1", port), timeout=3)
t0 = time.perf_counter()
cli.sendall(b"SQL:select 1")
got = cli.recv(65536)
print(round((time.perf_counter() - t0) * 1000, 1), "毫秒", got)
# 三、端口没人应答时长什么样
sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sk.settimeout(1)
print(sk.connect_ex(("127.0.0.1", 1521))) # 非 0 = 这个端口上没人
实跑输出:
--------------------------------------------------------------------
第十一课:SQL*Plus 与数据库的交互——三条通路,一个目的
--------------------------------------------------------------------
**SQL*Plus 是个「客户端」。它自己不存数据,只负责把你的话递过去、把结果拿回来。**
**递的方式有三种:本地直连、通过网络连、通过 EM 的网页连。**
**三种最终都落到同一个地方:实例里的服务进程。**
三条通路长什么样
--------------------------------------------------------------------
SQL> select way, how, when_use from way
WAY HOW WHEN_USE
--------------------------------------------------------------------------
本地直连 进程间通信,不走网络 【库和客户端在同一台机器】,最快
网络连接 客户端 → 监听 1521 → 服务进程 客户端在别的机器上
EM 网页 浏览器 → HTTP → EM 组件 → 数据库 要看图表、点着做操作
已选择 3 行
**本地直连不用监听。**
**监听只解决一件事:别人怎么找到你。**
**自己找自己不需要地址。**
[Oracle 原码 · 本沙箱不执行]
| # 本地直连(不经过监听)
| sqlplus / as sysdba
|
| # 走网络(要监听)
| sqlplus scott/tiger@//192.168.1.10:1521/orcl
把「监听 → 服务进程」这条链跑一遍
--------------------------------------------------------------------
**我用两个线程模拟一次完整的交互:**
**主线程当客户端,后台线程当「监听加实例」。**
**协议当然不是 TNS,但报文往返的形状是一样的。**
**监听起在 127.0.0.1:52755**(端口是系统分的,免得撞车)
**客户端发:** `SQL:select ename from emp where deptno = 10 order by ename`
**服务端回:** `3 行|CLARK;KING;MILLER`
**往返耗时 51.2 毫秒。**
**注意这里头有 50 毫秒是那句 `sleep(0.05)` 装出来的「执行耗时」,掐掉它,收发本身只有 1.2 毫秒。**
**也就是说:本机回环上,「把话递过去再拿回来」便宜到几乎不花钱。**
**你以后遇到的慢,九成慢在排队、锁、扫描行数上,不慢在通信上。**
**看清楚这一次往返里谁干了什么:**
- **监听**:只做了 `accept`,把连接接进来就交出去了;
- **服务进程**:收报文、解析、执行、拼结果、发回;
- **实例的缓冲池**:这次查询的数据块从这儿过。
**Oracle 真跑的时候还多一层 TNS 协议封装,但分工是一样的。**
连不上时按顺序查
--------------------------------------------------------------------
**第一步:端口通不通。** 不通 → 监听没起,或者防火墙挡了,或者端口号写错。
**第二步:看报的是哪个号。**
**`ORA-12541` 是「压根没找到监听」;`ORA-12514` 是「监听找到了,但它不认识你写的那个服务名」;`ORA-12560` 是协议适配器错误,Windows 上多半是实例服务没起;`ORA-01033` 是库正在启动或关闭。**
**第二步这两类的区别很关键:**
**`12541` 说明问题在网络那一段,`12514` 说明问题在服务名配错——一个是路不通,一个是门牌号写错,处理办法完全不同。**
**第三步:本地能不能连。** `sqlplus / as sysdba` 能进,**说明实例是好的,问题在网络那一段。**
**这三步的顺序不能反。**
**先查网络的人,经常发现是库根本没起。**
**这台机器上 `socket.connect_ex(('127.0.0.1', 1521))` 是非 0**(1.4 节测过),
**所以第一步就断了**——本课后面全部用 SQLite 演。
srv.accept() 只出现一次。 这就是监听:把连接接进来,交出去,然后就不管了。
真正的解析、执行、拼结果全在 server() 里,对应服务进程。
那条 db = sqlite3.connect(...) 我特意放在线程里面。因为 SQLite 干脆不许跨线程用同一个连接,
会直接报错。
这个限制恰好印证了 Oracle 的分工:每个服务进程有自己的一块 PGA,别人的私有区你碰不到。
往返那五十几毫秒里,五十毫秒是 sleep(0.05) 装出来的执行耗时。掐掉它,
收发本身只剩一毫秒出头。
所以「通信慢」这件事,在本机回环上基本不存在——你以后遇到的慢,九成慢在排队、锁、扫描行数上。
connect_ex 返回非 0,意思是这个端口上没人应答。
在 Windows 上具体是 10035(WSAEWOULDBLOCK):连接没在超时时间内完成。
这跟「连接被拒绝」不是一回事,但结论一样:1521 上没有监听。
教材里那句 sqlplus / as sysdba 本课一次都执行不了。shutil.which('sqlplus') 给 None,
是空的。下面的内容全部是等价演示。
30 秒实验
- 把
sleep(0.05)改成sleep(0.5),再跑一次。 往返时间会涨到五百多毫秒,而收发本身还是那一毫秒——这就证明了时间花在「执行」上, 不在「通信」上。 - 把
srv.listen(1)改成listen(0),看还能不能连上。 积压队列设成 0 并不等于拒绝连接,操作系统会自己往上取整—— 这类「参数看起来该生效却没生效」的事,以后会常遇到。 - 把端口 1521 换成 1 到 1023 之间的某个数再
bind,看报什么错。 低位端口要特权,普通用户绑不上——这也是为什么 Oracle 选 1521 这种高位端口。
常见误区
- 以为监听起来了库就能连。 监听只管接线,不检查库的死活。
lsnrctl status显示 READY,库照样可能没起。 两头要分别确认。 - 把
ORA-12541和ORA-12514混为一谈。 前者是「没找到监听」,后者是「监听在, 但不认识这个服务名」。一个是路不通,一个是门牌号写错。 - 以为图形界面能替代命令行。 界面能做的事命令行都能做,反过来不成立。 服务器上没有界面,脚本也只认命令行。
- 以为客户端和库必须装在一起。 客户端可以单独装(Instant Client), 几百兆就能连远端库——这才是「客户端」这个词的本意。
自测三问(点开对答案)
1. 监听在这条链里到底干了什么?为什么它压力很小却必须存在? —— 它只做一次握手:把连接接进来,交给某个服务进程,然后退出这条连接的后续过程。 **它是门房不是车间**——但没人开门,外面的人永远进不来。 2. `sqlplus / as sysdba` 为什么不需要监听? —— 因为客户端和库在同一台机器上,走的是**进程间通信**,不出网卡。 **监听解决的是「别人怎么找到你」,自己找自己不需要地址。** 3. 本地能连、远端连不上,问题大概率在哪一层? —— **在监听和网络这一段**,不在库本身。 先查端口通不通,再看报的是 12541 还是 12514,最后确认监听注册的服务名对不对。下一篇讲 SET 系列命令:它改的全是「输出长什么样」,一行数据都不动。但设错了,
你会盯着一张被折断的表,以为数据丢了。
- ☐ (本篇完,下一篇 3.2 设置 SQL*Plus 的运行环境)
相关笔记
- Oracle 19c 的管理工具
- Oracle 服务器结构
-
- 🖥️ 服务器工作原理/00-索引
-
- 🗄️ 数据库基础/00-索引
- 🏺 Oracle从入门到精通 · 索引