CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • 前言
  • 第1章 一点背景知识
  • 第2章 创建和填充数据库
  • 第3章 查询入门
  • 第4章 数据过滤
  • 第5章 多表查询
  • 第6章 集合操作
  • 第7章 数据生成、转换与操作
  • 第8章 分组和聚合
  • 第9章 子查询
  • 第10章 再谈连接
  • 第11章 条件逻辑
    • 什么是条件逻辑?
    • CASE表达式
      • 搜索型CASE表达式
      • 简单CASE表达式
    • CASE表达式示例
      • 结果集转换
      • 选择性聚合
      • 检查是否存在
      • 除零错误
      • 条件更新
      • 处理空值
    • 测试你的知识
      • 练习11 - 1
      • 练习11 - 2
  • 第12章 事务
  • 第13章 索引与约束
  • 第14章 视图
  • 第15章 元数据
  • 附录A 示例数据库的实体关系图
  • 附录B MySQL对SQL语言的扩展
  • 附录C 练习题答案
目录

第11章 条件逻辑

# 第11章 条件逻辑

在某些情形下,你可能希望SQL逻辑能依据特定列或表达式的值,朝不同方向进行分支处理。本章聚焦于如何编写能够在执行过程中,依据所遇到的数据而展现出不同行为的语句。

# 什么是条件逻辑?

条件逻辑指的是程序在执行期间,从多条路径中选择其一的能力。举例来说,在查询客户信息时,你可能会依据遇到的客户类型,从individual表中检索fname/lname列,或者从business表中检索name列。使用外连接(outer joins)的话,你可以返回两个字符串,让调用者去决定使用哪一个,就像这样:

mysql> SELECT c.cust_id,
    ->        c.fed_id,
    ->        c.cust_type_cd,
    ->        CONCAT(i.fname, ' ', i.lname) indiv_name,
    ->        b.name business_name
    -> FROM customer c
    -> LEFT OUTER JOIN individual i
    -> ON c.cust_id = i.cust_id
    -> LEFT OUTER JOIN business b
    -> ON c.cust_id = b.cust_id;
+---------+-------------+--------------+-----------------+------------------------+
| cust_id | fed_id      | cust_type_cd | indiv_name      | business_name          |
+---------+-------------+--------------+-----------------+------------------------+
| 1       | 111-11-1111 | I            | James Hadley    | NULL                   |
| 2       | 222-22-2222 | I            | Susan Tingley   | NULL                   |
| 3       | 333-33-3333 | I            | Frank Tucker    | NULL                   |
| 4       | 444-44-4444 | I            | John Hayward    | NULL                   |
| 5       | 555-55-5555 | I            | Charles Frasier | NULL                   |
| 6       | 666-66-6666 | I            | John Spencer    | NULL                   |
| 7       | 777-77-7777 | I            | Margaret Young  | NULL                   |
| 8       | 888-88-8888 | I            | Louis Blake     | NULL                   |
| 9       | 999-99-9999 | I            | Richard Farley  | NULL                   |
| 10      | 04-1111111  | B            | NULL            | Chilton Engineering    |
| 11      | 04-2222222  | B            | NULL            | Northeast Cooling Inc. |
| 12      | 04-3333333  | B            | NULL            | Superior Auto Body     |
| 13      | 04-4444444  | B            | NULL            | AAA Insurance Inc.     |
+---------+-------------+--------------+-----------------+------------------------+
13 rows in set (0.13 sec) 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

调用者可以查看cust_type_cd列的值,然后决定使用indiv_name列还是business_name列。不过,你也可以借助CASE表达式运用条件逻辑,来判定客户类型并返回合适的字符串,如下所示:

mysql> SELECT c.cust_id,
    ->        c.fed_id,
    ->        CASE
    ->            WHEN c.cust_type_cd = 'I'
    ->                THEN CONCAT(i.fname, ' ', i.lname)
    ->            WHEN c.cust_type_cd = 'B'
    ->                THEN b.name
    ->            ELSE 'Unknown'
    ->        END name
    -> FROM customer c
    -> LEFT OUTER JOIN individual i
    -> ON c.cust_id = i.cust_id
    -> LEFT OUTER JOIN business b
    -> ON c.cust_id = b.cust_id;
