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

数据库只管给数据,怎么排得好看是客户端的事。 SQLPlus 给了两件工具:COLUMN 管列(宽度、表头、对齐、数字格式), TTITLE/BTITLE 管页(页眉、页脚)。 这一手的价值不在「好看」,在服务器上只有终端的时候,它是唯一能把结果输出成人能看的样子*。

📝 本章和前面几章的关系

3.2 节的 SET 是全局的(整张表怎么摆),这一节的 COLUMN 是逐列的(这一列怎么摆)。 两者配合,才拼得出一张像样的报表。 数字格式串这套东西,4.4 节讲 TO_CHAR 时会再出现一次。 它们本来就是同一套模板语言——在这里学会,到那里就只是换了个函数名。

一、COLUMN:一列一列地调

SET LINESIZE 是「整行多宽」,COLUMN 是「这一列多宽、表头叫什么、怎么对齐、数字怎么排」。 粒度完全不同。

参数 作用 例子
HEADING 改表头文字 HEADING '姓名'
FORMAT 定宽度和数字格式 FORMAT a10 / FORMAT 999,990.00
JUSTIFY 左中右对齐 JUSTIFY CENTER
NOPRINT 这一列不打印 NOPRINT
NULL 空值显示成什么 NULL '无'

a10 里的 a 是「字符串(alpha)、宽 10」。 数字用另一套: 999,990.00 表示「最多六位整数、带千分位、两位小数」。 NOPRINT 值得单独提一句: 有时候你按某列排序,但不想把它打出来, COLUMN deptno NOPRINT 就能让它在结果里隐身,排序照样生效。

二、数字格式串:9 与 0 的区别

这两个字符的区别只有一句话:9 是「有数就显示、没数留空」,0 是「没数就补零」。 FORMAT 999990 打 2975 得到 2975;FORMAT 099999 得到 002975。 补零在编号、流水号上很有用,在金额上就是灾难——没人想看到 00800.00 元。 FM 前缀去掉前导空格。 Oracle 的数字格式默认按位补齐,短数字前面会留一串空格用来右对齐; FM999990 就把这些空格去掉。导数据时几乎一定要加 FM,否则每个字段前面都是一堆空格。 TO_CHAR(2975, '999,990.00') 用的是同一套格式串,结果一模一样。 区别只在位置:COLUMN 是「显示时排」,TO_CHAR 是「取出来就是字符串」。 后者会把结果变成文本,排序规则也跟着变——这一点 4.4 节会细讲。

三、TTITLE 与 BTITLE:给每页加头尾

TTITLE 是页眉,BTITLE 是页脚,都在顶部或底部打固定文字。 三个位置:LEFT、CENTER、RIGHT。 多行用 - 续行(这不是减号,是 SQLPlus 的续行符), SKIP 1 是「空一行」。 拼起来就是一张带抬头、带页码的报表。 SQL.PNO 是内置变量,值就是当前页码。 所以 BTITLE CENTER '第 ' SQL.PNO ' 页' 会真的打出「第 1 页」「第 2 页」。 这才是 PAGESIZE 存在的意义:不分页,页码就没意义。 注意 TTITLE 是「每页都打」。 设一次,后面所有输出都带着它, 直到 TTITLE OFF。* 很多人设完忘了关,后面查什么都顶着那个抬头,很尴尬。

四、BREAK ON:分组断行

BREAK ON deptno SKIP 1 的意思是:按 deptno 分组,值一变就空一行。 报表里「一个部门一段」的效果就是这么来的。 配合 COMPUTE 还能打小计: BREAK ON deptno SKIP 1 加 COMPUTE SUM OF sal ON deptno,每个部门末尾会多一行合计。 但 BREAK 有个前提:数据必须按那一列排好序。没 ORDER BY 的话,deptno 会 10、30、 10 地来回跳,BREAK 就断得莫名其妙。 顺序永远是先 ORDER BY,再 BREAK。

五、为什么现在很少人这么写

因为格式属于展示层。 把「列宽 12、千分位、页码」写进 SQL 脚本, 等于把展示逻辑焊死在数据层上。 换一个客户端,这些设置全都不认。 网页、Excel、报表工具,没有一家会去读你的 COLUMN 设置。 所以现代做法是:SQL 只负责取数据,排版交给客户端。 但在运维场景下它依然值钱。 服务器上只有终端,没有图形界面, spool 加 column 是唯一能把结果输出成人能看的样子。 会这一手,远程排障时能直接把结果贴进邮件,不用来回导。 我的建议是:知道它、会用、但别往生产脚本里塞。 排查问题时临时设一设,导出数据时清干净——这两件事分清楚就行。

