🏺 第4章 SQL 语言基础
📄 一句话概括

函数是「送进去一个值、还回来一个值」。Oracle 内置几百个,常用的就几十个。 按教材分五类:字符、数字、日期时间、转换、聚合。 这一篇的重点不是背函数名,是记住两条:① 聚合函数统一跳过 NULL; ② 同一个名字在不同引擎上规则可能不同(round 就是)。

📝 本章和前面几章的关系

3.4 节讲 COLUMN FORMAT 时见过数字格式串, 这一篇的 to_char 用的是同一套—— 区别只在位置:一个是显示时排,一个是取出来就已经是字符串。 聚合函数在这里先打个照面,第 9 章讲「分组与聚合」时会正式展开,包括 rollup、 cube 这些高级分组。

一、字符类:切、找、补

最常用的四个:substr 切、instr 找、length 数、|| 拼。 再加 lpad/rpad 补位、upper/lower 换大小写、replace 替换。 substr(s, 1, 3) 从第 1 个字符取 3 个。Oracle 的下标从 1 开始,不是 0。 substr(s, 0, 3) 和 substr(s, 1, 3) 结果一样, 这是 Oracle 特意兼容的,别指望别的库也这样。 instr 找不到时返回 0,不是 -1,也不是 NULL。 所以判「包不包含」要写 instr(x, 'A') > 0,写成 = 1 只能匹配「在第一位」。 这个 0 是 Oracle 的历史包袱(早期版本就这样),改不了。 lpad(7, 3, '0') 得 007,是做编号最常用的手法。 第一个参数得是字符串,传数字 Oracle 会自动转,但带上引号更明确。

二、数字类:round 与 trunc 的分工

round 会进位,trunc 直接砍掉。 这是它俩唯一的区别,也是全部的意义所在。 算工资、算税额用 round(该进的位要进); 算「满几年」「满几件」「用了几个整月」用 trunc(不满一年的不算)。 Oracle 的 round 是「四舍五入、远离零」:2.5→3、-2.5→-3。 Python 内置的 round 是「银行家舍入」:round(2.5) 给 2、round(3.5) 给 4。 同一个词,两套规则。 跨语言算钱的时候,这类差异必须显式处理,不能指望它一致。 mod(a, b) 的符号跟着 a 走:mod(-7, 3) 得 -1。 这跟某些语言「结果永远非负」的约定不同,做分片取模时要留意。

三、日期类:日期其实就是一个数

Oracle 的日期是「从公元前 4712 年 1 月 1 日起的第几天」,带小数表示时分秒。 所以日期能加减、能比较大小——它本质是个数。 add_months 会自动处理月末:1 月 31 日加一个月, 得 2 月 28(或 29)日,不会给你 3 月 3 日。 这个行为是 Oracle 特意做的,比自己算天数靠谱得多。 months_between 返回带小数的月数,除以 12 就是年。 但它是按「日历月」算的,不是按 30 天——跨月边界时结果会跳一下。 算工龄够用,算精确天数不能用它。 sysdate 是唯一一个不带括号就能用的「函数」——严格说它是关键字,不是函数。 在 SQLite 里必须写 sysdate(),不带括号会被当成列名直接报错。 这个区别在你写跨库脚本时会立刻显出来。 to_date('2026-10-30','YYYY-MM-DD') 一定要写明格式。 where hiredate = '1982-01-23' 也能跑,是因为 Oracle 拿会话的日期格式去猜。 换一个客户端、换一个会话参数,同一条 SQL 就可能报错或查错—— 隐式转换是最阴的坑,因为它平时都对。

四、转换类:同一套格式串的三种用法

to_char 数字转文本、to_date 文本转日期、to_number 文本转数字。 三个函数共用一套格式串,就是 3.4 节那套 999,990.00。 to_char 之后结果变文本,排序规则也跟着变。 to_char(sal) 排出来是「1000、800、900」这种(按字符比),不是数值顺序。 要在文本上排数值,要么补零对齐,要么干脆别转。 FM 前缀去掉前导空格。 Oracle 的数字格式默认按位补齐,短数字前面会留一串空格用来右对齐; FM999990 就把这些空格去掉。导数据时几乎一定要加。

