上一篇我们以分析世界银行全球 GDP 和人口数据为案例,回顾了基础篇所学的各种 SQL 基本查询和分析功能。
从本篇开始我们将会学习进阶篇的内容。进阶篇将会介绍更加复杂的多表查询、子查询、集合运算以及各种高级的数据分析技术,能够让我们真正体会到 SQL 数据处理和分析的强大。
作为进阶篇的开始,我们先来讨论一下 SQL 中的空值问题,因为空值的特殊性导致我们很容易出现一些错误和问题。

空值与三值逻辑

SQL 中的空值(NULL)是一个特殊的值,代表了缺失/未知的数据或者不适用的情况。例如,用户在注册时没有提供电子邮箱地址,那么该用户的邮箱就未知;是否怀孕对于男性员工就不适用。
对于大多数的编程语言而言,逻辑运算的结果只有两种情况:真(Ture)或者假(False)。但是对于 SQL 而言,逻辑运算结果存在三种情况:真、假或者未知(Unknown)
image.png
对于 SQL 查询中的 WHERE 条件,只有结果为真的数据才会返回,结果为假或者未知都不会返回。
因此,SQL 中的逻辑运算符 AND、OR 以及 NOT 的结果也存在三种情况。以下是 AND 操作符的逻辑真值表:

AND 操作符 TRUE FALSE NULL
TRUE TRUE FALSE NULL
FALSE FALSE FALSE FALSE
NULL NULL FALSE NULL

AND 操作符只有当两边的结果都为真时,最终结果才为真;否则结果为假或者未知,意味着不返回结果。
下面是 OR 操作符的逻辑真值表:

OR 操作符 TRUE FALSE NULL
TRUE TRUE TRUE TRUE
FALSE TRUE FALSE NULL
NULL TRUE NULL NULL

OR 操作符只要有一个结果为真,最终结果就为真;否则结果为假或者未知,意味着不返回结果。
最后是 OR 操作符的逻辑真值表:

NOT 操作符
TRUE FALSE
FALSE TRUE
NULL NULL

NOT 操作符用于取反操作,对于未知结果取反结果还是未知。
三值逻辑带来的直接问题就是空值的比较运算。

空值比较与运算

任何值与 NULL 值进行比较时,结果无法确认是真还是假。以下比较运算的结果都是未知:

  1. NULL = 0 NULL != 0 NULL > 1 NULL = NULL NULL != NULL

NULL 等于 NULL 的运算结果未知,NULL 不等于 NULL 的运算结果也未知。因此,为了判断某个值是否为空,SQL 提供了 IS NULL 和 IS NOT NULL 谓词:

  1. SELECT emp_id FROM employee WHERE NULL = NULL;
  2. emp_id|
  3. ------|
  4. SELECT emp_id FROM employee WHERE NULL IS NULL;
  5. emp_id|
  6. ------|
  7. 1|
  8. 2|
  9. 3|
  10. ...

第一个查询使用等号进行判断,没有返回结果;第二个查询使用 IS NULL,返回了所有的员工编号。
另外,当表达式或者函数涉及 NULL 值时,通常结果也是 NULL 值。例如,以下运算的结果都为空:

  1. SELECT 100 + NULL, UPPER(NULL) FROM employee WHERE emp_id = 1;
  2. 100 + NULL|UPPER(NULL)|
  3. ----------|-----------|
  4. | |

不过也存在一些例外,比如:

  1. SELECT CONCAT('hello', NULL) FROM employee WHERE emp_id = 1;

该查询只有在 MySQL 返回 NULL,CONCAT 函数在 Oracle、SQL Server 以及 PostgreSQL 中将 NULL 当作空字符串处理,返回字符串“hello”。由于不同数据库采取了不同的实现,因此在使用时这些函数时需要小心。

空值的排序

在第 6 篇中我们介绍了 ORDER BY 排序,并且讨论了空值的排序规则。由于 SQL 标准没有提出明确要求,导致在不同的数据库中存在两种空值排序方式:

  • Oracle 和 PostgreSQL,升序时空值排在最后,降序时空值排在最前;同时支持使用 NULLS FIRST 和 NULLS LAST 指定空值的顺序;
  • MySQL 和 SQL Server,升序时空值排在最前,降序时空值排在最后。

