🛢️ 第10章 常用函数
📄 一句话概括

函数和运算符是一回事:拿值换值,区别只在写法。 「有没有某个函数」取决于装的是哪一版,先清点再动手。 把函数包在列上,那一列的索引就废了——这是函数最常见的性能坑。

📝 本章和前面几章的关系

10.1 是这一章的导言,没有具体函数要记。它只定一件事:函数也是表达式。 这一章七篇,一个节一篇。后面六篇都是具体函数,这一篇先把「怎么判断能不能用」讲清。

一、函数就是「进去几个值,出来一个值」

四行的共同点:括号里有参数,括号外拿到一个值。 这一点和运算符完全一样,区别只在写法——运算符用符号,函数用名字加括号。

写法 结果
abs(-3) 3
upper('ab') AB
length('中文') 2
round(2.567, 2) 2.57

所以函数能出现的地方,运算符也能出现:SELECT 列表、WHERE、ORDER BY。 反过来也一样,能写 abs(x) 的地方就能写 x * -1。 第三行那个 length('中文') 给 2,不是 6。 SQLite 的 length 数的是字符数,MySQL 的 LENGTH 数的是字节数。 同一行字在 MySQL 里是 6 个字节(UTF-8 每个汉字三字节)。 要数字符得用 CHAR_LENGTH,用错一个,中文标题会被砍掉三分之二。

二、先清点:沙箱里有几个函数

一条 SQL 就能清点,这比翻手册靠谱。 PRAGMA function_list 是 SQLite 3.37 之后才有的,正好这个沙箱够新。

项 数
注册的函数条目(含重载) 192
不同的函数名 168
以 json_ 开头的 17

条目数比函数名多,是因为同名函数可以有不同参数个数,那叫重载。 round(x) 和 round(x, n) 就是两条,用的时候引擎按参数个数挑。 第三行那 17 个 json_ 函数最有意思:它们不是核心函数,是编译时带进来的扩展。 所以「这台机器上有没有某个函数」取决于装的是哪一版,不是查手册能查出来的。

三、同名不同义:一张对照单

跨库搬 SQL 时最危险的不是「没有这个函数」,而是「有,但意思不一样」。 没有会报错,意思不一样不会报错,只会算错。

函数 沙箱(SQLite) MySQL
length('中文') 2(字符数) 6(字节数)
round(2.5) 3.0 3
round(-2.5) -3.0 -3
substr(s, 2, 3) 第 2 个字符起取 3 个 同左

第一行是「数字节还是数字符」。 第三行和第四行两边一致,都是「四舍五入、遇五远离零」。 但 Python 内置的 round 是「遇五取偶」,round(2.5) 给它 2。 三个地方、三种行为,名字都叫 round。 所以「同一个名字」不保证「同一个行为」,跨库搬 SQL 时这几处必须重新验。 验的办法不是读文档,是跑一个边界值看结果——比如 2.5 和 -2.5。

四、没有的函数:自己补一个

reverse 在沙箱里报 no such function,补上只要三行:写一个函数,挂上去。

def reverse(s):
    return None if s is None else s[::-1]

c.create_function(u"reverse", 1, reverse)

挂上之后,它在 SQL 里跟内建函数没有区别,能出现在任何位置。 这一点很重要:它意味着「缺函数」从来不是硬障碍,只是多写三行。 reverse('中文abc') 给 cba文中,汉字没被拆开。 因为 Python 的字符串是「字符序列」,不是字节序列。 换成按字节倒序,一个汉字的三字节会被拆散,输出就是一串乱码。 「字符」和「字节」不是一回事,这一条在讲字符集时会反复出现。

五、把函数包在列上,索引就废了

这一条是函数最常见的性能坑,也是这一章最该记住的一句。 五万行的表,WHERE v > 3 走索引,WHERE abs(v) > 3 全表扫。

写法 执行计划
WHERE v > 3 SEARCH ... (v>?)
WHERE abs(v) > 3 SCAN t

原因很简单:索引里存的是 v 的原值,不是 abs(v) 算出来的值。 abs(v) > 3 这个条件对索引来说完全是陌生的,只能逐行算。 解法有两个:一是建「函数索引」(MySQL 8.0 支持,SQLite 叫表达式索引); 二是把条件改写到列这一侧——比如 v > 3 OR v < -3,两段都能走索引。 第二种更好,因为它不增加写入代价。 函数索引要多维护一份数据,写多的表要掂量一下。