六、动手 Demo:把一组查询排成一张报表

这一段把 COLUMN 和 TTITLE 的效果整块演出来: 列宽固定、表头改名、金额带千分位、页眉居中、页码右对齐。 然后用同一组格式串,对照 9 与 0 的差别。注意这张表里的每个数字,都是从库里查出来现算的。

import sqlite3
c = sqlite3.connect(":memory:")
c.execute("CREATE TABLE emp (ename TEXT, sal REAL, hiredate TEXT, deptno INT)")
c.executemany("INSERT INTO emp VALUES (?,?,?,?)", [
    ('SCOTT', 3000.0, '1987-04-19', 20),
    ('FORD', 3000.0, '1981-12-03', 20),
    ('JONES', 2975.0, '1981-04-02', 20),
    ('ADAMS', 1100.0, '1987-05-23', 20),
    ('SMITH', 800.0, '1980-12-17', 20),
])

# COLUMN ename HEADING '姓名' FORMAT a10 的等价物:定宽 + 改表头
# COLUMN sal HEADING '月薪' FORMAT 999,990.00 的等价物:千分位 + 两位小数
W = 40
print('RESEARCH 部门薪资表'.center(W))
print('=' * W)
print('姓名'.ljust(14) + '月薪'.ljust(14) + '入职日期'.ljust(12))
print('-' * W)
for ename, sal, hd in c.execute("select ename, sal, hiredate from emp "
                               "where deptno = 20 order by sal desc"):
    print(ename.ljust(14) + format(sal, ',.2f').ljust(14) + hd.ljust(12))
print('-' * W)
print('第 1 页'.rjust(W))          # BTITLE CENTER '第 ' SQL.PNO ' 页'

# 同一组格式串,换成 Python 的格式写法对照
for fmt, v in [('999,990.00', 2975.0), ('999990', 2975.0), ('0999.9', 2975.0)]:
    print(fmt, '->', v)

实跑输出:

--------------------------------------------------------------------
第十四课:格式化查询结果——COLUMN 与 TTITLE/BTITLE
--------------------------------------------------------------------

  **这一节是「把查询变成报表」。**
  **数据库只管给数据,怎么排得好看是客户端的事。**
  **SQL*Plus 给了两个工具:`COLUMN` 管列(宽度、表头、对齐、格式),`TTITLE`/`BTITLE` 管页(页眉、页脚)。**


  COLUMN:一列一列地调
  --------------------------------------------------------------------

  [Oracle 原码 · 本沙箱不执行]
  | COLUMN ename  HEADING '姓名'   FORMAT a10
  | COLUMN sal    HEADING '月薪'   FORMAT 999,990.00
  | COLUMN hiredate HEADING '入职日期' FORMAT a10

  **三个参数最常用:**
  **`HEADING` 改表头文字、`FORMAT` 定宽度和数字格式、`JUSTIFY` 定对齐。**
  **`a10` 里的 `a` 是「字符串、宽 10」;`999,990.00` 是「数字、带千分位、两位小数」。**
            RESEARCH 部门薪资表
  ========================================
  姓名          月薪          入职日期
  ----------------------------------------
  SCOTT         3,000.00      1987-04-19
  FORD          3,000.00      1981-12-03
  JONES         2,975.00      1981-04-02
  ADAMS         1,100.00      1987-05-23
  SMITH         800.00        1980-12-17
  ----------------------------------------
                                   第 1 页

  **上面这张表就是 `COLUMN` 加 `TTITLE` 的产物:**
  **列宽固定、表头改名、金额带千分位、页眉页脚居中。**

  数字格式差在哪
  --------------------------------------------------------------------
  **同一个数,三种格式:**
  `FORMAT 999,990.00` → **2,975.00**
  `FORMAT 999990` → **2975**
  `FORMAT 0999.9` → **2975.0**
  **`9` 是「有数就显示、没数留空」,`0` 是「没数就补零」,`FM` 去掉前导空格。**
  **`TO_CHAR` 用的是同一套格式串**(4.4 节会再讲一次)——**它们是同一件事:把数字按模板排版。**

  TTITLE 与 BTITLE:给每一页加头尾
  --------------------------------------------------------------------

  [Oracle 原码 · 本沙箱不执行]
  | TTITLE LEFT '企业人事系统' -
  |         CENTER '员工薪资明细' -
  |         RIGHT '2026-10-30' -
  |         SKIP 1
  | BTITLE CENTER '第 ' SQL.PNO ' 页'
  | COLUMN ename HEADING '姓名' FORMAT a10
  | COLUMN sal HEADING '月薪' FORMAT 999,990.00
  | BREAK ON deptno SKIP 1

  **`SQL.PNO` 是「当前页码」,`SKIP 1` 是「空一行」,`BREAK ON` 是「按某列分组,变了就空一行」。**
  **这一套在 SQL*Plus 里能拼出相当像样的报表**——**但只在终端里。**
  **要 PDF、要图表,得靠 EM 或者第三方报表工具。**

  为什么现在很少人这么写
  --------------------------------------------------------------------
  **因为格式属于展示层。**
  **把「列宽 12、千分位、页码」写进 SQL 脚本,等于把展示逻辑焊死在数据层。**
  **换一个客户端(比如网页),这些设置全都不认。**
  **所以现代做法是:SQL 只负责取数据,排版交给客户端。**
  **但运维场景下它很有用:**
  **服务器上只有终端,没有图形界面,`spool` 加 `column` 是唯一能把结果输出成人能看的样子。**
  **会这一手,远程排障时省很多事。**