我们可以利用 COALESCE 函数或者 CASE 表达式解决空值的排序问题,第 11 篇给出了 CASE 表达式相关的示例。下面我们介绍一下 SQL 中关于空值处理的函数。

空值函数

SQL 中定义了两个与空值转换相关的函数:NULLIFCOALESCE

NULLIF

NULLIF(expr1, expr2) 函数接受 2 个参数,如果第一个参数等于第二个参数,返回 NULL;否则,返回第一个参数的值。
以下示例说明了 NULLIF 函数的作用:

  1. SELECT NULLIF(1, 2), NULLIF(3, 3) FROM employee WHERE emp_id = 1;
  2. NULLIF(1, 2)|NULLIF(1, 1)|
  3. ------------|------------|
  4. 1| [NULL]|

因为 1 不等于 2,该查询的第一列结果为 1;因为 3 等于 3,第二列结果为 NULL。
NULLIF 函数的一个常见用途是防止除零错误:

  1. -- 除零错误
  2. SELECT * FROM employee WHERE 1 / 0 = 1;
  3. -- 避免除零错误
  4. SELECT * FROM employee WHERE 1 / NULLIF(0 , 0) = 1;

第一个查询中被除数为 0,出现除零错误(MySQL 可能不会提示错误);第二个查询使用 NULLIF 函数将被除数 0 转换为 NULL,整个结果为 NULL。

COALESCE

COALESCE(expr1, expr2, expr3, …) 函数接受一个参数列表,并且返回第一个非空的参数;如果所有参数都为空,返回 NULL。
以下示例用于查询员工的总收入:

  1. SELECT emp_name,
  2. salary*12 + bonus AS "年收入",
  3. salary*12 + COALESCE(bonus, 0) AS "年收入"
  4. FROM employee WHERE emp_id <= 6;
  5. emp_name|年收入 |年收入 |
  6. --------|---------|---------|
  7. 刘备 |370000.00|370000.00|
  8. 关羽 |322000.00|322000.00|
  9. 张飞 |298000.00|298000.00|
  10. 诸葛亮 |296000.00|296000.00|
  11. 黄忠 | [NULL]| 96000.00|
  12. 魏延 | [NULL]| 90000.00|

该查询中的第三列利用 COALESCE 函数将奖金为空的数据转换为 0,得到了正确的年收入。

专有函数

除了SQL 标准中定义的表达式之外,许多数据库还实现了一些类似的扩展函数:

  • Oracle 提供了 NVL(expr1, expr2) 以及 NVL2(expr1, expr2, expr3) 函数;
  • MySQL 提供了 IFNULL(expr1, expr2) 以及 IF(expr1, expr2, expr3) 函数;
  • SQL Server 提供了 ISNULL(expr1, expr2) 函数。

首先来看 NVL(expr1, expr2) 函数,它用于返回第一个非空的参数值,相当于只有两个参数的 COALESCE 函数。以下查询也返回员工的总收入:

  1. -- Oracle 实现
  2. SELECT emp_name,
  3. salary*12 + NVL(bonus, 0) AS "年收入"
  4. FROM employee WHERE emp_id <= 6;

该查询的结果与上一节 COALESCE 函数示例相同。
接下来是 NVL2(expr1, expr2, expr3) 函数,它包含三个参数。如果第一个参数不为空,返回第二个参数的值;否则,返回第三个参数的值。以下查询同样返回员工的总收入:

  1. -- Oracle 实现
  2. SELECT emp_name,
  3. NVL2(bonus, salary*12 + bonus, salary*12) AS "年收入"
  4. FROM employee WHERE emp_id <= 6;

该查询的结果与上一节 COALESCE 函数示例相同。
MySQL 提供了 IFNULL(expr1, expr2) 函数。该函数用于返回第一个非空的参数值,相当于只有两个参数的 COALESCE 函数。以下查询也返回员工的总收入:

  1. -- MySQL 实现
  2. SELECT emp_name,
  3. salary*12 + IFNULL(bonus, 0) AS "年收入"
  4. FROM employee WHERE emp_id <= 6;

