🐬 第10章 创建计算字段
📄 一句话概括

表里没有的列,可以当场算出来再交给你,这叫计算字段——它不占存储,只在查询时存在。 这一章干三件事:拼接字符串、做算术、给算出来的列起名字。 Demo 在六家供应商上拼出六行完整地址,在 20005 这张单上算出 4 行小计、合计 149.87; 两个没留邮箱的客户提醒了我们:NULL 一旦进了算术,整列都变 NULL。

📝 本章和前面几章的关系

前面六章都在「取现有的列」:选列、排序、筛行。从这一章开始造新列。 这也是数据库第一次参与「加工」而不是单纯「搬运」,后面第 12 章的聚集、第 14 章的子查询, 都是这条线上的。

一、计算字段:表里没有这一列

存进表里的是原始数据,交给你的时候未必要保持原样。供应商表里名字、国家分两列, 可你要的是 ACME (USA) 这样一个字符串。 两种做法:把两列取回来在应用里拼,或者让数据库拼好再返回。 计算字段就是后一种——它在 SELECT 的列位置写一个表达式,结果占一列,但表结构一点没变。 记住三条:不落库、不占空间、每次查询重算。 重算意味着它永远是最新的,也意味着它不能建索引(第 8 章那个代价的同款)。

二、拼接字段:CONCAT 与双竖线

教材用 CONCAT(vend_name, ' (', vend_country, ')'):MySQL 的拼接函数,参数不限个数, NULL 参与就整体给 NULL。 SQLite 没有 CONCAT,用双竖线 ||:vend_name || ' (' || vend_country || ')',结果完全一样。 双竖线在 MySQL 里默认是逻辑或——'a' || 'b' 会给 0(假), 除非启动时开了 PIPES_AS_CONCAT。 这就是教材绕道用函数的原因: 同一个符号,两个引擎两种意思。 教材还强调先 RTRIM 再拼:MySQL 的 CHAR 是定长,短值会被补空格补齐, 拼起来就是 ACME (USA)。 本课的列是 TEXT,不补空格,所以 Demo 里 rtrim 前后长度一样——换个字段类型, 这条建议就从必须降级成可以不写。

三、别名:AS 不只是好看

不写别名,列名就是整段表达式:vend_name || ' (' || vend_country || ')' 会原样变成表头。 程序里取值要照抄这一长串,改一个空格就取不到。 三种写法:AS vend_title(推荐)、vend_title(省 AS,能跑但别省)、 AS "vend title"(别名含空格时加双引号)。 别名只改表头,不动数据,也不改表结构。 它的真正价值是给调用方一个稳定的名字—— 表达式以后怎么改,接口不变。

四、执行算术计算

算术运算符和别的语言一样:加、减、乘、除,乘除先算,括号优先。 quantity * item_price 就是小计,四行加起来就是这张单的金额。 除法要小心:SQLite 里两个整数相除,结果还是整数(7 / 2 给 3); MySQL 默认给 3.5000。 要小数就把一边写成小数:7.0 / 2 才是 3.50。 取余写 %(有的数据库写 MOD)。 这一脚换库时最容易踩:同一句求平均单价,一个先截掉小数再往下算,结果能差出几个百分点。

五、NULL 会传染

NULL 不是 0,也不是空字符串,它是「不知道」。 任何值跟它做运算,结果还是「不知道」——所以 LENGTH(NULL) 给 NULL,NULL + 1 也给 NULL。 本课两个客户没留邮箱,邮箱长度那一列就空着两个 NULL。 这不是 bug,是 SQL 的三值逻辑(真/假/不知道)。 要兜底用 COALESCE:COALESCE(cust_email, '(未填)') 有值就用值,NULL 就用第二个参数。 算钱之前先问一句「这列允许空吗」,比事后查半天强。

六、小结

教材小结:计算字段在 SELECT 里现算,拼接用 CONCAT,算术直接用运算符,别名用 AS。 本课补三条:一是 SQLite 没有 CONCAT,用 ||;二是整数相除会截断,要小数先写小数; 三是 NULL 参与运算结果仍是 NULL,用 COALESCE 兜。

七、动手 Demo:从两列拼成一列,再算出一列钱

Demo 六段:先看原始两列,再拼成一列、起别名、查尾空格,然后算 20005 这张单的小计, 最后看 NULL 怎么把整列染成 NULL。 每一段的数字都是当场跑出来的,你可以把 20005 换成别的单号再试。

import sqlite3
con = sqlite3.connect(":memory:")