六、动手 Demo:函数是什么,以及沙箱里到底有几个

这一段做四件事:跑四个函数看形状、清点沙箱里的函数、注册一个缺的、 最后用五万行的表把「函数废索引」量出来。

import sqlite3

c = sqlite3.connect(u":memory:")

# ① 清点
fl = c.execute(u"PRAGMA function_list").fetchall()
print(len(fl), len(set(x[0] for x in fl)))

# ② 缺的函数自己补
def reverse(s):
    return None if s is None else s[::-1]

c.create_function(u"reverse", 1, reverse)
print(c.execute(u"SELECT reverse('中文abc')").fetchone())

# ③ 函数包在列上,索引用不上
c.execute(u"CREATE TABLE t (id INTEGER PRIMARY KEY, v REAL)")
c.executemany(u"INSERT INTO t VALUES (?, ?)",
               [(i, (i * 37 % 100000) / 100.0) for i in range(1, 50001)])
c.execute(u"CREATE INDEX i_v ON t (v)")
for q in [u"SELECT COUNT(*) FROM t WHERE v > 3",
          u"SELECT COUNT(*) FROM t WHERE abs(v) > 3"]:
    print(c.execute(u"EXPLAIN QUERY PLAN " + q).fetchone()[-1])

实跑输出:

--------------------------------------------------------------------
第四十四课:函数是什么,以及沙箱里到底有几个
--------------------------------------------------------------------

  **教材 10.1 是这一章的导言:函数是什么、怎么调、MySQL 分了几类。**
  **这一节没有具体函数要记,但它定了一件事:函数和运算符一样,都是「拿值换值」。**
  **所以这一课不抄分类表,改成清点:沙箱里有多少函数、哪些和 MySQL 同名同义、哪些同名不同义。**

  一、函数就是「进去几个值,出来一个值」
  --------------------------------------------------------------------

  写法                      结果
  ------------------------------
  `SELECT abs(-3)`          3
  `SELECT upper('ab')`      AB
  `SELECT length('中文')`   2
  `SELECT round(2.567, 2)`  2.57
  **四行的共同点:括号里有参数,括号外拿到一个值。**
  **这一点和运算符一样,区别只在写法——运算符用符号,函数用名字加括号。**
  **所以函数能出现的地方,运算符也能出现:`SELECT` 列表、`WHERE`、`ORDER BY`。**
  **反过来也一样,能写 `abs(x)` 的地方就能写 `x * -1`。**
  **第三行那个 `length('中文')` 给 2,不是 6。**
  **SQLite 的 `length` 数的是字符数,MySQL 的 `LENGTH` 数的是字节数。**
  **同一行字在 MySQL 里是 6 个字节(UTF-8 每个汉字三字节),要数字符得用 `CHAR_LENGTH`。**
  **这是「同名不同义」的典型,后面每一类函数都会撞到几个。**

  二、清点:沙箱里有 192 个函数
  --------------------------------------------------------------------

  项                        数
  -----------------------------
  注册的函数条目(含重载)  192
  不同的函数名              168
  以 `json_` 开头的         17
  **一条 SQL 就能清点,这比翻手册靠谱。**
  **`PRAGMA function_list` 是 SQLite 3.37 之后才有的,正好这个沙箱够新。**
  **条目数比函数名多,是因为同名函数可以有不同参数个数,那叫重载。**
  **`round(x)` 和 `round(x, n)` 就是两条,用的时候引擎按参数个数挑。**
  **最有意思的是第三行:17 个 `json_` 函数。**
  **它们不是「核心函数」,是编译时带进来的扩展——这说明「有没有某个函数」取决于装的是哪一版。**

  三、没有的函数:注册一个替身
  --------------------------------------------------------------------
  **`reverse` 直接报 `no such function: reverse`。**

  写法                  结果
  -----------------------------
  `reverse('abc')`      cba
  `reverse('中文abc')`  cba文中
  **三行代码就补上了:写一个 Python 函数,用 `create_function` 挂上函数名和参数个数。**
  **挂上之后,它在 SQL 里跟内建函数没有区别,能出现在任何位置。**
  **注意第二行:`'中文abc'` 反过来是 `cba文中`,汉字没被拆开。**
  **因为 Python 的字符串是「字符序列」,不是字节序列。**
  **换成按字节倒序,一个汉字的三字节会被拆散,输出就是一串乱码。**
  **这个差别在讲字符集时会反复出现,先记住「字符」和「字节」不是一回事。**

  四、同名不同义:一张对照单
  --------------------------------------------------------------------

  函数                 沙箱(SQLite)          MySQL
  ----------------------------------------------------------------
  `length('中文')`     2(字符数)             6(字节数)
  `length` 的数字符版  就是 `length` 本身      `char_length`
  `round(2.5)`         3.0                     3
  `round(-2.5)`        -3.0                    -3
  `substr(s, 2, 3)`    从第 2 个字符起取 3 个  同左,索引从 1 开始
  **第一行和第二行是同一个坑:数字节还是数字符。**
  **做「标题最多多少个字」这种校验,用错一个函数,中文标题会被砍掉三分之二。**
  **第三行和第四行两边其实一致,都是「四舍五入、遇五远离零」。**
  **但 Python 内置的 `round` 是「遇五取偶」,`round(2.5)` 给它 2,和这两个引擎都不一样。**
  **所以「同一个名字」不保证「同一个行为」,跨库搬 SQL 时这几处必须重新验。**

  五、把函数包在列上,索引就废了
  --------------------------------------------------------------------

  写法                执行计划                                 耗时
  ----------------------------------------------------------------------
  `WHERE v > 3`       SEARCH t USING COVERING INDEX i_v (v>?)  1.61 毫秒
  `WHERE abs(v) > 3`  SCAN t                                   6.51 毫秒
  **第一行是 `SEARCH`,用上了索引;第二行是 `SCAN`,五万行挨个看。**
  **耗时差了 4.0 倍,而且行数越多差得越多。**
  **原因很简单:索引里存的是 `v` 的原值,不是 `abs(v)` 算出来的值。**
  **`abs(v) > 3` 这个条件对索引来说完全是陌生的,只能逐行算。**
  **解法有两个:一是建「函数索引」(MySQL 8.0 支持,SQLite 叫表达式索引),**
  **二是把条件改写到列这一侧——比如 `v > 3 OR v < -3`,两段都能走索引。**