该查询的结果与上一节 COALESCE 函数示例相同。
MySQL 提供了类似于 NVL2 的 IF(expr1, expr2, expr3) 函数。它接受三个参数,如果第一个参数为真(expr1 <> 0 并且 expr1 不为空),返回第二个参数的值;否则,返回第三个参数的值。以下查询同样返回员工的总收入:

  1. -- MySQL 实现
  2. SELECT emp_name,
  3. IF(bonus, salary*12 + bonus, salary*12) AS "年收入"
  4. FROM employee WHERE emp_id <= 6;

该查询的结果与上一节 COALESCE 函数示例相同。
SQL Server 提供了 ISNULL (expr1, expr2) 函数。该函数用于返回第一个非空的参数值,相当于只有两个参数的 COALESCE 函数。以下查询也返回员工的总收入:

  1. -- SQL Server 实现
  2. SELECT emp_name,
  3. salary*12 + ISNULL(bonus, 0) AS "年收入"
  4. FROM employee WHERE emp_id <= 6;

该查询的结果与上一节 COALESCE 函数示例相同。

分组与聚合函数中的空值

GROUP BY 子句、DISTINCT 运算符将所有的空值视为相同,因此将它们分为一组。虽然许多员工没有奖金,以下查询的结果中只有一个 NULL 值:

  1. SELECT DISTINCT bonus FROM employee;
  2. SELECT bonus FROM employee GROUP BY bonus;

该查询的结果如下:

  1. bonus |
  2. --------|
  3. 10000.00|
  4. 8000.00|
  5. [NULL]|
  6. 5000.00|
  7. 6000.00|
  8. 2000.00|
  9. 1500.00|

另外,在第 12 篇中我们介绍了常见的聚合函数(AVG、SUM、COUNT 等),聚合函数通常会忽略空值。

NOT IN 中的空值

IN 操作符用于查询位于列表之中的数据,NOT IN 用于查询不位于列表中的数据。以下是一个 IN 操作符的示例:
SELECT empid FROM employee WHERE emp_id IN (1, 2, 3, NULL); emp_id| ———| 1| 2| 3| 复制
该查询的结果返回了 3 条记录,因为他们的员工编号位于列表之中。接下来使用 NOT IN 查询不在列表中的数据:
SELECT emp_id FROM employee WHERE emp_id NOT IN (1, 2, 3, NULL); emp_id|
———|_ 复制
该查询没有返回任何结果。原因在于 IN 操作符使用等号(=)依次与列表中的数据进行比较,该查询等价于以下形式:
SELECT emp_id FROM employee WHERE emp_id != 1 AND emp_id != 2 AND emp_id != 3 AND emp_id != NULL; 复制
任何值与 NULL 比较的结果都是未知;因此没有数据满足该条件,也就没有返回结果。
为了避免各种情况下空值可能带来的问题,可以利用 SQL 提供的空值处理函数将 NULL 值转换为其他数据。

小结

空值(NULL)是 SQL 中容易被忽略的地方,经常会导致一些不可预知的错误结果。需要注意 SQL 在各个子句和函数中对于空值的处理方式。一个比较通用的解决方法就是利用 COALESCE 函数或者 CASE 表达式将这些空值转换成确定的数据。
练习题:假设下面的 emp_contact 表中存储的是员工的联系电话,包括工作电话、移动电话、家庭电话以及紧急联系人电话:
CREATE TABLE emp_contact( emp_id INT NOT NULL PRIMARY KEY, work_phone VARCHAR(20), mobile_phone VARCHAR(20), home_phone VARCHAR(20), emergency_phone VARCHAR(20) ); INSERT INTO emp_contact VALUES (1, ‘010-61231111’, NULL, NULL, NULL); INSERT INTO emp_contact VALUES (2, NULL, ‘13222222222’, ‘010-61232222’, ‘13123456789’); INSERT INTO emp_contact VALUES (3, NULL, NULL, NULL, ‘13123450000’); INSERT INTO emp_contact VALUES (4, NULL, NULL, ‘010-61234444’, ‘13123457777’); 复制
现在需要找出每个员工的联系电话,规则是先找移动电话;如果没有,再找工作电话;如果没有,再找家庭电话;最后找紧急联系人电话。试试你可以写出几种查询方法?