五、聚合类:统一跳过 NULL

count、sum、avg、max、min,就这五个。 它们有一条统一规则:跳过 NULL。 count(*) 数行,count(列) 只数非空。 差别全在 NULL 上——14 行里有 10 个空,两个数就差 10。 avg(sal) 的分母是非空个数,不是总行数。 所以「给空值补 0 再平均」和「直接平均」是两个完全不同的口径。 报表上必须写清楚用的是哪个,否则同一个部门能算出两个数。 空表上 count(*) 给 0,其他四个给 NULL——不是 0,是 NULL。 所以 sum() 的结果直接参与运算,整个表达式会变成 NULL。 要兜底就写 nvl(sum(sal), 0)。

六、动手 Demo:五类函数各跑一组,重点看两套引擎的差异

这一段按五类各跑几条。 真正值得看的是两处:round 在负数上的行为,以及 sysdate 在两套引擎里长得不一样。 聚合那条配了一张空表——bonus 是空的,正好用来演「空表上聚合返回什么」。

import sqlite3, datetime
c = sqlite3.connect(":memory:")
c.create_function("round2", 2,
                  lambda x, n: (int(x * 10 ** n + (0.5 if x >= 0 else -0.5))) / 10 ** n)

# 一、字符类
print(c.execute("select substr('ALLEN',1,3), instr('ALLEN','A'), "
                 "length('ALLEN'), upper('allen')").fetchall())
print(c.execute("select lpad('7',3,'0'), rpad('AB',5,'*'), "
                 "replace('2026-10-30','-','/')").fetchall())

# 二、数字类:Oracle 的 round 是「四舍五入、远离零」
print(c.execute("select round(2.5), round(-2.5)").fetchall())
print(round(2.5), round(-2.5))          # Python 内置是银行家舍入

# 三、日期类:SQLite 没有 sysdate,用自己的函数凑
print(c.execute("select date('now'), time('now')").fetchall())
print(c.execute("select strftime('%Y-%m-%d', date('now','+3 month'))").fetchall())

# 四、转换类
print(c.execute("select cast('2975' as integer), printf('%,d', 2975)").fetchall())

# 五、聚合类:count(*) 数行,count(列) 只数非空
c.execute("create table emp (ename text, comm real, sal real)")
c.executemany("insert into emp values (?,?,?)",
               [('A', 300, 1600), ('B', None, 2975), ('C', None, 800)])
print(c.execute("select count(*), count(comm), sum(sal), avg(sal) from emp").fetchall())
print(c.execute("select sum(sal), avg(sal) from emp where 1 = 0").fetchall())

实跑输出:

--------------------------------------------------------------------
第十八课:Oracle 常用系统函数——五类,一类一个真问题
--------------------------------------------------------------------

  **函数是「送进去一个值、还回来一个值」。**
  **Oracle 内置了几百个,常用的就那么几十个。**
  **这一节按教材分五类:字符、数字、日期时间、转换、聚合。**
  **每类我只挑几个真会用到的,重点是「它们在第几层执行」。**


  一、字符类函数
  --------------------------------------------------------------------

  SQL> select ename, length(ename) as 长度, substr(ename, 1, 3) as 前三, instr(ename, 'A') as A的位置 from emp where deptno = 30 order by ename
  ENAME  长度 前三 A的位置
  ------------------------
  ALLEN  5    ALL  1
  BLAKE  5    BLA  3
  JAMES  5    JAM  2
  MARTIN 6    MAR  2
  TURNER 6    TUR  0
  WARD   4    WAR  2
  已选择 6 行
  **`substr(ename, 1, 3)` 从第 1 个字符开始取 3 个。**
  **注意 Oracle 的下标从 1 开始**(不是 0),`substr(ename, 0, 3)` 和 `substr(ename, 1, 3)` 一样。
  **`instr` 找不到时返回 0,不是 -1,也不是 NULL。**
  **所以判「包不包含」要写 `instr(x, 'A') > 0`,不能写 `= 1`。**

  SQL> select lpad('7', 3, '0') as 补零, rpad('AB', 5, '*') as 右补, initcap('hello world') as 首字母大写, replace('2026-10-30', '-', '/') as 换分隔符 from dual
  补零 右补  首字母大写  换分隔符
  ---------------------------------
  007  AB*** Hello World 2026/10/30
  已选择 1 行
  **`lpad` 补零是做编号最常用的手法**(`LPAD(7, 3, '0')` 得 `007`)。
  **注意它的第一个参数得是字符串**,传数字 Oracle 会自动转,但带上引号更明确。**
  **`initcap` 只把每个单词首字母大写**,`'hello world'` 变成 `'Hello World'`。
  **它不认识「不该大写的词」**,`initcap('ORACLE 19c')` 会给你 `'Oracle 19C'`。**

  二、数字类函数
  --------------------------------------------------------------------

  SQL> select round(2975.567, 2) as 四舍五入, trunc(2975.567, 2) as 截断, mod(2975, 100) as 余数, abs(-2975) as 绝对值 from dual
  四舍五入 截断    余数 绝对值
  ----------------------------
  2975.57  2975.56 75   2975
  已选择 1 行
  **`round` 会进位,`trunc` 直接砍掉**——这是它俩唯一的区别,也是最常用的区别。
  **算工资、算税额用 `round`;算「满几年」「满几件」用 `trunc`。**

  SQL> select round(2.5) as 正数, round(-2.5) as 负数, round(3.5) as 三点五, round(4.5) as 四点五 from dual
  正数 负数 三点五 四点五
  -----------------------
  3    -3   4      5
  已选择 1 行
  **Oracle 的 `round` 是「四舍五入、远离零」:`2.5→3`、`-2.5→-3`。**
  **而 Python 内置的 `round` 是「银行家舍入」:`round(2.5)` 给 `2`、`round(3.5)` 给 `4`。**
  **同一个词,两种规则**——跨语言算钱的时候,这类差异必须显式处理。
  **`mod(a, b)` 的符号跟着 `a` 走**(`mod(-7, 3)` 得 `-1`),
  **这跟某些语言「结果永远非负」的约定不同。**分页取模时要注意。

  三、日期和时间类函数
  --------------------------------------------------------------------

  [Oracle 原码 · 本沙箱不执行]
  | select sysdate from dual;
  | select to_char(sysdate, 'YYYY-MM-DD') from dual;
    Oracle 里 sysdate 不带括号。


  SQL> select to_char(sysdate(), 'YYYY-MM-DD') as 日期, to_char(sysdate(), 'HH24:MI:SS') as 时刻, to_char(sysdate(), 'YYYY-MM-DD HH24:MI:SS') as 完整时间 from dual
  日期       时刻     完整时间
  ---------------------------------------
  2026-10-30 13:51:53 2026-10-30 13:51:53
  已选择 1 行
  **注意 `sysdate` 本身就是一个「带时分秒的时间戳」**,不带格式的时候原样打出。
  **要只拿日期或只拿时刻,都得靠 `to_char` 拆。**
  **上面这条是 SQLite 的等价写法:`sysdate()` 必须带括号。**
  **不带括号时 SQLite 会把它当成「一个叫 sysdate 的列」,直接报「没有这一列」。**
  **Oracle 的 `sysdate` 是唯一一个不带括号就能用的「函数」**,
  **它是关键字不是函数**——这个区别在你写跨库脚本时会立刻显出来。

  SQL> select ename, hiredate, to_char(add_months(hiredate, 3), 'YYYY-MM-DD') as 三个月后, to_char(last_day(hiredate), 'YYYY-MM-DD') as 当月最后一天, round(months_between(sysdate(), hiredate) / 12, 1) as 工龄年 from emp where deptno = 10 order by hiredate
  ENAME  HIREDATE   三个月后   当月最后一天 工龄年
  ------------------------------------------------
  CLARK  1981-06-09 1981-09-09 1981-06-30   45.4
  KING   1981-11-17 1982-02-17 1981-11-30   45
  MILLER 1982-01-23 1982-04-23 1982-01-31   44.8
  已选择 3 行
  **`add_months` 会自动处理月末**:1 月 31 日加一个月得 2 月 28(或 29)日,
  **不会给你 3 月 3 日。** 这个行为是 Oracle 特意做的,比自己算天数靠谱。
  **`months_between` 返回带小数的月数**,除以 12 就是年。
  **但它是按「日历月」算的,不是按 30 天**——跨月边界时结果会跳一下,
  **算工龄够用,算精确天数不能用它。**
  **日期在 Oracle 里就是一个数**(从公元前 4712 年起的第几天,带小数表示时分秒)。
  **所以日期能加减、能比较大小**,`hiredate > to_date('1982-01-01','YYYY-MM-DD')` 是合法的。
  **用字符串直接比较则是错的**——字符串比的是字符顺序,不是时间先后。

  四、转换类函数
  --------------------------------------------------------------------

  SQL> select to_char(2975.5, '999,990.00') as 千分位, to_char(2975, 'FM999990') as 去前导空格, to_number('2975') as 转回数字, to_date('2026-10-30', 'YYYY-MM-DD') as 转日期 from dual
  千分位   去前导空格 转回数字 转日期
  ---------------------------------------
  2,975.50 2975       2975     2026-10-30
  已选择 1 行
  **这三个函数是「同一套格式串」的三种用法**,3.4 节讲 `COLUMN` 时见过。
  **区别只在位置:`COLUMN` 是显示时排,`to_char` 是「取出来就已经是字符串」。**
  **`to_char` 之后结果变文本,排序规则也跟着变。**
  **`to_char(sal)` 排出来是 `1000, 800, 900` 这种(按字符比),不是数值顺序。**
  **要在文本上排数值,要么补零对齐,要么干脆别转。**
  **隐式转换是最阴的坑:** `where hiredate = '1982-01-23'` 能跑,
  **是因为 Oracle 拿你的会话日期格式去猜。**
  **换一个客户端、换一个会话参数,同一条 SQL 就可能报错或查错。**
  **规矩:日期一律 `to_date(..., 'YYYY-MM-DD')` 显式转,别指望它猜。**

  五、聚合函数:把多行压成一个值
  --------------------------------------------------------------------

  SQL> select count(*) as 行数, count(comm) as 非空个数, count(distinct deptno) as 部门数, sum(sal) as 工资总额, round(avg(sal), 2) as 平均工资, max(sal) as 最高, min(sal) as 最低 from emp
  行数 非空个数 部门数 工资总额 平均工资 最高 最低
  ------------------------------------------------
  14   4        3      29025    2073.21  5000 800
  已选择 1 行
  **`count(*)` 和 `count(列)` 的区别只在 NULL 上**:前者数行,后者只数非空。
  **所以「有多少人有奖金」必须写 `count(comm)`。**
  **`avg(sal)` 会自动忽略 NULL**,**分母是非空个数,不是总行数。**
  **这是聚合函数统一的行为:除了 `count(*)`,它们都跳过 NULL。**
  **想让 NULL 当 0 参与,得写 `avg(nvl(comm, 0))`——但要清楚这是另一个口径了。**

  SQL> select count(*) as 空表的行数, sum(sal) as 空表的和, avg(sal) as 空表的平均 from bonus
  空表的行数 空表的和 空表的平均
  ------------------------------
  0          NULL     NULL
  已选择 1 行
  **空表上 `count(*)` 给 0,`sum`/`avg`/`max`/`min` 全给 NULL。**
  **不是 0,是 NULL。**
  **所以 `select sum(sal) from 空表` 拿到 NULL 之后直接参与运算,整个结果会变成 NULL。**
  **要兜底就写 `nvl(sum(sal), 0)`。**