+---------+-------------+------------------------+
| cust_id | fed_id      | name                   |
+---------+-------------+------------------------+
| 1       | 111-11-1111 | James Hadley           |
| 2       | 222-22-2222 | Susan Tingley          |
| 3       | 333-33-3333 | Frank Tucker           |
| 4       | 444-44-4444 | John Hayward           |
| 5       | 555-55-5555 | Charles Frasier        |
| 6       | 666-66-6666 | John Spencer           |
| 7       | 777-77-7777 | Margaret Young         |
| 8       | 888-88-8888 | Louis Blake            |
| 9       | 999-99-9999 | Richard Farley         |
| 10      | 04-1111111  | Chilton Engineering    |
| 11      | 04-2222222  | Northeast Cooling Inc. |
| 12      | 04-3333333  | Superior Auto Body     |
| 13      | 04-4444444  | AAA Insurance Inc.     |
+---------+-------------+------------------------+
13 rows in set (0.00 sec)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

这个版本的查询返回一个名为name的单列,该列由查询第二行开始的CASE表达式生成。在这个例子中,CASE表达式会检查cust_type_cd列的值,然后返回个人的名和姓,或者企业名称。

# CASE表达式

所有主流数据库服务器都包含内置函数,这些函数旨在模仿大多数编程语言中的if-then-else语句(例如,Oracle的decode()函数、MySQL的if()函数以及SQL Server的coalesce()函数)。CASE表达式同样是为了实现if-then-else逻辑而设计的,但相较于内置函数,它具有两个优势:

  • CASE表达式是SQL标准(SQL92版本)的一部分,并且已在Oracle数据库、SQL Server、MySQL、Sybase、PostgreSQL、IBM UDB等数据库中得以实现。
  • CASE表达式内置于SQL语法中,可以在SELECT、INSERT、UPDATE和DELETE语句中使用。

接下来的两个小节将介绍两种不同类型的CASE表达式,然后为你展示一些实际运用CASE表达式的示例。

# 搜索型CASE表达式

本章前面展示的CASE表达式属于搜索型CASE表达式,其语法如下:

CASE
    WHEN C1 THEN E1
    WHEN C2 THEN E2
    ...
    WHEN CN THEN EN
    [ELSE ED]
END
1
2
3
4
5
6
7

在上述定义中,符号C1、C2、...、CN代表条件,符号E1、E2、...、EN代表CASE表达式要返回的表达式。如果WHEN子句中的条件求值结果为真,那么CASE表达式就会返回相应的表达式。此外,符号ED代表默认表达式,当C1、C2、...、CN中没有一个条件求值为真时,CASE表达式将返回该默认表达式(ELSE子句是可选的,所以用方括号括起来)。各个WHEN子句返回的所有表达式必须求值为相同的数据类型(例如,日期、数字、可变长度字符串varchar )。

以下是一个搜索型CASE表达式的示例:

CASE
    WHEN employee.title = 'Head Teller' THEN 'Head Teller'
    WHEN employee.title = 'Teller'
        AND YEAR(employee.start_date) > 2007 THEN 'Teller Trainee'
    WHEN employee.title = 'Teller'
        AND YEAR(employee.start_date) < 2006 THEN 'Experienced Teller'
    WHEN employee.title = 'Teller' THEN 'Teller'
    ELSE 'Non-Teller'
END
1
2
3
4
5
6
7
8
9

这个CASE表达式返回一个字符串,可用于确定小时工资标准、打印员工名牌等等。在计算CASE表达式时,WHEN子句会按照从上到下的顺序进行求值;一旦某个WHEN子句中的条件求值为真,就会返回相应的表达式,并且剩余的WHEN子句将被忽略。如果没有一个WHEN子句的条件求值为真,那么就会返回ELSE子句中的表达式。

尽管前面的示例返回的是字符串表达式,但要记住,CASE表达式可以返回任何类型的表达式,其中也包括子查询。下面是本章前面个人/企业名称查询的另一个版本,它使用子查询而非外连接,从individual表和business表中检索数据:

mysql> SELECT c.cust_id,
    ->        c.fed_id,
    ->        CASE
    ->            WHEN c.cust_type_cd = 'I' THEN
                (
                    SELECT CONCAT(i.fname, ' ', i.lname)
                    FROM individual i
                    WHERE i.cust_id = c.cust_id
                )
    ->            WHEN c.cust_type_cd = 'B' THEN
                (
                    SELECT b.name
                    FROM business b
                    WHERE b.cust_id = c.cust_id
                )
    ->            ELSE 'Unknown'
    ->        END name
    -> FROM customer c;
