第4章 数据过滤
# 第4章 数据过滤
有时候,你可能需要处理表中的每一行数据,例如:
- 从用于暂存新数据仓库输入数据的表中清除所有数据。
- 在添加新列后修改表中的所有行。
- 从消息队列表中检索所有行。
在这些情况下,你的SQL语句不需要WHERE
子句,因为你无需排除任何行。然而,在大多数时候,你会希望将关注范围缩小到表中部分行。因此,所有的SQL数据操作语句(除了INSERT
语句)都包含一个可选的WHERE
子句,用于设置过滤条件,限制SQL语句作用的行数。此外,SELECT
语句还包含一个HAVING
子句,可以在其中设置与分组数据相关的过滤条件。本章将探讨你可以在SELECT
、UPDATE
和DELETE
语句的WHERE
子句中使用的各种过滤条件;第8章将探讨SELECT
语句HAVING
子句中过滤条件的使用。
# 条件求值
WHERE
子句可以包含一个或多个条件,这些条件由AND
和OR
运算符分隔。如果多个条件仅由AND
运算符分隔,那么只有当所有条件都为真时,该行才会被包含在结果集中。考虑以下WHERE
子句:
WHERE title = 'Teller' AND start_date < '2007-01-01'
根据这两个条件,只有在2007年之前开始在银行工作的出纳才会被包含在内(换个角度看,任何不是出纳,或者在2007年及以后开始在银行工作的员工都将被排除在外)。虽然这个例子只使用了两个条件,但无论你的WHERE
子句中有多少个条件,只要它们由AND
运算符分隔,就必须全部为真,该行才会被包含在结果集中。
然而,如果WHERE
子句中的所有条件都由OR
运算符分隔,那么只要有一个条件为真,该行就会被包含在结果集中。考虑以下两个条件:
WHERE title = 'Teller' OR start_date < '2007-01-01'
现在,对于给定的员工行,有多种情况会使其被包含在结果集中:
- 该员工是出纳,且在2007年之前入职。
- 该员工是出纳,且在2007年1月1日之后入职。
- 该员工不是出纳,但在2007年之前入职。
表4-1展示了包含两个由OR
运算符分隔条件的WHERE
子句可能的求值结果。
表4-1 使用OR的双条件求值
中间结果 | 最终结果 |
---|---|
WHERE true OR true | True |
WHERE true OR false | True |
WHERE false OR true | True |
WHERE false OR false | False |
在上述例子中,只有当员工不是出纳且在2007年1月1日或之后入职时,该行才会被排除在结果集之外。
# 使用括号
如果你的WHERE
子句包含三个或更多同时使用AND
和OR
运算符的条件,你应该使用括号,以便让数据库服务器和其他阅读你代码的人都能清楚你的意图。下面的WHERE
子句扩展了前面的例子,增加了检查员工是否仍在银行工作的条件:
WHERE end_date IS NULL
AND (title = 'Teller' OR start_date < '2007-01-01')
2
现在有三个条件;要使某一行进入最终结果集,第一个条件必须为真,并且第二个或第三个条件(或两者)必须为真。表4-2展示了这个WHERE
子句可能的求值结果。
表4-2 使用AND、OR的三条件求值
中间结果 | 最终结果 |
---|---|
WHERE true AND (true OR true) | True |
WHERE true AND (true OR false) | True |
WHERE true AND (false OR true) | True |
WHERE true AND (false OR false) | False |
WHERE false AND (true OR true) | False |
WHERE false AND (true OR false) | False |
WHERE false AND (false OR true) | False |
WHERE false AND (false OR false) | False |
可以看到,WHERE
子句中的条件越多,服务器需要求值的组合就越多。在这种情况下,八种组合中只有三种最终结果为真。
# 使用NOT运算符
希望前面的三条件示例比较容易理解。不过,考虑以下修改:
WHERE end_date IS NULL
AND NOT (title = 'Teller' OR start_date < '2007-01-01')
2
你发现与前面例子的不同之处了吗?我在第二行的AND
运算符后面添加了NOT
运算符。现在,我寻找的不是仍在职且是出纳或在2007年之前开始在银行工作的员工,而是仍在职且既不是出纳又在2007年或之后开始在银行工作的员工。表4-3展示了这个例子可能的求值结果。
表4-3 使用AND、OR和NOT的三条件求值
中间结果 | 最终结果 |
---|---|
WHERE true AND NOT (true OR true) | False |
WHERE true AND NOT (true OR false) | False |
WHERE true AND NOT (false OR true) | False |
WHERE true AND NOT (false OR false) | True |
WHERE false AND NOT (true OR true) | False |
WHERE false AND NOT (true OR false) | False |
WHERE false AND NOT (false OR true) | False |
WHERE false AND NOT (false OR false) | False |
虽然数据库服务器处理起来很容易,但对于人来说,评估包含NOT
运算符的WHERE
子句通常比较困难,这就是为什么你不太常遇到它。在这种情况下,你可以重写WHERE
子句以避免使用NOT
运算符:
WHERE end_date IS NULL
AND title != 'Teller'
AND start_date >= '2007-01-01'
2
3
我相信服务器对这两种写法没有偏好,但你理解这个版本的WHERE
子句可能会更容易一些。
# 构建条件
既然你已经了解了服务器如何对多个条件进行求值,现在让我们退一步,看看单个条件是由什么组成的。一个条件由一个或多个表达式与一个或多个运算符组合而成。表达式可以是以下任意一种:
- 一个数字。
- 表或视图中的一列。
- 一个字符串字面值,例如
'Teller'
。 - 一个内置函数,例如
concat('Learning', ' ', 'SQL')
。 - 一个子查询。
- 一个表达式列表,例如
('Teller', 'Head Teller', 'Operations Manager')
。
条件中使用的运算符包括:
- 比较运算符,例如
=
、!=
、<
、>
、<>
、LIKE
、IN
和BETWEEN
。 - 算术运算符,例如
+
、−
、*
和/
。
以下部分将展示如何组合这些表达式和运算符来构建各种类型的条件。
# 条件类型
有很多不同的方法可以过滤掉不需要的数据。你可以查找特定的值、值的集合或值的范围,以决定是否包含或排除某些数据,或者在处理字符串数据时,使用各种模式搜索技术来查找部分匹配的数据。接下来的四个小节将详细探讨每种条件类型。
# 等值条件
你编写或遇到的很大一部分过滤条件的形式为column = expression
,例如:
title = 'Teller'
fed_id = '111-11-1111'
amount = 375.25
dept_id = (SELECT dept_id FROM department WHERE name = 'Loans')
2
3
4
这样的条件被称为等值条件,因为它们使一个表达式等于另一个表达式。前三个例子将列与字面值(两个字符串和一个数字)进行比较,第四个例子将列与子查询返回的值进行比较。以下查询使用了两个等值条件;一个在ON
子句中(连接条件),另一个在WHERE
子句中(过滤条件):
mysql> SELECT pt.name product_type, p.name product
-> FROM product p INNER JOIN product_type pt
-> ON p.product_type_cd = pt.product_type_cd
-> WHERE pt.name = 'Customer Accounts';
+-----------------------+----------------------------------+
| product_type | product |
+-----------------------+----------------------------------+
| Customer Accounts | certificate of deposit |
| Customer Accounts | checking account |
| Customer Accounts | money market account |
| Customer Accounts | savings account |
+-----------------------+----------------------------------+
4 rows in set (0.08 sec)
2
3
4
5
6
7
8
9
10
11
12
13
这个查询展示了所有属于客户账户类型的产品。
# 不等值条件
另一种比较常见的条件类型是不等值条件,它表示两个表达式不相等。以下是将前面查询的WHERE
子句中的过滤条件改为不等值条件后的查询:
mysql> SELECT pt.name product_type, p.name product
-> FROM product p INNER JOIN product_type pt
-> ON p.product_type_cd = pt.product_type_cd
-> WHERE pt.name <> 'Customer Accounts';
+----------------------------------+----------------------------------+
| product_type | product |
+----------------------------------+----------------------------------+
| Individual and Business Loans | auto loan |
| Individual and Business Loans | business line of credit |
| Individual and Business Loans | home mortgage |
| Individual and Business Loans | small business loan |
+----------------------------------+----------------------------------+
4 rows in set (0.00 sec)
2
3
4
5
6
7
8
9
10
11
12
13
这个查询展示了所有不属于客户账户类型的产品。在构建不等值条件时,你可以选择使用!=
或<>
运算符。
# 使用等值条件修改数据
等值/不等值条件在修改数据时经常用到。例如,假设银行有每年清理旧账户行的政策。你的任务是从account
表中删除2002年关闭的账户行。一种解决方法如下:
DELETE FROM account
WHERE status = 'CLOSED' AND YEAR(close_date) = 2002;
2
这条语句包含两个等值条件:一个用于查找已关闭的账户,另一个用于检查2002年关闭的账户。
在编写
DELETE
和UPDATE
语句示例时,我尽量使每条语句都不会修改任何行。这样,当你执行这些语句时,你的数据将保持不变,并且SELECT
语句的输出将始终与本书中所示的一致。
由于MySQL会话默认处于自动提交模式(见第12章),如果我的某条语句修改了示例数据,你将无法回滚(撤销)所做的更改。当然,你可以对示例数据进行任何操作,包括清空数据并重新运行我提供的脚本,但我尽量保持数据的完整性。
# 范围条件
除了检查一个表达式是否等于(或不等于)另一个表达式之外,你还可以构建条件来检查某个表达式是否落在特定的范围内。在处理数值或时间数据时,这类条件很常见。考虑以下查询:
mysql> SELECT emp_id, fname, lname, start_date
-> FROM employee
-> WHERE start_date < '2007-01-01';
+-----------+-----------------+-------------------+--------------+
| emp_id | fname | lname | start_date |
+-----------+-----------------+-------------------+--------------+
| 1 | Michael | Smith | 2005-06-22 |
| 2 | Susan | Barker | 2006-09-12 |
| 3 | Robert | Tyler | 2005-02-09 |
| 4 | Susan | Hawthorne | 2006-04-24 |
| 8 | Sarah | Parker | 2006-12-02 |
| 9 | Jane | Grossman | 2006-05-03 |
| 10 | Paula | Roberts | 2006-07-27 |
| 11 | Thomas | Ziegler | 2004-10-23 |
| 13 | John | Blake | 2004-05-11 |
| 14 | Cindy | Mason | 2006-08-09 |
| 16 | Theresa | Markham | 2005-03-15 |
| 17 | Beth | Fowler | 2006-06-29 |
| 18 | Rick | Tulman | 2006-12-12 |
+-----------+-----------------+-------------------+--------------+
13 rows in set (0.15 sec)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
这个查询找出了2007年之前入职的所有员工。除了为入职日期指定上限外,你可能还想为入职日期指定下限:
mysql> SELECT emp_id, fname, lname, start_date
-> FROM employee
-> WHERE start_date < '2007-01-01'
-> AND start_date >= '2005-01-01';
+-----------+-----------------+-------------------+--------------+
| emp_id | fname | lname | start_date |
+-----------+-----------------+-------------------+--------------+
| 1 | Michael | Smith | 2005-06-22 |
| 2 | Susan | Barker | 2006-09-12 |
| 3 | Robert | Tyler | 2005-02-09 |
| 4 | Susan | Hawthorne | 2006-04-24 |
| 8 | Sarah | Parker | 2006-12-02 |
| 9 | Jane | Grossman | 2006-05-03 |
| 10 | Paula | Roberts | 2006-07-27 |
| 14 | Cindy | Mason | 2006-08-09 |
| 16 | Theresa | Markham | 2005-03-15 |
| 17 | Beth | Fowler | 2006-06-29 |
| 18 | Rick | Tulman | 2006-12-12 |
+-----------+-----------------+-------------------+--------------+
11 rows in set (0.00 sec)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
这个版本的查询检索出了2005年或2006年入职的所有员工。
# BETWEEN操作符
当你为范围同时设置了上限和下限时,可以选择使用一个利用BETWEEN
操作符的条件,而不是使用两个单独的条件,如下所示:
mysql> SELECT emp_id, fname, lname, start_date
-> FROM employee
-> WHERE start_date BETWEEN '2005-01-01' AND '2007-01-01';
+-----------+-----------------+-------------------+--------------+
| emp_id | fname | lname | start_date |
+-----------+-----------------+-------------------+--------------+
| 1 | Michael | Smith | 2005-06-22 |
| 2 | Susan | Barker | 2006-09-12 |
| 3 | Robert | Tyler | 2005-02-09 |
| 4 | Susan | Hawthorne | 2006-04-24 |
| 8 | Sarah | Parker | 2006-12-02 |
| 9 | Jane | Grossman | 2006-05-03 |
| 10 | Paula | Roberts | 2006-07-27 |
| 14 | Cindy | Mason | 2006-08-09 |
| 16 | Theresa | Markham | 2005-03-15 |
| 17 | Beth | Fowler | 2006-06-29 |
| 18 | Rick | Tulman | 2006-12-12 |
+-----------+-----------------+-------------------+--------------+
11 rows in set (0.03 sec)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
使用BETWEEN
操作符时,有几点需要注意。你应该始终先指定范围的下限(在BETWEEN
之后),再指定范围的上限(在AND
之后)。如果错误地先指定上限,会出现以下情况:
mysql> SELECT emp_id, fname, lname, start_date
-> FROM employee
-> WHERE start_date BETWEEN '2007-01-01' AND '2005-01-01';
Empty set (0.00 sec)
2
3
4
如你所见,没有返回任何数据。这是因为服务器实际上是使用<=
和>=
操作符,从你提供的单个条件生成两个条件,如下所示:
mysql> SELECT emp_id, fname, lname, start_date
-> FROM employee
-> WHERE start_date >= '2007-01-01'
-> AND start_date <= '2005-01-01';
Empty set (0.00 sec)
2
3
4
5
由于一个日期不可能既大于2007年1月1日,又小于2005年1月1日,所以该查询返回一个空集。这就引出了使用BETWEEN
时的第二个陷阱,即要记住上限和下限是包含边界值的,也就是说你提供的值包含在范围限制内。在这种情况下,我想将2005年1月1日指定为范围的下限,将2006年12月31日指定为上限,而不是2007年1月1日。尽管可能没有员工在2007年元旦开始为银行工作,但最好还是准确指定你想要的值。
除了日期,你还可以构建用于指定数字范围的条件。数值范围比较容易理解,如下所示:
mysql> SELECT account_id, product_cd, cust_id, avail_balance
-> FROM account
-> WHERE avail_balance BETWEEN 3000 AND 5000;
+--------------+-------------+----------+------------------+
| account_id | product_cd | cust_id | avail_balance |
+--------------+-------------+----------+------------------+
| 3 | CD | 1 | 3000.00 |
| 17 | CD | 7 | 5000.00 |
| 18 | CHK | 8 | 3487.19 |
+--------------+-------------+----------+------------------+
3 rows in set (0.10 sec)
2
3
4
5
6
7
8
9
10
11
该查询返回了所有可用余额在3000美元到5000美元之间的账户。同样,要确保先指定较小的金额。
# 字符串范围
虽然日期和数字的范围很容易理解,但你也可以构建用于搜索字符串范围的条件,不过这类条件在可视化方面有点困难。例如,假设你要搜索社会安全号码在特定范围内的客户。社会安全号码的格式是“XXX - XX - XXXX”,其中X是0到9的数字,你想找到社会安全号码在“500 - 00 - 0000”到“999 - 99 - 9999”之间的每一位客户。该语句如下所示:
mysql> SELECT cust_id, fed_id
-> FROM customer
-> WHERE cust_type_cd = 'I'
-> AND fed_id BETWEEN '500-00-0000' AND '999-99-9999';
+----------+------------------+
| cust_id | fed_id |
+----------+------------------+
| 5 | 555-55-5555 |
| 6 | 666-66-6666 |
| 7 | 777-77-7777 |
| 8 | 888-88-8888 |
| 9 | 999-99-9999 |
+----------+------------------+
5 rows in set (0.01 sec)
2
3
4
5
6
7
8
9
10
11
12
13
14
要处理字符串范围,你需要了解字符集(character set)内字符的顺序(字符集内字符的排序顺序称为排序规则collation)。
# 成员条件
在某些情况下,你不会将一个表达式限制为单个值或某个值的范围,而是限制为一组有限的值。例如,你可能想要查找产品代码为'CHK'、'SAV'、'CD'或'MM'的所有账户:
mysql> SELECT account_id, product_cd, cust_id, avail_balance
-> FROM account
-> WHERE product_cd = 'CHK'
OR product_cd = 'SAV'
OR product_cd = 'CD'
OR product_cd = 'MM';
+------------+------------+---------+---------------+
| account_id | product_cd | cust_id | avail_balance |
+------------+------------+---------+---------------+
| 1 | CHK | 1 | 1057.75 |
| 2 | SAV | 1 | 500.00 |
| 3 | CD | 1 | 3000.00 |
| 4 | CHK | 2 | 2258.02 |
| 5 | SAV | 2 | 200.00 |
| 7 | CHK | 3 | 1057.75 |
| 8 | MM | 3 | 2212.50 |
| 10 | CHK | 4 | 534.12 |
| 11 | SAV | 4 | 767.77 |
| 12 | MM | 4 | 5487.09 |
| 13 | CHK | 5 | 2237.97 |
| 14 | CHK | 6 | 122.37 |
| 15 | CD | 6 | 10000.00 |
| 17 | CD | 7 | 5000.00 |
| 18 | CHK | 8 | 3487.19 |
| 19 | SAV | 8 | 387.99 |
| 21 | CHK | 9 | 125.67 |
| 22 | MM | 9 | 9345.55 |
| 23 | CD | 9 | 1500.00 |
| 24 | CHK | 10 | 23575.12 |
| 28 | CHK | 12 | 38552.05 |
+------------+------------+---------+---------------+
21 rows in set (0.28 sec)
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
虽然这个WHERE
子句(四个条件用OR
连接)生成起来并不太繁琐,但想象一下,如果表达式集包含10个或20个成员会怎样。对于这些情况,你可以使用IN
操作符来替代:
SELECT account_id, product_cd, cust_id, avail_balance
FROM account
WHERE product_cd IN ('CHK','SAV','CD','MM');
2
3
使用IN
操作符时,无论集合中有多少个表达式,你都可以编写一个单一条件。
# 使用子查询
除了编写自己的表达式集,如('CHK','SAV','CD','MM')
,你还可以使用子查询动态地为你生成一个集合。例如,前面查询中使用的所有四种产品类型的product_type_cd
都是'ACCOUNT',那么为什么不使用针对product
表的子查询来检索这四种产品代码,而不是显式地列出它们呢:
mysql> SELECT account_id, product_cd, cust_id, avail_balance
-> FROM account
-> WHERE product_cd IN (SELECT product_cd FROM product
-> WHERE product_type_cd = 'ACCOUNT');
+--------------+-------------+----------+------------------+
| account_id | product_cd | cust_id | avail_balance |
+--------------+-------------+----------+------------------+
| 3 | CD | 1 | 3000.00 |
| 15 | CD | 6 | 10000.00 |
| 17 | CD | 7 | 5000.00 |
| 23 | CD | 9 | 1500.00 |
| 1 | CHK | 1 | 1057.75 |
| 4 | CHK | 2 | 2258.02 |
| 7 | CHK | 3 | 1057.75 |
| 10 | CHK | 4 | 534.12 |
| 13 | CHK | 5 | 2237.97 |
| 14 | CHK | 6 | 122.37 |
| 18 | CHK | 8 | 3487.19 |
| 21 | CHK | 9 | 125.67 |
| 24 | CHK | 10 | 23575.12 |
| 28 | CHK | 12 | 38552.05 |
| 8 | MM | 3 | 2212.50 |
| 12 | MM | 4 | 5487.09 |
| 22 | MM | 9 | 9345.55 |
| 2 | SAV | 1 | 500.00 |
| 5 | SAV | 2 | 200.00 |
| 11 | SAV | 4 | 767.77 |
| 19 | SAV | 8 | 387.99 |
+--------------+-------------+----------+------------------+
21 rows in set (0.11 sec)
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
子查询返回了一个包含四个值的集合,主查询会检查product_cd
列的值是否能在子查询返回的集合中找到。
# 使用NOT IN
有时,你想要查看某个特定表达式是否存在于一组表达式中,而有时你则想查看该表达式是否不存在于这组表达式中。对于这些情况,你可以使用NOT IN
操作符:
mysql> SELECT account_id, product_cd, cust_id, avail_balance
-> FROM account
-> WHERE product_cd NOT IN ('CHK','SAV','CD','MM');
2
3
+--------------+-------------+----------+------------------+
| account_id | product_cd | cust_id | avail_balance |
+--------------+-------------+----------+------------------+
| 25 | BUS | 10 | 0.00|
| 27 | BUS | 11 | 9345.55|
| 29 | SBL | 13 | 50000.00|
+--------------+-------------+----------+------------------+
3 rows in set (0.09 sec)
2
3
4
5
6
7
8
此查询找出了所有不是支票账户、储蓄账户、定期存款账户或货币市场账户的账号。
# 匹配条件
到目前为止,你已经了解了用于识别精确字符串、字符串范围或字符串集合的条件;最后一种条件类型涉及部分字符串匹配。例如,你可能想要查找所有姓氏以T开头的员工。你可以使用内置函数截取lname
列的首字母,如下所示:
mysql> SELECT emp_id, fname, lname
-> FROM employee
-> WHERE LEFT(lname, 1) = 'T';
+-----------+--------------+----------+
| emp_id | fname | lname |
+-----------+--------------+----------+
| 3 | Robert | Tyler |
| 7 | Chris | Tucker |
| 18 | Rick | Tulman |
+-----------+--------------+----------+
3 rows in set (0.01 sec)
2
3
4
5
6
7
8
9
10
11
虽然内置函数left()
能够完成这项工作,但它的灵活性欠佳。相反,你可以使用通配符构建搜索表达式,下一节将对此进行演示。
# 使用通配符
在搜索部分字符串匹配时,你可能会关注以下几种情况:
- 以特定字符开头/结尾的字符串
- 以特定子字符串开头/结尾的字符串
- 字符串中任意位置包含特定字符的字符串
- 字符串中任意位置包含特定子字符串的字符串
- 具有特定格式,而不考虑单个字符的字符串
你可以使用表4-4中所示的通配符构建搜索表达式,以识别上述及其他许多部分字符串匹配的情况。
通配符 | 匹配情况 |
---|---|
_ | 恰好一个字符 |
% | 任意数量的字符(包括0个) |
下划线字符代表单个字符,而百分号可以代表可变数量的字符。在构建使用搜索表达式的条件时,你需要使用LIKE
操作符,如下所示:
mysql> SELECT lname
-> FROM employee
-> WHERE lname LIKE '_a%e%';
+-----------+
| lname |
+-----------+
| Barker |
| Hawthorne |
| Parker |
| Jameson |
+-----------+
4 rows in set (0.00 sec)
2
3
4
5
6
7
8
9
10
11
12
上述示例中的搜索表达式指定了在字符串的第二个位置包含字母a,并且在字符串的其他任意位置(包括最后一个位置)包含字母e的字符串。表4-5展示了更多搜索表达式及其解释。
搜索表达式 | 解释 |
---|---|
F% | 以F开头的字符串 |
%t | 以t结尾的字符串 |
%bas% | 包含子字符串“bas”的字符串 |
__t | 第三个位置为t的四个字符的字符串 |
_______-____ | 在第四个和第七个位置有连字符的十一个字符的字符串 |
你可以使用表4-5中的最后一个示例查找联邦ID符合社会安全号码格式的客户,如下所示:
mysql> SELECT cust_id, fed_id
-> FROM customer
-> WHERE fed_id LIKE '___-__-____';
+----------+------------------+
| cust_id | fed_id |
+----------+------------------+
| 1 | 111-11-1111 |
| 2 | 222-22-2222 |
| 3 | 333-33-3333 |
| 4 | 444-44-4444 |
| 5 | 555-55-5555 |
| 6 | 666-66-6666 |
| 7 | 777-77-7777 |
| 8 | 888-88-8888 |
| 9 | 999-99-9999 |
+----------+------------------+
9 rows in set (0.02 sec)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
通配符在构建简单搜索表达式时效果很好;然而,如果你的需求更复杂一些,你可以使用多个搜索表达式,如下所示:
mysql> SELECT emp_id, fname, lname
-> FROM employee
-> WHERE lname LIKE 'F%' OR lname LIKE 'G%';
2
3
+-----------+--------------+-----------+
| emp_id | fname | lname |
+-----------+--------------+-----------+
| 5 | John | Gooding |
| 6 | Helen | Fleming |
| 9 | Jane | Grossman |
| 17 | Beth | Fowler |
+-----------+--------------+-----------+
4 rows in set (0.00 sec)
2
3
4
5
6
7
8
9
此查询找出了所有姓氏以F或G开头的员工。
# 使用正则表达式
如果你发现通配符的灵活性不足,可以使用正则表达式构建搜索表达式。本质上,正则表达式是一种功能更强大的搜索表达式。如果你刚接触SQL,但使用过Perl等编程语言进行编码,那么你可能已经对正则表达式非常熟悉了。如果你从未使用过正则表达式,那么你可能需要查阅Jeffrey E.F. Friedl所著的《精通正则表达式》(Mastering Regular Expressions,http://oreilly.com/catalog/9780596528126/ ,O’Reilly),因为这是一个太大的话题,本书无法涵盖。以下是前面那个查询(查找所有姓氏以F或G开头的员工)使用MySQL的正则表达式实现时的样子:
mysql> SELECT emp_id, fname, lname
-> FROM employee
-> WHERE lname REGEXP '^[FG]';
+-----------+--------------+-----------+
| emp_id | fname | lname |
+-----------+--------------+-----------+
| 5 | John | Gooding |
| 6 | Helen | Fleming |
| 9 | Jane | Grossman |
| 17 | Beth | Fowler |
+-----------+--------------+-----------+
4 rows in set (0.00 sec)
2
3
4
5
6
7
8
9
10
11
12
REGEXP
操作符将正则表达式(在这个例子中是^[FG]
)应用于条件左侧的表达式(lname
列)。现在,该查询包含一个使用正则表达式的单一条件,而不是两个使用通配符的条件。
Oracle数据库和Microsoft SQL Server也支持正则表达式。在Oracle数据库中,你需要使用regexp_like
函数,而不是前面示例中所示的REGEXP
操作符;而在SQL Server中,正则表达式可以与LIKE
操作符一起使用。
# Null:那个四个字母的单词
我尽可能地拖延,但现在是时候探讨一个常让人感到恐惧、不确定和畏惧的话题了:空值(Null value)。Null表示没有值;例如,在员工被解雇之前,员工表(employee table)中的end_date(结束日期)列应该为Null。在这种情况下,没有任何值可以合理地分配给end_date列。然而,Null有点难以捉摸,因为它有多种类型:
- 不适用(Not applicable):比如在自动取款机(ATM machine)上进行的交易,其员工ID(employee ID)列。
- 值未知(Value not yet known):比如在创建客户行(customer row)时,联邦ID(federal ID)未知。
- 值未定义(Value undefined):比如为一个尚未添加到数据库(database)的产品创建账户时。
一些理论家认为,应该有不同的表达式来涵盖这些(以及更多)情况,但大多数从业者都认为,存在多个Null值会让人困惑不已。
在处理Null时,你应该记住:
- 表达式可以为Null,但永远不会等于Null。
- 两个Null值永远不相等。
要测试一个表达式是否为Null,需要使用is null运算符,如下所示:
mysql> SELECT emp_id, fname, lname, superior_emp_id
-> FROM employee
-> WHERE superior_emp_id IS NULL;
+----------------+------------------+------------------+--------------------------+
| emp_id | fname | lname | superior_emp_id |
+----------------+------------------+------------------+--------------------------+
| 1 | Michael | Smith | NULL |
+----------------+------------------+------------------+--------------------------+
1 row in set (0.00 sec)
2
3
4
5
6
7
8
9
本查询返回所有没有上司的员工(这不是很棒吗?)。下面是使用= null而不是is null的相同查询:
mysql> SELECT emp_id, fname, lname, superior_emp_id
-> FROM employee
-> WHERE superior_emp_id = NULL;
Empty set (0.01 sec)
2
3
4
如你所见,该查询可以解析并执行,但不会返回任何行。这是缺乏经验的SQL程序员常犯的错误,而且数据库服务器(database server)不会提醒你这个错误,所以在构建测试Null的条件时要格外小心。
如果你想查看某列是否已被赋值,可以使用is not null运算符,如下所示:
mysql> SELECT emp_id, fname, lname, superior_emp_id
-> FROM employee
-> WHERE superior_emp_id IS NOT NULL;
+--------+----------+-----------+-----------------+
| emp_id | fname | lname | superior_emp_id |
+--------+----------+-----------+-----------------+
| 2 | Susan | Barker | 1 |
| 3 | Robert | Tyler | 1 |
| 4 | Susan | Hawthorne | 3 |
| 5 | John | Gooding | 4 |
| 6 | Helen | Fleming | 4 |
| 7 | Chris | Tucker | 6 |
| 8 | Sarah | Parker | 6 |
| 9 | Jane | Grossman | 6 |
| 10 | Paula | Roberts | 4 |
| 11 | Thomas | Ziegler | 10 |
| 12 | Samantha | Jameson | 10 |
| 13 | John | Blake | 4 |
| 14 | Cindy | Mason | 13 |
| 15 | Frank | Portman | 13 |
| 16 | Theresa | Markham | 4 |
| 17 | Beth | Fowler | 16 |
| 18 | Rick | Tulman | 16 |
+--------+----------+-----------+-----------------+
17 rows in set (0.00 sec)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
这个版本的查询返回了另外17名员工,与迈克尔·史密斯不同,他们有上司。
在暂时放下Null这个话题之前,再探讨一个潜在的陷阱会很有帮助。假设你被要求找出所有不是由海伦·弗莱明(其员工ID为6)管理的员工。你的第一反应可能是这样做:
mysql> SELECT emp_id, fname, lname, superior_emp_id
-> FROM employee
-> WHERE superior_emp_id != 6;
+--------+----------+-----------+-----------------+
| emp_id | fname | lname | superior_emp_id |
+--------+----------+-----------+-----------------+
| 2 | Susan | Barker | 1 |
| 3 | Robert | Tyler | 1 |
| 4 | Susan | Hawthorne | 3 |
| 5 | John | Gooding | 4 |
| 6 | Helen | Fleming | 4 |
| 10 | Paula | Roberts | 4 |
| 11 | Thomas | Ziegler | 10 |
| 12 | Samantha | Jameson | 10 |
| 13 | John | Blake | 4 |
| 14 | Cindy | Mason | 13 |
| 15 | Frank | Portman | 13 |
| 16 | Theresa | Markham | 4 |
| 17 | Beth | Fowler | 16 |
| 18 | Rick | Tulman | 16 |
+--------+----------+-----------+-----------------+
14 rows in set (0.00 sec)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
虽然这些14名员工确实不是为海伦·弗莱明工作,但如果你仔细查看数据,就会发现还有一名员工也不是为海伦工作,但这里没有列出。那名员工是迈克尔·史密斯,他的superior_emp_id列是Null(因为他是大老板)。因此,要正确回答这个问题,你需要考虑到某些行的superior_emp_id列可能包含Null的情况:
mysql> SELECT emp_id, fname, lname, superior_emp_id
-> FROM employee
-> WHERE superior_emp_id != 6 OR superior_emp_id IS NULL;
+--------+----------+-----------+-----------------+
| emp_id | fname | lname | superior_emp_id |
+--------+----------+-----------+-----------------+
| 1 | Michael | Smith | NULL |
| 2 | Susan | Barker | 1 |
| 3 | Robert | Tyler | 1 |
| 4 | Susan | Hawthorne | 3 |
| 5 | John | Gooding | 4 |
| 6 | Helen | Fleming | 4 |
| 10 | Paula | Roberts | 4 |
| 11 | Thomas | Ziegler | 10 |
| 12 | Samantha | Jameson | 10 |
| 13 | John | Blake | 4 |
| 14 | Cindy | Mason | 13 |
| 15 | Frank | Portman | 13 |
| 16 | Theresa | Markham | 4 |
| 17 | Beth | Fowler | 16 |
| 18 | Rick | Tulman | 16 |
+--------+----------+-----------+-----------------+
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
现在结果集包含了所有不为海伦工作的15名员工。在处理你不熟悉的数据库时,最好弄清楚表中的哪些列允许Null值,这样你就可以在过滤条件中采取适当措施,避免数据遗漏。
# 测试你的知识
以下练习测试你对过滤条件的理解。答案见附录C。
前两个练习使用以下交易数据:
Txn_id | Txn_date | Account_id | Txn_type_cd | Amount |
---|---|---|---|---|
1 | 2005-02-22 | 101 | CDT | 1000.00 |
2 | 2005-02-23 | 102 | CDT | 525.75 |
3 | 2005-02-24 | 101 | DBT | 100.00 |
4 | 2005-02-24 | 103 | CDT | 55 |
5 | 2005-02-25 | 101 | DBT | 50 |
6 | 2005-02-25 | 103 | DBT | 25 |
7 | 2005-02-25 | 102 | CDT | 125.37 |
8 | 2005-02-26 | 103 | DBT | 10 |
9 | 2005-02-27 | 101 | CDT | 75 |
# 练习4 - 1
以下过滤条件会返回哪些交易ID?
txn_date < '2005-02-26' AND (txn_type_cd = 'DBT' OR amount > 100)
# 练习4 - 2
以下过滤条件会返回哪些交易ID?
account_id IN (101,103) AND NOT (txn_type_cd = 'DBT' OR amount > 100)
# 练习4 - 3
构建一个查询,检索2002年开设的所有账户。
# 练习4 - 4
构建一个查询,查找所有姓氏中第二个位置包含字母a且在a之后任意位置包含字母e的非企业客户。