# 拼接:MySQL 写 CONCAT(...),SQLite 写双竖线
con.execute("SELECT vend_name || ' (' || vend_country || ')' AS vend_title "
            "FROM vendors ORDER BY vend_name")
# 不写别名,列名就变成整段表达式
con.execute("SELECT vend_name || ' (' || vend_country || ')' FROM vendors LIMIT 2")
# RTRIM 去尾空格:本课的 TEXT 列看不出差别,用 LENGTH 证明
con.execute("SELECT vend_name, LENGTH(vend_name) AS len_raw, "
            "LENGTH(RTRIM(vend_name)) AS len_trim FROM vendors")
# 算术计算:单价乘数量
con.execute("SELECT prod_id, quantity, item_price, "
            "quantity * item_price AS expanded_price "
            "FROM orderitems WHERE order_num = 20005")
# 整数相除会截断,要小数就把一边写成小数
con.execute("SELECT 7 / 2 AS a, 7.0 / 2 AS b, 7 % 2 AS c")
# NULL 会传染,用 COALESCE 兜底
con.execute("SELECT cust_name, LENGTH(cust_email) AS email_len FROM customers")
con.execute("SELECT cust_name, COALESCE(cust_email, '(未填)') AS email FROM customers")

实跑输出:

--------------------------------------------------------------------
第十课:创建计算字段
--------------------------------------------------------------------

  【一】先把原始列摆出来:没有「完整地址」这一列
  SQL> SELECT vend_name, vend_country FROM vendors ORDER BY vend_name
  vend_name          vend_country
  --------------------------------
  ACME               USA
  Anvils R Us        USA
  Furball Inc.       USA
  Jet Set            England
  Jouets Et Ours     France
  LT Supplies        USA
  (6 行)
  **六家供应商,名字和国家分成两列,地址得在应用里自己拼。**
  **计算字段就是让数据库把这步算完再交出来**——它不存进表,每次查询现算。

  【二】拼接:MySQL 用 CONCAT,SQLite 用双竖线
  SQL> SELECT vend_name || ' (' || vend_country || ')' AS vend_title FROM vendors ORDER BY vend_name
  vend_title
  ------------------------
  ACME (USA)
  Anvils R Us (USA)
  Furball Inc. (USA)
  Jet Set (England)
  Jouets Et Ours (France)
  LT Supplies (USA)
  (6 行)
  **这时结果里多了一列 vend_title,表里根本没有这一列。**
  **教材原码用的是 `CONCAT(vend_name, ' (', vend_country, ')')`**,MySQL 的拼接函数;
  SQLite 用双竖线 `||`,两者结果一样。
  **双竖线在 MySQL 里默认是逻辑或**(除非开了 PIPES_AS_CONCAT),
  所以教材才绕道用 CONCAT——**同一个符号,两个引擎两种意思。**

  【三】别名:不写会怎样
  SQL> SELECT vend_name || ' (' || vend_country || ')' FROM vendors LIMIT 2
  vend_name || ' (' || vend_country || ')'
  -----------------------------------------
  Anvils R Us (USA)
  LT Supplies (USA)
  (2 行)
  **列名直接变成了整段表达式**——程序里取值时得照抄这一长串,容易错。
  **别名三种写法**:`AS vend_title`、`vend_title`(省 AS,可以但别省)、
  `AS "vend title"`(含空格时加引号)。**AS 只影响列名,不改数据。**

  【四】RTRIM:教材为什么反复强调去尾空格
  SQL> SELECT vend_name, LENGTH(vend_name) AS len_raw,
       LENGTH(RTRIM(vend_name)) AS len_trim FROM vendors ORDER BY vend_name
  vend_name          len_raw     len_trim
  -----------------------------------------
  ACME               4           4
  Anvils R Us        11          11
  Furball Inc.       12          12
  Jet Set            7           7
  Jouets Et Ours     14          14
  LT Supplies        11          11
  (6 行)
  **两列一模一样,因为本课的 vend_name 是 TEXT,不补空格。**教材强调 RTRIM,
  是因为 MySQL 的 CHAR 定长列会把短值补空格补齐——
  **那次拼接会拼出一堆看不见的尾空格**,所以先 RTRIM 再拼。
  **换个字段类型,这条建议就从「必须」降级成「可以不写」。**

  【五】算术计算:单价乘数量
  SQL> SELECT prod_id, quantity, item_price,
       quantity * item_price AS expanded_price FROM orderitems WHERE order_num = 20005
  prod_id     quantity     item_price     expanded_price
  -------------------------------------------------------
  ANV01       10           5.99           59.90
  ANV02       3            9.99           29.97
  TNT2        5            10.00          50.00
  FB          1            10.00          10.00
  (4 行)
  **四行的 expanded_price 加起来是 149.87**,就是 20005 这张单的金额。
  **算术运算符和 Python 一样**:加、减、乘、除,乘除先算,括号优先。
  **除法要小心**:SQLite 里两个整数相除,结果还是整数;MySQL 默认给小数。
  **要小数就把一边写成小数。**

  SQL> SELECT 7 / 2 AS a, 7.0 / 2 AS b, 7 % 2 AS c
  a     b        c
  -----------------
  3     3.50     1
  (1 行)
  **`7 / 2` 给 3、`7.0 / 2` 给 3.50**——差别只在那个 `.0`。
  **`%` 是取余**(有的数据库写 MOD),`7 % 2` 给 1。
  **这一脚换库时最容易踩**:同一句求平均单价,SQLite 先截掉小数再往下算,
  结果能和 MySQL 差出几个百分点。

  【六】NULL 会传染:算进算术就全变 NULL
  SQL> SELECT cust_name, cust_email,
       LENGTH(cust_email) AS email_len FROM customers ORDER BY cust_id
  cust_name          cust_email              email_len
  ------------------------------------------------------
  Coyote Inc.        [email protected]         15
  Mouse House        NULL                    NULL
  Wascals            [email protected]     19
  Yosemite Place     [email protected]        16
  E Fudd             NULL                    NULL
  (5 行)
  **两个客户没留邮箱,`LENGTH(NULL)` 给的不是 0,是 NULL。**
  **任何值跟 NULL 做运算,结果都是 NULL**——
  **所以算钱、算长度之前先想一句:这一列允许空吗?**空的那几行要不要 `COALESCE(列, 0)` 兜一下。
  SQL> SELECT cust_name, COALESCE(cust_email, '(未填)') AS email FROM customers ORDER BY cust_id
  cust_name          email
  ---------------------------------------
  Coyote Inc.        [email protected]
  Mouse House        (未填)
  Wascals            [email protected]
  Yosemite Place     [email protected]
  E Fudd             (未填)
  (5 行)