+---------+-------------+------------------------+
| cust_id | fed_id      | name                   |
+---------+-------------+------------------------+
| 1       | 111-11-1111 | James Hadley           |
| 2       | 222-22-2222 | Susan Tingley          |
| 3       | 333-33-3333 | Frank Tucker           |
| 4       | 444-44-4444 | John Hayward           |
| 5       | 555-55-5555 | Charles Frasier        |
| 6       | 666-66-6666 | John Spencer           |
| 7       | 777-77-7777 | Margaret Young         |
| 8       | 888-88-8888 | Louis Blake            |
| 9       | 999-99-9999 | Richard Farley         |
| 10      | 04-1111111  | Chilton Engineering    |
| 11      | 04-2222222  | Northeast Cooling Inc. |
| 12      | 04-3333333  | Superior Auto Body     |
| 13      | 04-4444444  | AAA Insurance Inc.     |
+---------+-------------+------------------------+
13 rows in set (0.01 sec)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

这个版本的查询在FROM子句中仅包含customer表,并使用相关子查询为每个客户检索合适的名称。相较于本章前面的外连接版本,我更倾向于这个版本,因为服务器仅在有需要时才会从individual表和business表中读取数据,而不是始终连接所有三个表。

# 简单CASE表达式

简单CASE表达式与搜索型CASE表达式非常相似,但灵活性稍差。其语法如下:

CASE V0
    WHEN V1 THEN E1
    WHEN V2 THEN E2
    ...
    WHEN VN THEN EN
    [ELSE ED]
END
1
2
3
4
5
6
7

在上述定义中,V0代表一个值,符号V1、V2、...、VN代表要与V0进行比较的值。符号E1、E2、...、EN代表CASE表达式要返回的表达式,ED代表当V1、V2、...、VN中没有一个值与V0匹配时要返回的表达式。

以下是一个简单CASE表达式的示例:

CASE customer.cust_type_cd
    WHEN 'I' THEN
        (SELECT  CONCAT(i.fname,  '   ',  i.lname)
         FROM  individual  I
         WHERE  i.cust_id  =  customer.cust_id)
    WHEN 'B' THEN
        (SELECT  b.name
         FROM  business  b
         WHERE  b.cust_id  =  customer.cust_id)
    ELSE  'Unknown Customer Type'
END
1
2
3
4
5
6
7
8
9
10
11

简单CASE表达式的功能比搜索型CASE表达式弱,因为你不能指定自己的条件,而是内置了相等条件。为了让你明白我的意思,下面是一个与前面简单CASE表达式逻辑相同的搜索型CASE表达式:

CASE
    WHEN customer.cust_type_cd  =   'I'   THEN
        (SELECT  CONCAT(i.fname,  '   ',  i.lname)
         FROM  individual  I
         WHERE  i.cust_id  =  customer.cust_id)
    WHEN customer.cust_type_cd  =   'B'  THEN
        (SELECT  b.name
         FROM  business  b
         WHERE  b.cust_id  =  customer.cust_id)
    ELSE  'Unknown Customer Type'
END
1
2
3
4
5
6
7
8
9
10
11

使用搜索型CASE表达式,你可以构建范围条件、不等条件,以及使用AND/OR/NOT的多部分条件。因此,除了最简单的逻辑外,我建议在所有情况下都使用搜索型CASE表达式。

# CASE表达式示例

以下部分展示了各种示例,说明了条件逻辑在SQL语句中的实用性。

# 结果集转换

你可能遇到过这样的情况:对一组有限的值(例如一周中的每一天)进行聚合操作,但你希望结果集包含一行数据,每个值对应一列,而不是每个值对应一行。例如,假设要求你编写一个查询,显示2000年到2005年开设的账户数量:

mysql> SELECT YEAR(open_date) year,
    ->        COUNT(*) how_many
    -> FROM account
    -> WHERE open_date > '1999-12-31'
    ->   AND open_date < '2006-01-01'
    -> GROUP BY YEAR(open_date);