字符那一组里 instr('ALLEN','A') 给 1,instr('TURNER','A') 给 0。 0 就是「没找到」——不是 -1。判包含必须写 > 0。 round(2.5) 给 3,round(-2.5) 给 -3。 这就是「远离零」的意思:正数往上、负数往下,都往远离 0 的方向走。 而 Python 的 round(-2.5) 给 -2(往偶数靠),两边不一致。 日期那条是本课唯一一处「SQL 写法被迫改动」的地方: Oracle 写 sysdate,SQLite 必须写 sysdate()。 不带括号时 SQLite 会把它当成「一个叫 sysdate 的列」,直接报「没有这一列」。 to_char(sysdate(), 'HH24:MI:SS') 只取时刻,'YYYY-MM-DD' 只取日期。 格式串里的 HH24 是 24 小时制,写成 HH 是 12 小时制—— 少个 24,下午三点会变成 03,这种错最难发现。 聚合那一组:count(*) 给 14、count(comm) 给 4。 差的 10 就是空的个数。 avg(sal) 用的是 14 做分母(sal 没有空值), 如果换成 avg(comm),分母就只有 4——同样叫「平均」,口径完全不同。 最后那条查空表 bonus:count(*) 给 0,sum 和 avg 都给 NULL。 不是 0,是 NULL。 所以「本月奖金总额」这种报表,空表那一个月必须写 nvl(sum(comm), 0),否则数字列会变成空白。