第一段只摆原始列。 六家供应商,名字与国家分两列,要拼成地址得自己动手。 第二段拼接。 六行 ACME (USA) 这样的结果,表里没有 vend_title 这一列,是当场算的。 第三段故意不写别名。 表头变成了整段表达式,这就是为什么每条表达式都该有个名字。 第四段量尾空格。 两列一模一样(4 对 4、11 对 11),因为本课用的是 TEXT; 教材那次是 CHAR 定长补空格,才非 RTRIM 不可。 第五段算钱。 四行小计 59.90、29.97、50.00、10.00,加起来 149.87,就是 20005 这张单的金额。 第六段 NULL。 LENGTH(cust_email) 在两个空邮箱上行给 NULL,换上 COALESCE 之后变成 (未填), 5 行全都有值了。

30 秒实验

  1. 把拼接改成 vend_country || ' - ' || vend_name,看六行变成什么样。
  2. 用 quantity * item_price 算 20009 那张单的金额,和 149.87 比一比。
  3. 把 COALESCE(cust_email, '(未填)') 换成 cust_email,观察空邮箱那两行的区别。

常见误区

  1. 以为计算字段会存进表。它只在查询结果里存在,表结构一个字都没改。
  2. 在 MySQL 里直接用 || 拼字符串。默认它算逻辑或,得用 CONCAT 或开 PIPES_AS_CONCAT。
  3. 以为 NULL 参与算术等于 0。NULL 加任何数还是 NULL,要兜底得写 COALESCE。
自测三问(点开对答案) 1. 计算字段和表里的列,本质区别在哪? —— 表里的列有存储、有类型、能建索引;计算字段只在查询时算出来,不落库、不占空间、每次重算。 2. 为什么教材拼字符串前要先 RTRIM? —— 因为 MySQL 的 CHAR 定长列会把短值补空格补齐,直接拼会拼进一串看不见的尾空格; 换成 TEXT 或 VARCHAR 就不补,RTRIM 可以不写。 3. `SELECT 7 / 2` 在 SQLite 和 MySQL 上分别给什么? —— SQLite 给 3(整数除整数还是整数),MySQL 给 3.5000;要小数得把一边写成 7.0。

能拼能算了,但写法还绑在具体引擎上:拼接用函数还是竖线、取年份用什么, 换个库就得改。 下一章专讲这块最不可移植的地方——函数。

相关笔记