清点那两个数是 192 和 168:条目里有重载,所以名比条目少。 这个数会随版本变,所以「我这台机器上有没有」只能现场查。 注册那三行是这一章最实用的技巧。 教材里点名的函数,沙箱缺了十几个,全靠这个办法补上。 最后两行执行计划一个是 SEARCH、一个是 SCAN,差别就在「函数包没包在列上」。 这个差别在五万行时是四倍多,行数越多差距越大。

30 秒实验

  1. 把 length('中文') 换成 length('abc'),看它给几——英文下字节和字符是同一个数。
  2. 用 PRAGMA function_list 数一数 json_ 开头的有几个。
  3. 把 WHERE abs(v) > 3 改成 WHERE v > 3 OR v < -3,看执行计划变回 SEARCH。

常见误区

  1. 以为「同名函数行为一样」,LENGTH 和 length 差了一个字节口径。
  2. 在 WHERE 里包函数,索引废了还不知道,只觉得「这表怎么这么慢」。
  3. 看到「找不到函数」就以为环境装坏了,其实补三行就能用。
自测三问(点开对答案) 1. 函数和运算符有什么共同点? —— 都是「拿值换值」,能出现的位置也一样:`SELECT` 列表、`WHERE`、`ORDER BY`。 区别只在写法,运算符用符号,函数用名字加括号。 2. `length('中文')` 在沙箱和 MySQL 里为什么不一样? —— 沙箱的 `length` 数字符(给 2),MySQL 的 `LENGTH` 数字节(UTF-8 每个汉字三字节,给 6)。 MySQL 要数字符得用 `CHAR_LENGTH`。 3. 为什么 `WHERE abs(v) > 3` 用不上索引? —— 因为索引里存的是 `v` 的原值,不是 `abs(v)` 的结果。条件对索引来说是陌生的,只能逐行算。 要解决就建函数索引,或者把条件改写到列这一侧。

下一篇 10.2 进数学函数,重点是「取整有三种口径」,混用会算错钱。

相关笔记