30 秒实验

  1. 把 round(2.5) 换成 round(2.4) 和 round(2.6) 各跑一次,确认临界点在哪。 然后再用 Python 算一遍同样的数,对比两边结果。
  2. trunc(2975.567, 2) 给 2975.56,round(2975.567, 2) 给 2975.57。 把第二个参数改成负数(round(2975.567, -2))看会怎样—— 它会把百位之后全抹掉,得到 3000。
  3. substr('ALLEN', -2) 试试负下标。 Oracle 里负数是「从右往左数」,给的是最后两个字符 EN——这个特性在取扩展名时很好用。
  4. 把 count(comm) 改成 count(nvl(comm, 0)),看结果从 4 变成 14。 这就是「跳过 NULL」和「把 NULL 当 0」的差别,一个数之差,口径全变。

常见误区

  1. 以为 round 在所有语言里规则一样。 Oracle 是「远离零」,Python 是「银行家舍入」。 涉及钱的跨语言计算,必须显式规定舍入规则。
  2. 用 instr 判包含时写 = 1。 它给的是「第一次出现的位置」,不在第一位就不等于 1, 但也不等于 0。判包含永远写 > 0。
  3. 日期用字符串直接比较。 '1982-1-5' > '1982-01-23' 在字符串上是真, 在时间上是假。 日期一律 to_date 显式转。
  4. 以为空表上 sum() 返回 0。 返回 NULL。 NULL 参与任何运算结果还是 NULL,报表上就是一片空白。
自测三问(点开对答案) 1. `round(-2.5)` 在 Oracle 里给多少?为什么和 Python 不一样? —— 给 **-3**。Oracle 是「四舍五入、远离零」,负数往下走。 **Python 是银行家舍入(往偶数靠),给 -2。** 两套规则都对,但结果不同。 2. `instr` 找不到时返回什么?判包含该怎么写? —— 返回 **0**(不是 -1,也不是 NULL)。 **判包含写 `instr(x,'A') > 0`**——写 `= 1` 只能匹配「在第一位」。 3. 空表上 `count(*)`、`sum()`、`avg()` 分别返回什么? —— `count(*)` 给 **0**,`sum()` 和 `avg()` 给 **NULL**。 **NULL 参与运算结果仍是 NULL**,所以报表要 `nvl(sum(x), 0)` 兜底。

下一篇讲子查询: 把一条查询的结果,当成另一条查询的输入。重点是一个不报错但结果全错的坑——not in 遇到 NULL。

相关笔记