+------+----------+
| year | how_many |
+------+----------+
| 2000 | 3        |
| 2001 | 4        |
| 2002 | 5        |
| 2003 | 3        |
| 2004 | 9        |
+------+----------+
5 rows in set (0.00 sec)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

然而,你还被要求返回一行数据,包含六列(数据范围内的每一年对应一列)。为了将这个结果集转换为一行,你需要创建六列,并且在每一列中,只对与该年份相关的行进行求和:

mysql> SELECT
    ->     SUM(
            CASE
                WHEN EXTRACT(YEAR FROM open_date) = 2000 THEN 1
                ELSE 0
            END
        ) year_2000,
    ->     SUM(
            CASE
                WHEN EXTRACT(YEAR FROM open_date) = 2001 THEN 1
                ELSE 0
            END
        ) year_2001,
    ->     SUM(
            CASE
                WHEN EXTRACT(YEAR FROM open_date) = 2002 THEN 1
                ELSE 0
            END
        ) year_2002,
    ->     SUM(
            CASE
                WHEN EXTRACT(YEAR FROM open_date) = 2003 THEN 1
                ELSE 0
            END
        ) year_2003,
    ->     SUM(
            CASE
                WHEN EXTRACT(YEAR FROM open_date) = 2004 THEN 1
                ELSE 0
            END
        ) year_2004,
    ->     SUM(
            CASE
                WHEN EXTRACT(YEAR FROM open_date) = 2005 THEN 1
                ELSE 0
            END
        ) year_2005
    -> FROM account
    -> WHERE open_date > '1999-12-31' AND open_date < '2006-01-01';
+-----------+-----------+-----------+-----------+-----------+-----------+
| year_2000 | year_2001 | year_2002 | year_2003 | year_2004 | year_2005 |
+-----------+-----------+-----------+-----------+-----------+-----------+
| 3         | 4         | 5         | 3         | 9         | 0         |
+-----------+-----------+-----------+-----------+-----------+-----------+
1 row in set (0.01 sec)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

前面查询中的六列,除了年份值不同,其他都相同。当EXTRACT()函数返回该列所需的年份时,CASE表达式返回值1;否则,返回0。对2000年以来开设的所有账户进行求和时,每一列返回的是该年份开设的账户数量。显然,这种转换只适用于少量的值;如果为1905年以来的每一年都生成一列,很快就会变得繁琐。

虽然这对本书来说有点高级,但值得指出的是,SQL Server和Oracle Database 11g都包含PIVOT子句,专门用于这类查询。

# 选择性聚合

在第9章中,我给出了一个示例的部分解决方案,该示例展示了如何找出账户余额与交易表中的原始数据不一致的账户。给出部分解决方案是因为完整的解决方案需要用到条件逻辑,现在所有要素都已具备,可以完成这项任务了。以下是我在第9章结束时留下的内容:

SELECT  CONCAT('ALERT!  :  Account  #',  a.account_id, '   Has  Incorrect  Balance!')
FROM  account  a
WHERE  (a.avail_balance,  a.pending_balance)  <>
(SELECT  SUM(<expression  to  generate  available  balance>), SUM(<expression  to  generate  pending  balance>)
FROM  transaction  t
WHERE  t.account_id  =  a.account_id);
1
2
3
4
5
6

该查询对交易表使用了一个相关子查询,将特定账户的各个交易金额加总。在对交易金额求和时,需要考虑以下两个问题:

  • 交易金额始终为正数,所以你需要查看交易类型,以判断该交易是借方(debit)还是贷方(credit),对于借方交易要反转符号(乘以 -1 )。
  • 如果资金可用日期(funds_avail_date)列中的日期大于当前日期,该笔交易应计入未决余额(pending balance)总计,但不计入可用余额(available balance)总计。

虽然部分交易需要从可用余额中排除,但所有交易都包含在未决余额中,这使得未决余额的计算相对简单。以下是用于计算未决余额的CASE表达式:

CASE
WHEN  transaction.txn_type_cd  =  'DBT' THEN  transaction.amount  *  −1
ELSE  transaction.amount END
1
2
3

因此,对于借方交易,所有交易金额都乘以 -1,而对于贷方交易则保持不变。同样的逻辑也适用于可用余额的计算,但仅应包含已到账的交易。所以,用于计算可用余额的CASE表达式多了一个WHEN子句:

CASE
WHEN  transaction.funds_avail_date  >  CURRENT_TIMESTAMP() THEN  0
WHEN  transaction.txn_type_cd  =  'DBT' THEN  transaction.amount  *  −1
ELSE  transaction.amount END
1
2
3
4

有了第一个WHEN子句,未到账的资金(比如尚未兑现的支票)对总和的贡献为0美元。以下是包含两个CASE表达式的最终查询:

SELECT  CONCAT('ALERT!  :  Account  #',  a.account_id, '   Has  Incorrect  Balance!')
FROM  account  a
WHERE  (a.avail_balance,  a.pending_balance)  <> (SELECT
SUM(CASE
WHEN  t.funds_avail_date  >  CURRENT_TIMESTAMP() THEN  0
WHEN  t.txn_type_cd  =  'DBT' THEN  t.amount  *  −1
ELSE  t.amount
END), SUM(CASE
WHEN  t.txn_type_cd  =  'DBT' THEN  t.amount  *  −1
ELSE  t.amount END)
FROM  transaction  t
WHERE  t.account_id  =  a.account_id);
1
2
3
4
5
6
7
8
9
10
11
12

通过使用条件逻辑,两个CASE表达式为SUM()聚合函数提供经过处理的数据,从而实现对相应金额的求和。

# 检查是否存在

有时,你可能想要确定两个实体之间是否存在某种关系,而无需考虑数量。例如,你可能想知道某个客户是否拥有支票账户或储蓄账户,但并不关心该客户每种类型的账户有多少个。下面的查询使用多个CASE表达式生成两个输出列,一个显示客户是否拥有支票账户,另一个显示客户是否拥有储蓄账户:

mysql> SELECT c.cust_id,
    ->        c.fed_id,
    ->        c.cust_type_cd,
    ->        CASE
    ->            WHEN EXISTS (
                    SELECT 1
                    FROM account a
                    WHERE a.cust_id = c.cust_id
                      AND a.product_cd = 'CHK'
                ) THEN 'Y'
            ELSE 'N'
        END has_checking,
    ->        CASE
    ->            WHEN EXISTS (
                    SELECT 1
                    FROM account a
                    WHERE a.cust_id = c.cust_id
                      AND a.product_cd = 'SAV'
                ) THEN 'Y'
            ELSE 'N'
        END has_savings
    -> FROM customer c;
+---------+-------------+--------------+--------------+-------------+
| cust_id | fed_id      | cust_type_cd | has_checking | has_savings |
+---------+-------------+--------------+--------------+-------------+
| 1       | 111-11-1111 | I            | Y            | Y           |
| 2       | 222-22-2222 | I            | Y            | Y           |
| 3       | 333-33-3333 | I            | Y            | N           |
| 4       | 444-44-4444 | I            | Y            | Y           |
| 5       | 555-55-5555 | I            | Y            | N           |
| 6       | 666-66-6666 | I            | Y            | N           |
| 7       | 777-77-7777 | I            | N            | N           |
| 8       | 888-88-8888 | I            | Y            | Y           |
| 9       | 999-99-9999 | I            | Y            | N           |
| 10      | 04-1111111  | B            | Y            | N           |
| 11      | 04-2222222  | B            | N            | N           |
| 12      | 04-3333333  | B            | Y            | N           |
| 13      | 04-4444444  | B            | N            | N           |
+---------+-------------+--------------+--------------+-------------+
13 rows in set (0.00 sec) 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

每个CASE表达式都包含一个针对账户表的相关子查询;一个用于查找支票账户,另一个用于查找储蓄账户。由于每个WHEN子句都使用了EXISTS运算符,只要客户至少拥有一个所需类型的账户,条件就会被评估为真。

在其他情况下,你可能会关心遇到的行数,但仅在一定程度上。例如,下一个查询使用一个简单的CASE表达式来计算每个客户的账户数量,然后返回“None”、“1”、“2”或“3+”:

mysql> SELECT c.cust_id,
    ->        c.fed_id,
    ->        c.cust_type_cd,
    ->        CASE (
                    SELECT COUNT(*)
                    FROM account a
                    WHERE a.cust_id = c.cust_id
                )
            WHEN 0 THEN 'None'
            WHEN 1 THEN '1'
            WHEN 2 THEN '2'
            ELSE '3+'
        END num_accounts
    -> FROM customer c;
