🏺 第3章 SQL*Plus 命令
📄 一句话概括

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 秒实验

  1. 把 sleep(0.05) 改成 sleep(0.5),再跑一次。 往返时间会涨到五百多毫秒,而收发本身还是那一毫秒——这就证明了时间花在「执行」上, 不在「通信」上。
  2. 把 srv.listen(1) 改成 listen(0),看还能不能连上。 积压队列设成 0 并不等于拒绝连接,操作系统会自己往上取整—— 这类「参数看起来该生效却没生效」的事,以后会常遇到。
  3. 把端口 1521 换成 1 到 1023 之间的某个数再 bind,看报什么错。 低位端口要特权,普通用户绑不上——这也是为什么 Oracle 选 1521 这种高位端口。

常见误区

  1. 以为监听起来了库就能连。 监听只管接线,不检查库的死活。 lsnrctl status 显示 READY,库照样可能没起。 两头要分别确认。
  2. 把 ORA-12541 和 ORA-12514 混为一谈。 前者是「没找到监听」,后者是「监听在, 但不认识这个服务名」。一个是路不通,一个是门牌号写错。
  3. 以为图形界面能替代命令行。 界面能做的事命令行都能做,反过来不成立。 服务器上没有界面,脚本也只认命令行。
  4. 以为客户端和库必须装在一起。 客户端可以单独装(Instant Client), 几百兆就能连远端库——这才是「客户端」这个词的本意。
自测三问(点开对答案) 1. 监听在这条链里到底干了什么?为什么它压力很小却必须存在? —— 它只做一次握手:把连接接进来,交给某个服务进程,然后退出这条连接的后续过程。 **它是门房不是车间**——但没人开门,外面的人永远进不来。 2. `sqlplus / as sysdba` 为什么不需要监听? —— 因为客户端和库在同一台机器上,走的是**进程间通信**,不出网卡。 **监听解决的是「别人怎么找到你」,自己找自己不需要地址。** 3. 本地能连、远端连不上,问题大概率在哪一层? —— **在监听和网络这一段**,不在库本身。 先查端口通不通,再看报的是 12541 还是 12514,最后确认监听注册的服务名对不对。

下一篇讲 SET 系列命令:它改的全是「输出长什么样」,一行数据都不动。但设错了, 你会盯着一张被折断的表,以为数据丢了。

相关笔记