这张表就是 column 加 ttitle 的产物: 列宽固定、表头改成中文、金额带千分位、页眉居中、 页脚右对齐。数据库一点都没参与排版。 注意 3,000.00 和 2,975.00 的宽度一样。 因为 FORMAT 定死了宽度, 数字不会把列撑开——这正是报表要的效果:上下对得齐。 800.00 前面没有 0,也没有被补成 000800.00。因为 999,990.00 里全是 9, 9 的规矩是「没数就留空」。 换成 0999,990.00,它立刻变成 000800.00。 页脚那个「第 1 页」是右对齐的。 对应 BTITLE RIGHT 或者 SQL.PNO 的位置—— 页码右对齐是印刷报表的惯例,因为翻页时手指在右下角。 这里只有五条数据,看不出分页。 但 PAGESIZE 一设成个位数,这份报表就会自己翻页, 每页都重打抬头和页码——这就是 TTITLE 加 SQL.PNO 的价值。

30 秒实验

  1. 把 FORMAT 999,990.00 换成 FORMAT 0999,990.00,看 800.00 变成什么。 这就是 9 与 0 的唯一区别:一个留空,一个补零。
  2. 给表加一列 comm(全是空值),再 COLUMN comm NULL '无'。 空值会显示成「无」,而不是一片空白——报表里空白和零是两件事,必须能分清。
  3. 同时设 TTITLE 和 BREAK ON deptno SKIP 1,把两个部门的数据混在一起查。 注意 BREAK 要生效,必须先 ORDER BY deptno——不排序,它会断得毫无规律。

常见误区

  1. 以为 COLUMN 改的是数据。 它只改显示。 sal 存的是 2975,3,000.00 是渲染出来的样子,取出来算还是原值。
  2. 忘了 TTITLE OFF。 设一次之后每份输出都顶着那个抬头,直到你关掉它或者退出会话。
  3. 不 ORDER BY 就 BREAK ON。 数据没排序,分组边界就是乱的, 断行位置毫无意义,小计也跟着错。
  4. 把报表格式写进生产脚本。 换客户端就不认, 而且把展示逻辑和数据逻辑搅在一起,以后谁都不敢改。 排查时临时用,交付时清干净。
自测三问(点开对答案) 1. `FORMAT 999990` 和 `FORMAT 099999` 打同一个数,差在哪? —— **`9` 是「没数就留空」,`0` 是「没数就补零」。**2975 用前者得 `2975`,用后者得 `002975`。 **金额上用 `9`,流水号上用 `0`。** 2. `TTITLE` 里的 `SQL.PNO` 是什么?为什么它需要 `PAGESIZE` 配合? —— 它是**当前页码**这个内置变量。 **不分页就没有「页」的概念,页码也就没有意义**——所以 `PAGESIZE` 设成 0 时,页码永远是 1。 3. `BREAK ON` 为什么必须先排序? —— 因为它是**按「值变了」来断行的**。 数据没排序,同一个值会分散在各处,**分组边界就变成随机的**——断行位置乱,小计也跟着错。

第 3 章到此为止。 下一篇进第 4 章:SELECT 基础——从最简单的 select * from emp 开始,把查询这件事拆开看。

相关笔记