+---------+-------------+--------------+--------------+
| cust_id | fed_id      | cust_type_cd | num_accounts |
+---------+-------------+--------------+--------------+
| 1       | 111-11-1111 | I            | 3+           |
| 2       | 222-22-2222 | I            | 2            |
| 3       | 333-33-3333 | I            | 2            |
| 4       | 444-44-4444 | I            | 3+           |
| 5       | 555-55-5555 | I            | 1            |
| 6       | 666-66-6666 | I            | 2            |
| 7       | 777-77-7777 | I            | 1            |
| 8       | 888-88-8888 | I            | 2            |
| 9       | 999-99-9999 | I            | 3+           |
| 10      | 04-1111111  | B            | 2            |
| 11      | 04-2222222  | B            | 1            |
| 12      | 04-3333333  | B            | 1            |
| 13      | 04-4444444  | B            | 1            |
+---------+-------------+--------------+--------------+
13 rows in set (0.01 sec)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

对于这个查询,我不想区分拥有两个以上账户的客户,所以CASE表达式只是创建了一个“3+”类别。如果你正在寻找可能需要联系以开设新银行账户的客户,这样的查询可能会很有用。

# 除零错误

在进行包含除法的计算时,你始终要注意确保分母永远不为零。有些数据库服务器(如Oracle数据库)在遇到分母为零时会抛出错误,而MySQL则只是将计算结果设置为NULL,如下所示:

mysql> SELECT 100 / 0;
+---------+
| 100 / 0 |
+---------+
| NULL    |
+---------+
1 row in set (0.00 sec)
1
2
3
4
5
6
7

为了避免计算出错,或者更糟糕的是,避免计算结果被莫名其妙地设为NULL,你应该使用条件逻辑将所有分母包裹起来,如下所示:

mysql> SELECT a.cust_id,
    ->        a.product_cd,
    ->        a.avail_balance /
        CASE
            WHEN prod_tots.tot_balance = 0 THEN 1
            ELSE prod_tots.tot_balance
        END percent_of_total
    -> FROM account a
    -> INNER JOIN (
        SELECT a.product_cd,
               SUM(a.avail_balance) tot_balance
        FROM account a
        GROUP BY a.product_cd
    ) prod_tots
    -> ON a.product_cd = prod_tots.product_cd;
+---------+------------+------------------+
| cust_id | product_cd | percent_of_total |
+---------+------------+------------------+
| 10      | BUS        | 0.000000         |
| 11      | BUS        | 1.000000         |
| 1       | CD         | 0.153846         |
| 6       | CD         | 0.512821         |
| 7       | CD         | 0.256410         |
| 9       | CD         | 0.076923         |
| 1       | CHK        | 0.014488         |
| 2       | CHK        | 0.030928         |
| 3       | CHK        | 0.014488         |
| 4       | CHK        | 0.007316         |
| 5       | CHK        | 0.030654         |
| 6       | CHK        | 0.001676         |
| 8       | CHK        | 0.047764         |
| 9       | CHK        | 0.001721         |
| 10      | CHK        | 0.322911         |
| 12      | CHK        | 0.528052         |
| 3       | MM         | 0.129802         |
| 4       | MM         | 0.321915         |
| 9       | MM         | 0.548282         |
| 1       | SAV        | 0.269431         |
| 2       | SAV        | 0.107773         |
| 4       | SAV        | 0.413723         |
| 8       | SAV        | 0.209073         |
| 13      | SBL        | 1.000000         |
+---------+------------+------------------+
24 rows in set (0.13 sec)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

这个查询计算了每个账户余额与相同产品类型的所有账户总余额的比率。由于某些产品类型(如商业贷款)如果所有贷款目前都已全额偿还,总余额可能为零,因此最好包含CASE表达式以确保分母永远不为零。

# 条件更新

在更新表中的行时,有时需要确定某些列应设置的值。例如,插入新交易记录后,需要修改账户表中的可用余额(avail_balance)、待处理余额(pending_balance)和上次活动日期(last_activity_date)列。虽然最后两列很容易更新,但要正确修改可用余额列,需要通过查看交易表中的资金可用日期(funds_avail_date)列,来确定交易资金是否能立即使用。假设刚刚插入了交易ID为999的记录,可以使用以下更新语句来修改账户表中的这三列:

UPDATE account
SET last_activity_date = CURRENT_TIMESTAMP(),
    pending_balance = pending_balance + (SELECT t.amount *
                                          CASE t.txn_type_cd
                                              WHEN 'DBT' THEN -1
                                              ELSE 1
                                              END
                                      FROM transaction t
                                      WHERE t.txn_id = 999),
    avail_balance = avail_balance + (SELECT
                                          CASE
                                              WHEN t.funds_avail_date > CURRENT_TIMESTAMP() THEN 0
                                              ELSE t.amount *
                                                   CASE t.txn_type_cd
                                                       WHEN 'DBT' THEN -1
                                                       ELSE 1
                                                       END
                                              END
                                      FROM transaction t
                                      WHERE t.txn_id = 999)
WHERE account.account_id = (SELECT t.account_id
                            FROM transaction t
                            WHERE t.txn_id = 999);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

此语句总共包含三个CASE表达式:其中两个(第5行和第13行)用于反转借方交易的交易金额符号,第三个CASE表达式(第10行)用于检查资金可用日期。如果日期在未来,则向可用余额添加零;否则,添加交易金额。

# 处理空值

如果某列的值未知,在表中存储空值是合适的,但检索空值用于显示或参与表达式运算并不总是合适的。例如,可能希望在数据输入屏幕上显示“未知”一词,而不是让字段留空。在检索数据时,可以使用CASE表达式,在值为空时替换字符串,如下所示:

SELECT emp_id, fname, lname, CASE
    WHEN title IS NULL THEN 'Unknown'
    ELSE title
    END
FROM employee;
1
2
3
4
5

对于计算,空值通常会导致结果为空,如下所示:

mysql> SELECT (7 * 5) / ((3 + 14) * null);
+----------------------------------+
| (7 * 5) / ((3 + 14) * null)      |
+----------------------------------+
| NULL                             |
+----------------------------------+
1 row in set (0.08 sec)
1
2
3
4
5
6
7

在进行计算时,CASE表达式可用于将空值转换为一个数字(通常是0或1),使计算能够产生非空结果。例如,如果进行的计算包含account.avail_balance列,可以为已创建但尚未注资的账户替换为0(如果进行加法或减法运算)或1(如果进行乘法或除法运算):

SELECT <some calculation> + CASE
    WHEN avail_balance IS NULL THEN 0
    ELSE avail_balance
    END + <rest of calculation>
...
1
2
3
4
5

如果允许数字列包含空值,在任何包含该列的计算中使用条件逻辑通常是个好主意,这样结果才有用。

# 测试你的知识

通过以下示例挑战自己解决条件逻辑问题的能力。完成后,将你的解决方案与附录C中的内容进行比较。

# 练习11 - 1

重写以下使用简单CASE表达式的查询,使其使用搜索CASE表达式获得相同结果。尽量使用最少的WHEN子句。

SELECT emp_id, CASE title
    WHEN 'President' THEN 'Management'
    WHEN 'Vice President' THEN 'Management'
    WHEN 'Treasurer' THEN 'Management'
    WHEN 'Loan Manager' THEN 'Management'
    WHEN 'Operations Manager' THEN 'Operations'
    WHEN 'Head Teller' THEN 'Operations'
    WHEN 'Teller' THEN 'Operations'
    ELSE 'Unknown'
    END
FROM employee;
1
2
3
4
5
6
7
8
9
10
11

# 练习11 - 2

重写以下查询,使结果集包含一行四列(每个分支一列)。将这四列命名为branch_1到branch_4。

mysql> SELECT open_branch_id, COUNT(*)
    -> FROM account
    -> GROUP BY open_branch_id;
+---------------+----------+
| open_branch_id | COUNT(*) |
+---------------+----------+
| 1             | 8        |
| 2             | 7        |
| 3             | 3        |
| 4             | 6        |
+---------------+----------+
4 rows in set (0.00 sec)
1
2
3
4
5
6
7
8
9
10
11
12
第10章 再谈连接
第12章 事务

← 第10章 再谈连接 第12章 事务→

最近更新
01
C++语言面试问题集锦 目录与说明
03-27
02
第四章 Lambda函数
03-27
03
第二章 关键字static及其不同用法
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式