摘录一些《SQL 必知必会》中值得一记的内容。

Chap.1 了解 SQL

模式

  表具有一些特性,这些特性定义了数据在表中的存储,包括存储什么样的数据,数据如何分解,各部分信息如何命名等信息。这些信息称作模式

三层结构

  通过三层结构,保障无论概念模式(DBMS 从 MySQL 换成 PgSQL,或者从 MySQL 换成 MongoDB)或是内模式(换服务器/硬盘了,诶~)变了,外模式(表结构)都可以不用改。

  可参见浅谈数据库三大模式

三层结构

主键

  • 任意两行都不具有相同的主键值
  • 每一行都必须有一个主键值(主键列不允许空值NULL)
  • 主键列中的值不允许修改或更新
  • 主键值不能重用(如果某行从表中删除,它的主键不能赋给以后的新行)

外键

  说是挺重要的,详见第 12 章。是不是就是说这一列的取值只能是另一列出现过的值?譬如「学生 - 选课」列只能取「课程 - 课程号」列中的某个值。

SQL 语言的注释

1
2
3
4
5
/*
块注释
*/
SELECT a FROM b -- 行注释
SELECT a FROM b # 行注释

SQL 的组成

Standard Query Language

Chap 2. 检索数据

这一课介绍如何使用 SELECT 语句从表中检索一个或多个数据列。

检索单个列

1
SELECT column_name FROM TableName;

检索多个列

1
SELECT column1, column2 FROM TableName;

检索所有列

1
SELECT * FROM TableName;

检索不同的值

即去重检索。

1
2
SELECT DISTINCT column_name
FROM TableName;

注意:DISTINCT 对于后面所有的列都生效(后面你就知道,有些限定关键字只对紧接着的列生效)。换言之,下面这一行语句,会对三列都进行去重检索。

1
SELECT DISTINCT a, b, c FROM TableName;

有人说这一句相当于:

1
SELECT a, b, c FROM TableName group by a, b, c

有人说实际效果是只有三列都一样才认为是一样(予以折叠)。

限制结果

我只想要一定数量的行。

1
SELECT column_name FROM TableName LIMIT 5;

上面这条语句只对 MySQL、PostgreSQL 等有效,对于 Oracle 等无效。

SQL 语句和大小写

  • SQL 语句不区分大小写
  • 约定
    • SQL 关键字:大写
    • 列名:小写
    • 表名:首字母大写(?

Chap. 3 排序数据

这一节讲授如何使用 SELECT 语句的 ORDER BY 字句,根据需要对检索出的数据进行排序。

什么是子句

SELECT 语句、FROM 子句、ORDER BY 子句。

排序数据

默认是升序排列。降序排列在后面有写,不着急,慢慢看。

1
2
SELECT prod_name FROM Products
ORDER BY prod_name

但其实你也可以用没有显示出来的列进行排序:

1
2
SELECT prod_name FROM Products
ORDER BY price

注意:ORDER BY 字句必须位于 SELECT 语句的最后面。

按多个列排序

先按价格升序排,再按名字字母顺序升序排。

1
2
SELECT id, price, name FROM Products
ORDER BY price, name

按结果显示的列位置排序

1
2
SELECT id, price, name FROM Products
ORDER BY 2, 3;

和上面的排法一样:先按价格升序排,再按名字字母顺序升序排。

老子要降序排列

按照价格降序排列。

1
2
SELECT id, price, name FROM Products
ORDER BY price DESC;

按照价格降序,价格一样的按照名字升序

1
2
SELECT id, price, name FROM Products
ORDER BY price DESC, name;
1
2
SELECT id, price, name FROM Products
ORDER BY price DESC, name ASC;

按照价格降序,价格一样的按照名字降序

1
2
SELECT id, price, name FROM Products
ORDER BY price DESC, name DESC;

Chap 4. 筛选数据

使用 SELECT 语句的 WHERE 子句。

使用 WHERE 子句

1
2
SELECT name, price FROM Products
WHERE price = 233;

另外,正如之前所言,ORDER BY 子句要丢最后,意味着要用就得这么用:

1
2
3
SELECT name, price FROM Products
WHERE price = 233
ORDER BY name;

WHERE 子句操作符

操作符 说明
= 等于
<> / != 不等于
!< 不小于
!> 不大于
BETWEEN 在指定的两个值之间
IS NULL 为 NULL 值

范围值检查

1
2
SELECT name, price FROM Products
WHERE price BETWEEN 5 AND 10;

Chap 5. 高级数据过滤

这一课讲授如何组合 WHERE 子句以建立功能更强、更高级的搜索条件。我们还将学习如何使用 NOT 和 IN 操作符。

AND 和 OR

逻辑操作符:AND 和 OR。

1
2
3
SELECT id, price, name
FROM Products
WHERE factory_id = 'TSMC' AND price <= 10

顺序

优先处理 AND 操作符,再处理 OR。当然,你可以用括号。

IN

1
2
3
SELECT name, price FROM Products
WHERE factory_id IN ('TSMC', 'SAMSUNG')
ORDER BY name;

其实用 OR 也能实现

1
2
3
SELECT name, price FROM Products
WHERE factory_id = 'TSMC' OR factory_id = 'SAMSUNG'
ORDER BY name;

但相比用一连串的 OR,IN 的最大优点是可以包含其他 SELECT 语句。详见 Chap. 11

NOT

1
2
3
SELECT name, price FROM Products
WHERE NOT factory_id IN ('TSMC', 'SAMSUNG')
ORDER BY name;

注意,而不是 NOT IN!是 NOT {column} IN,别的也一样:NOT {column} = 1

Chap 6. 用通配符进行过滤

有点像正则表达式。

  • LIKE 操作符。
    • 其实它在技术上应该称作「谓词」
  • 百分号 %:任意字符出现任意次数
1
2
SELECT id, name FROM Products
WHERE name LIKE 'Fish%';

找出以 Fish 打头,后面内容随意的 name。

  • 下划线 _:任意字符出现一次
  • 方括号 [ABC]:匹配字符集 A/B/C 中的一个字符一次。
    • [^ABC]:除了 ABC 以外的
1
2
3
SELECT name FROM users
WHERE name LIKE '[JM]%'
ORDER BY name;

结果如下:

1
2
3
4
5
name
------------
Jim Jones
John Smith
Michelle Green

Chap 7. 创建计算字段

计算字段

其实就是一个虚拟列,使用其他列的数据经过拼接、变换等计算操作后得到的。计算字段是运行时(Runtime)在 SELECT 语句执行时创建的,从客户端看,计算字段的数据返回方式和其他列的数据返回方式相同。

本章概览

  • TRIM():去掉字符串左右两边的空格
    • LTRIM()RTRIM()
  • AS:给计算字段赋一个别名

拼接字段

感受世界的参差!(逃

Microsoft SQL Server 用 + 号:

1
2
3
SELECT name + '(' + country + ')'
FROM Athletes
ORDER BY name;

DB2、Oracle、PostgreSQL、SQLite 用 || 号:

1
2
3
SELECT name || '(' || country || ')'
FROM Athletes
ORDER BY name;

MySQL、MariaDB 用 Concat 函数:

1
2
3
SELECT Concat(name, '(', country, ')')
FROM Athletes
ORDER BY name;

别名

1
2
3
SELECT name || '(' || country || ')' AS display_name
FROM Athletes
ORDER BY name;

算术计算

1
2
3
SELECT quantity*price AS total_price
FROM Items
WHERE customer = 010;

Chap 8. 使用函数处理数据

难过的是,几乎每个系列的 DBMS 都有自己的一套函数体系,所以涉及这部分的 SQL 代码不具备通用性(即不可移植)。

匹配发音相似的单词

这个放上来就图一乐,正经人谁用这玩意啊

1
2
SELECT name FROM Customers
WHERE SOUNDEX(name) = SOUNDEX('Michael Green')

输出:

1
2
3
name
---------
Michelle Green

日期与时间

Oracle 下找出 2020 年全年的订单:

1
2
3
SELECT order_num
FROM Orders
WHERE order_date BETWEEN to_date('2020-01-01', 'yyyy-mm-dd') AND to_date('2020-12-31', 'yyyy-mm-dd');

数值处理函数

函数 说明
ABS() 绝对值
SIN() 正弦
EXP() 指数
PI() $\pi$
SQRT() 开方

Chap 9. 汇总数据

聚集函数

其实就是获取一些统计量,平均值什么的。

函数 说明
AVG() 返回某列的平均值
COUNT() 返回某列的行数
MAX()
MIN()
SUM() 返回某列值之和

AVG() 函数

全部商品的平均价格:

1
2
SELECT AVG(price) AS avg_price
FROM Products;

某一特定品牌的商品平均价格:

1
2
3
SELECT AVG(price) AS avg_price
FROM Products
WHERE brand = 'Xiaomi';

注:AVG() 函数忽略值为 NULL 的行。

COUNT() 函数

两种使用方式:

  • 使用 COUNT(*) 对表中行的数目进行计数,不管表列中包含的是空值 NULL 还是非空值。
  • 使用 COUNT(column) 对特定列中具有值的行进行计数,忽略 NULL 值的行。

有多少用户?

1
2
SELECT COUNT(*) AS num_users
FROM Users;

有多少用户绑了手机的?

1
2
SELECT COUNT(phone_number) AS num_users_with_phone
FROM Users;

SUM() 函数

酱瓜一共氪了多少钱?

1
2
3
SELECT SUM(money) AS total_kejin
FROM Payments
WHERE username = 'Jonb';

酱瓜一共买了多少钱?

1
2
3
SELECT SUM(quantity*price) AS total_price
FROM Payments
WHERE username = 'Jonb';

去重后取平均

假设每个人工资都不一样,但有些同学提交了多次,所以需要统计去重后 UX 团队的平均工资。

1
2
3
SELECT AVG(DISTINCT salary) AS avg_salary
FROM Salaries
WHERE team = 'UX'

DISTINCT 称为「聚集参数」。

DISTINCT 不能用于 COUNT(*),或者说,DISTINCT 不能用于计算或表达式。

Chap 10. 分组数据

涉及 GROUP BY 和 HAVING,它们都是 SELECT 语句的子句。

  • 如何使用 GROUP BY 子句对多组数据进行汇总计算,返回每个组的结果。
  • 如何使用 HAVING 子句过滤特定的组。
  • ORDER BY 和 GROUP BY 之间的差异。
  • WHERE 和 HAVING 之间的差异。

什么是分组

这个分组有点类似 Excel 里面选一列,然后点「筛选」。

之前提到:酱瓜一共氪了多少钱?

1
2
3
SELECT SUM(money) AS total_kejin
FROM Payments
WHERE username = 'Jonb';

用分组能实现:每个用户分别氪了多少钱?

1
2
3
SELECT username, SUM(money) AS total_kejin
FROM Payments
GROUP BY username;

输出:

1
2
3
4
5
6
username	total_kejin
-------- ------------
Jonb 83
Lisa 99
Alice 0
Bob 0

按照多列来分组

先按 SELECT 出来的第二列分组,再按第一列分组:

1
GROUP BY 2, 1

若 GROUP BY 後面指定兩個以上的欄位時,則要符合所有欄位值皆相同資料才會被分為一組。

过滤分组

  • 不能用 WHERE:WHERE 过滤的是行而不是分组。
  • 用 HAVING 子句:甚至于所有能写成 WHERE 的情况都能用 HAVING 代替。
  • WHERE 过滤行,HAVING 过滤分组。
  • 或者你可以这么理解
    • WHERE 先过滤后分组,所以 WHERE 完以后会少掉一些行
    • HAVING 先分组后过滤,所以 HAVING 完以后会少掉一些分组
    • WHERE 排除的行将不包括在分组中。这可能会改变计算值,从而影响 HAVING 子句中基于这些值过滤掉的分组。

列出所有至少有两个订单的顾客:

1
2
3
4
SELECT customer_id, COUNT(*) AS order_count
FROM Orders
GROUP BY customer_id
HAVING COUNT(*) >= 2;

列出具有两个以上高价产品的供应商,其中高价产品指产品价格 ≥ 5000:

1
2
3
4
5
SELECT vendor_id, COUNT(*) AS num_prods
FROM Products
WHERE prod_price >= 5000
GROUP BY vendor_id
HAVING COUNT(*) >= 2;

这段话的执行顺序是:先把价格大于 5000 的产品信息行抽出来放在一个新表里面,在这个新表里面再按照供应商 ID 分组,然后再选出产品数不少于2的供应商。

分组,然后输出时记得排序

就是加上 ORDER BY 子句。

一个订单可以订多个商品。检索订了≥3个商品的订单,并且按照商品数量升序排列。

1
2
3
4
5
SELECT order_id, COUNT(*) AS item_count
FROM OrderItems
GROUP BY order_id
HAVING COUNT(*) >= 3
ORDER BY item_count, order_id

输出:

1
2
3
4
5
6
order_id	item_count
-------- ----------
0006 3
0008 3
0002 5
0001 9

小结:SELECT 子句顺序

子句 说明 是否必须使用
SELECT 要返回的列或表达式 Yes
FROM 从中检索数据的表 仅在从表中选择数据时使用。*
WHERE 行级过滤 No
GROUP BY 分组说明 仅再按组计算聚集时使用。
HAVING 组级过滤 No
ORDER BY 输出时的顺序 No

*:如果你只是测试一下比如集聚函数或者TIME()之类的就可以不用。

Chap 11. 使用子查询

子查询就是嵌套在其他查询中的查询,就是指 SELECT 里面嵌套的那些个小 SELECT。

利用子查询进行过滤

与 IN 关键字相关:你这个元素在不在某个集合里面?

本书的数据库表均为关系表。订单存储在两个表中。

Order 表:

订单编号 顾客 ID 订单日期

OrderItems 表:

订单编号 订单内的物品

用户信息存在 Customers 表中:

顾客 ID 昵称 密码 地址

列出所有订购了 iPhone13 的顾客的地址。分解动作应该是:

  1. 检索包含物品 iPhone13 的所有订单的编号
  2. 检索具有前一步骤列出的订单编号的所有顾客的 ID
  3. 检索前一步骤返回的所有顾客 ID 的顾客信息(就是他们的地址)

对应 SQL 语句就是:

1
2
3
4
# 检索包含物品 iPhone13 的所有订单的编号
SELECT order_num
FROM OrderItems
WHERE prod_id = 'iPhone13'

输出:

1
2
3
4
order_num
---------
20007
20008
1
2
3
4
# 检索具有前一步骤列出的订单编号的所有顾客的 ID
SELECT cust_id
FROM Orders
WHERE order_num IN (20007, 20008)

输出:

1
2
3
4
cust_id
-------
10000004
10000005
1
2
3
4
# 检索前一步骤返回的所有顾客 ID 的顾客地址
SELECT cust_name, cust_address
FROM Customers
WHERE cust_id IN (10000004, 10000005)

当然,实际上我们会把这三句合一(三句简一句):

1
2
3
4
5
6
7
8
9
10
11
# 列出所有订购了 iPhone13 的顾客和地址
SELECT cust_name, cust_address
FROM Customers
WHERE cust_id IN (
SELECT cust_id
FROM Orders
WHERE order_num IN (SELECT order_num
FROM OrderItems
WHERE prod_id = 'iPhone13'
)
)

注意:

  1. 作为子查询的 SELECT 语句只能查询单个列,若试图检索多列将报错。这点其实很好理解,cust_id IN (…) 的 … 应该就是一个 cust_id 的列表;如果有两个 IN,你应该用 (a IN A) AND (b IN B) 而非 a, b IN (A, B)a, b IN (A, B) 的写法是令人费解的。
  2. 使用子查询并不总是执行这类数据检索的最有效方法,具体请参阅第12章:JOIN语句。

子查询作为计算列

即:子查询一个计算字段,作为结果的一列。

1
2
3
4
5
6
7
SELECT cust_name,		-- 客户姓名
cust_state, -- 客户地区 (美国的州)
(SELECT COUNT(*)
FROM Orders
WHERE Orders.cust_id = Customers.cust_id) AS orders -- 客户累计订单数
FROM Customers
ORDER BY cust_name;

上述语句引入了一个新概念,叫「完全限定列名」,它同时指定了表名和列名。

1
WHERE Orders.cust_id = Customers.cust_id

Chap 12. 联结表

本章将全文摘录。

联结

  SQL 最强大的功能之一就是能在数据查询的执行中联结(join)表。联结是利用 SQL 的 SELECT 能执行的最重要的操作,很好地理解联结及其语法是学习 SQL 的极为重要的部分。

  在能够有效地使用联结前,必须了解关系表以及关系数据库设计的一些基础知识。下面的介绍并不能涵盖这一主题的所有内容,但作为入门已经够了。

关系表

  理解关系表,最好是来看个例子。

  有一个包含产品目录的数据库表,其中每类物品占一行。对于每一种物品,要存储的信息包括产品描述、价格,以及生产该产品的供应商。

  现在有同一供应商生产的多种物品,那么在何处存储供应商名、地址、联系方法等供应商信息呢?将这些数据与产品信息分开存储的理由是:

  • 同一个供应商生产的每个产品,其供应商信息都是相同的,对每个产品。重复此信息既浪费时间又浪费存储空间
  • 供应商信息发生变化,例如供应商迁址或电话号码变动,只需修改一次即可
  • 如果有重复数据(即每种产品都存储供应商信息),则很难保证每次输入该数据的方式都相同。不一致的数据在报表中就很难利用。

  关键是,相同的数据出现多次决不是一件好事,这是关系数据库设计的基础。关系表的设计就是要把信息分解成多个表,一类数据一个表。各表通过某些共同的值互相关联(所以才叫关系数据库)。

  在这个例子中可建立两个表:一个存储供应商信息,另一个存储产品信息。Vendors 表包含所有供应商信息,每个供应商占一行,具有唯一的标识。此标识称为主键(primary key),可以是供应商ID或任何其他唯一值。

  Products 表只存储产品信息,除了存储供应商 ID(Vendors 表的主键)外,它不存储其他有关供应商的信息。Vendors 表的主键将 Vendors 表与 Products 表关联,利用供应商 ID 能从 Vendors 表中找出相应供应商的详细信息。

这样做的好处是:

  • 供应商信息不重复,不会浪费时间和空间;
  • 如果供应商信息变动, 可以只更新Vendors表中的单个记录, 相关表中的数据不用改动;
  • 由于数据不重复,数据显然是一致的,使得处理数据和生成报表更简单。

总之,关系数据可以有效地存储,方便地处理。因此,关系数据库的可伸缩性远比非关系数据库要好。

可伸缩性

能够适应不断增加的工作量而不失败。设计良好的数据库或应用程序称为可伸缩性好(scale well)。

为什么使用联结

  如前所述,将数据分解为多个表能更有效地存储,更方便地处理,并日可伸缩性更好。但这些好处是有代价的。

  如果数据存储在多个表中, 怎样用一条 SELECT 语句就检索出数据呢?

  答案是使用联结。简单说,联结是一种机制,用来在一条 SELECT 语句中关联表,因此称为联结。使用特殊的语法,可以联结多个表返回一组输出,联结在运行时关联表中正确的行。

创建联结

创建联结非常简单,知道要联结的所有表以及关联它们的方式即可。下面的例子创建了一个自然联结:

1
2
3
SELECT vend_name, prod_name, prod_price
FROM Vendors, Products
WHERE Vendors.vend_id = Products.vend_id;

其实就是在 Vendors × Products 这个笛卡儿积里面取出 Vendors.vend_id = Producs.vend_id 的那些行,并且结果表只有一个 vend_id 列。

如果有两个 vend_id 列,那就不是自然联结,而是等值联结。

笛卡儿积:由没有联结条件的表关系返回的结果为笛卡儿积(即没有 WHERE xx = yy 的联结查询)。检索出的行的数目将是第一个表中的行数乘以第二个表中的行数。

有时,返回笛卡儿积的联结,被称为叉联结(cross join)

内联结

内联结,又称等值联结,这种联结基于两个表之间的相等测试。

上面一节介绍了一种简单的等值语法:

1
2
3
SELECT vend_name, prod_name, prod_price
FROM Vendors, Products
WHERE Vendors.vend_id = Products.vend_id;

但其实ANSI SQL规范推荐的内联结方式是下面这样的:

1
2
3
SELECT vend_name, prod_name, prod_price
FROM Vendors INNER JOIN Products
ON Vendors.vend_id = Products.vend_id;

联结多个表

1
2
3
4
5
SELECT prod_name, vend_name, prod_price, quantity
FROM OrderItems, Products, Vendors
WHERE Products.vend_id = Vendors.vend_id
AND OrderItems.prod_id = Products.prod_id
AND order_num = 20007;

上面的例子是要显示订单 20007 中的物品。

下面,我们回顾一下Chap 11中的一个例子,现在需要查询订购了 iPhone 13 的顾客列表。

1
2
3
4
5
6
7
8
9
10
SELECT cust_name, cust_address
FROM Customers
WHERE cust_id IN (
SELECT cust_id
FROM Orders
WHERE order_num IN (SELECT order_num
FROM OrderItems
WHERE prod_id = 'iPhone13'
)
)

前面也说过,子查询并不总是执行复杂 SELECT 操作的最有效方法,换用联结重写之:

1
2
3
4
5
SELECT cust_name, cust_address
FROM Customers, Orders, OrderItems
WHERE Customers.cust_id = Orders.cust_id
AND OrderItems.order_num = Orders.order_num
AND prod_id = 'iPhone13';

Chap S1. 别名

SQL 中可以给列(包括计算列)和表起别名。其中,Oracle 数据库没有 AS 关键字,直接拍个空格即可(即 Customers AS C 写作 Customers C)。

列别名

Chap. 7

1
2
3
4
SELECT RTRIM(vend_name) + '(' + RTRIM(vend_country) + ')'
AS vend_title
FROM Vendors
ORDER BY vend_name;

表别名

1
2
3
4
5
SELECT cust_name, cust_address
FROM Customers AS C, Orders AS O, OrderItems AS OI
WHERE C.cust_id = O.cust_id
AND OI.order_num = O.order_num
AND prod_id = 'iPhone13';

需要注意,表别名只在查询执行中使用,不会返回到客户端——这里和列别名不一样,列别名是会传回客户端的。

使用表别名的情况:

  • 缩短 SQL 语句
  • ==允许在一条 SELECT 语句中多次使用相同的表==

Chap 13. 创建高级联结

自联结

自联结 Self-join,即同一个表“自交”。

现在有一张 URC 校友联系表,需要给和酱瓜同一个公司的 URC 校友发一封邮件。

你可以用子查询:先找到酱瓜工作的公司,然后找出在该公司工作的 URC 校友。

1
2
3
4
5
SELECT alumnus_id, name, now_company
FROM URCAlumnus
WHERE now_company = (SELECT now_company
FROM URCAlumnus
WHERE name = 'Jonb');

以上方式挺符合直觉的。不过 SQL 处理联结的速度比处理子查询要快一些,我们可以换用联结看看。

1
2
3
4
SELECT A.alumnus_id, A.name, A.now_company
FROM Alumnus AS A, Alumnus AS B
WHERE A.now_company = B.now_company
AND B.name = 'Jonb';

其实就是:

ID 姓名 当前单位 ID 姓名 当前单位
1 Jonb Fantuan Corp. 1 Jonb Fantuan Corp.
1 Jonb Fantuan Corp. 2 Lisa Fantuan Corp.
1 Jonb Fantuan Corp. 3 JiangGua Fantuan Corp.
2 Lisa Fantuan Corp. 1 Jonb Fantuan Corp.
2 Lisa Fantuan Corp. 2 Lisa Fantuan Corp.
2 Lisa Fantuan Corp. 3 JiangGua Fantuan Corp.
3 JiangGua Fantuan Corp. 1 Jonb Fantuan Corp.
3 JiangGua Fantuan Corp. 2 Lisa Fantuan Corp.
3 JiangGua Fantuan Corp. 3 JiangGua Fantuan Corp.

自然联结

  自然联结不允许一个同样意思的列出现多次。咋搞呢?系统不会帮忙的,你得自己搞。自己去删掉多余的列、或者手动只选出独特的列(一般是对第一个表用通配符 SELECT * 选所有列,而对其它表显式地指定 SELECT 哪些列)。

1
2
3
4
5
SELECT C.*, O.order_num, O.order_date, OI.prod_id, OI.quantity, OI.item_price
FROM Customers AS C, Orders AS O, OrderItems AS OI
WHERE C.cust_id = O.cust_id
AND OI.order_num = O.order_num
AND prod_id = 'iPhone13';

  但其实,我们可能一辈子都用不到不是自然联结的等值联结——因为不是自然联结的等值联结是令人费解的。

外联结

  现在有两张表,一张是老师和开课的对应表,一张是学生和选课的对应表,现在想看看每门课有谁选,包括这门课没人选你也得给我显示出来。内联结做不了这件事(学生选课表里面压根就没出现你这老师,咋 SELECT ?!),得出动外联结。

  左外联结 LEFT OUTER JOIN,即 Students 主动接近 Teachers:

1
2
3
SELECT Teachers.class_id, Teachers.name, Students.name
FROM Teachers LEFT OUTER JOIN Students
ON Teachers.class_id = Students.class_id

得到:

班级号 老师 学生
MAS.01 酱瓜 Lisa
MAS.01 酱瓜 Jonb
MAS.02 ~rui NULL

  看到那个 NULL 了吗,意思就是 ~rui 老师的课就 nmd 没人选。被贴贴的“主表”的所有行至少会出现一次,“舔狗”表如果没有对应能联结上的值,那这一行就填个 NULL。

  当然你也可以右外联结 RIGHT OUTER JOIN

1
2
3
SELECT Teachers.class_id, Teachers.name, Students.name
FROM Students RIGHT OUTER JOIN Teachers
ON Teachers.class_id = Students.class_id

  或者全外联结 FULL OUTER JOIN,那这样两边都可能出现 NULL 了。

1
2
3
SELECT Teachers.class_id, Teachers.name, Students.name
FROM Teachers FULL OUTER JOIN Students
ON Teachers.class_id = Students.class_id

包含聚集函数的联结

检索所有顾客和每个顾客所下的订单数:

1
2
3
4
SELECT Customers.id,
COUNT(Orders.id) AS order_count
FROM Customers LEFT OUTER JOIN Orders ON Customers.id = Orders.cust_id
GROUP BY Customers.id;

  这条 SELECT 语句使用 INNER JOIN 将 Customers 和 Orders 表相关联。GROUP BY 子句按照顾客分组数据,所以COUNT()函数可以对某一顾客的订单进行计算,并作为 order_count 返回。

顾客编号 订单总数
Jonb 4
Lisa 0
JiangGua 3

Chap 14. 组合查询

本章讲述如何利用 UNION 操作符将多条 SELECT 语句组合成一个结果。

组合查询

组合查询即执行多个查询(多条 SELECT 语句),并将结果作为一个查询结果集返回。你可以理解为将多个查询的结果取了一个并集,有时也称为复合查询

主要有两种情况需要用到组合查询:

  • 在一个查询中从不同的表返回结构数据;
  • 对一个表执行多个查询,按一个查询返回数据。

组合查询和多个 WHERE 条件

  多数情况下,组合相同表的两个查询所完成的工作与具有多个 WHERE 子句条件的一个查询所完成的工作相同。换言之,任何具有多个 WHERE 子句的 SELECT 语句都可以作为一个组合查询,在下面可以看到这一点。

创建组合查询

查询 P10、P11、P12 位阶的高层管理人员和所有 Jonbgua Labs 的成员(不论位阶是多少)。

使用 WHERE 完成:

1
2
3
4
SELECT name
FROM Employees
WHERE internel_level IN ('P10', 'P11', 'P12')
OR department = 'Jonbgua Labs';

使用 UNION 完成:

1
2
3
4
5
6
7
SELECT name
FROM Employees
WHERE internel_level IN ('P10', 'P11', 'P12')
UNION
SELECT name
FROM Employees
WHERE department = 'Jonbgua Labs';

  在这个简单的例子中,使用 UNION 可能比使用 WHERE 更复杂。但对于较复杂的过滤条件、或者从多个表中检索数据时,UNION 能使代码更直观。

UNION 规则

  • UNION 中的每个查询必须包含相同的列、表达式或聚集函数。不过各个列不需要以相同的次序列出。
  • 列数据类型必须兼容:类型不必完全相同,但必须是 DBMS 可以隐含转换的类型,例如不同但都是数值类型、或不同但都是日期类型。

关于 UNION 的列名

  如果结合 UNION 使用的 SELECT 语句遇到不同的列名,那么会返回什么名字呢?比如如果一条语句是 SELECT prod_name,另一条是 SELECT productName,那么查询结果返回的是什么名字呢?

  答案是它会返回第一个名字,本例中就是 prod_name。你可以对第一个名字定义别名,那这样就可以自定义返回的列名了。

  这个行为会导致你如果需要排序也得用第一次出现的列名。上例中,ORDER BY prod_name 是合法的,但 ORDER BY productName 是非法的。

包含或取消重复的行

  UNION 由于是取并集,所以默认会去重。你如果不希望它去重,你就用 UNION ALL 吧。

对组合查询结果排序

  SELECT 语句的输出用 ORDER BY 子句排序。在用 UNION 组合查询时,只能使用一条 ORDER BY 子句,它必须位于最后一条 SELECT 语句之后。对于结果集,不存在用一种方式排序一部分、而又用另一种方式排序另一部分的情况——这种情况令人费解。因而不允许使用多条 ORDER BY 子句。

还是上面那个例子:

1
2
3
4
5
6
7
8
SELECT name
FROM Employees
WHERE internel_level IN ('P10', 'P11', 'P12')
UNION
SELECT name
FROM Employees
WHERE department = 'Jonbgua Labs'
ORDER BY name;

其它集合运算

有些 DBMS 支持除 UNION(取并集) 之外的集合运算,分别是 MINUS 和 INTERSECT。

  • MINUS / EXCEPT:集合减法
  • INTERSECT:取交集

即便不支持,你也可以用 SELECT .. WHERE .. 的方式自行实现。

Chap 15. 插入数据

本章介绍如何使用 INSERT 语句将数据插入表中。

数据插入

数据插入有以下几种情况:

  • 插入完整的一行
  • 插入行的一部分(某些列)
  • 插入某些查询的结果

插入完整的一行

你大可以省略列名,直接按顺序插入一行。需要注意的是,各列必须按照它们在表的定义中出现的次序填充。

1
2
3
4
5
6
7
8
9
INSERT INTO Employees
VALUES(
1,
'Jonb',
'CEO',
'管理楼 666'
NULL,
'13888888888'
)

虽然这种语法很简单,但并不安全,应该尽量避免使用。上面的SQL语句高度依赖于表中列的定义次序,万一服务端的表格式改了但你客户端代码没改,你这个顺序就寄了。

所以更推荐的方式是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
INSERT INTO Employees(
id,
name,
title,
office,
phone,
department
)
VALUES(
1,
'Jonb',
'CEO',
'管理楼 666',
'13888888888'
NULL
)

插入行的一部分

1
2
3
4
5
6
7
8
9
10
INSERT INTO Employees(
id,
name,
title,
)
VALUES(
1,
'Jonb',
'CEO',
)

这样的话,办公室、部门、手机三列将被自动设为缺省值(NULL 或者别的)。

注意:省略列

如果表的定义允许,则可以在 INSERT 操作中省略某些列。省略的列必须满足以下两个条件之一:

  • 该列定义为允许 NULL 值
  • 在表定义中给出了默认值(缺省值)

但如果表中的对应列不允许有缺值,但你又省略了它的值,那就会报错。

插入检索出的数据

你可以利用 INSERT 将 SELECT 语句的结果插入表中,有人把这个操作称为 INSERT SELECT。

一般的 INSERT 一次只插入一行,但 INSERT SELECT 可以把 SELECT 出来的所有行一下子全插进去。

1
2
3
4
5
6
7
INSERT INTO Customers(
id,
email
)
SELECT cust_id,
cust_email
FROM OldCustomersTable;

INSERT SELECT 中的列名

DBMS 一点也不关心 SELECT 返回的列名。它只使用了列的位置。所以 SELECT 中的第一列将被用来填充表列中指定的第一列,以此类推。

复制表

创建一个新表 EmployeeCopy,并将 Employees 表的所有内容导进去:

1
CREATE TABLE EmployeeCopy AS SELECT * FROM Employees;

你当然也可以只导入部分的列:

1
2
CREATE TABLE EmployeeCopy AS
SELECT name FROM Employees;

SQL Server

对于 SQL Server,是用 SELECT INTO 来复制表的:

1
SELECT * INTO EmployeeCopy FROM Employees;

注意,在使用 SELECT INTO 时,需要知道一些事情:

  • 任何 SELECT 选项和子句都可以使用,包括 WHERE 和 GROUP BY;
  • 可利用联结从多个表插入数据;
  • 不管从多少个表中检索数据,数据都只能插入到一个表中。

小结

这一章介绍如何将行插入到数据库表中。我们学习了使用 INSERT 的几种方法,为什么要明确使用列名,如何用 INSERT SELECT 从其它表中导入行,如何用 SELECT INTO 将行导出到一个新表。下一章将讲述如何使用 UPDATE 和 DELETE 更新和删除行。

Chap 16. 更新和删除数据

本章介绍如何利用 UPDATE 和 DELETE 语句进一步操作表数据。

更新数据

本节将分别介绍两种使用 UPDATE 语句的方式:

  • 更新表中的特定行
  • 更新表中的所有行

基本的 UPDATE 语句由三部分组成,分别是:

  1. 要更新的表
  2. 列名和它们的新值
  3. 确定要更新哪些行的筛选条件

一条基本的 UPDATE 语句如下,它一下子就改了一行的两列。

1
2
3
4
UPDATE Employees
SET email = '[email protected]',
title = 'Consultant'
WHERE name = 'JiangGua';

如果你想改所有行的某一列,把 WHERE 去掉即可。

在 UPDATE 语句中使用子查询

UPDATE 语句中可以使用子查询,即:用 SELECT 语句检索出的数据来更新列数据。请参见 Chap 11.

删除数据

1
2
DELETE FROM Employees
WHERE name = '9bishi';

外键与 DELETE

Chap 12 介绍了联结,简单联结两个表只需要这两个表中的公用字段。也可以让 DBMS 通过使用外键来严格实施关系(这些定义在附录 A 中)。存在外键时,DBMS 使用它们实施引用完整性。例如要向 Products 表中插入一个新产品,DBMS 不允许通过位置的供应商 ID 插入它,因为 vend_id 列时作为外键连接到 Vendors 表的。那么,这与DELETE 有什么关系呢?使用外键确保引用完整性的一个好处是,DBMS 通常可以防止某个关系需要用到的行。例如,要从 Products 表中删除一个产品,而这个产品用在了 OrderItems 的已有订单中,那么 DELETE 语句将抛出错误并终止。这是总要定义外键的另一个理由。

快速清空表

如果想从表中删除所有行,不要使用 DELETE。可以使用 TRUNCATE TABLE 语句,它速度更快:因为不记录数据的变动。

更新和删除的指导原则

在实践中,SQL Coder 应遵守以下规则:

  • 除非确实打算更新和删除所有行,否则绝对不要使用不带 WHERE子句的UPDATE或DELETE。
  • 保障每个表都有主键,尽可能在WHERE子句中用主键来锁定某行。
  • 在 UPDATE 或DELETE前,用SELECT语句先看看这一行是不是就是目标行
  • 使用强制实施引用完整性的数据库:它们不允许删除那些与其它表有关联的行。

更多内容请参阅《数据库入门:从删库到跑路》(bs

Chap 17. 创建、更改、删除表

创建表

1
2
3
4
5
6
7
8
CREATE TABLE Products
(
prod_id CHAR(10) NOT NULL,
prod_price DECIMAL(8, 2) NOT NULL,
prod_desc VARCHAR(1000) NULL,
stored_num INTEGER NOT NULL DEFAULT 1,
update_date DATETIME NOT NULL DEFAULT CURRENT_DATE(),
);

更新表

无非就是增加列/删除列。DBMS 们一般都支持增加列,但未必支持删除列。

1
2
ALTER TABLE Vendors
ADD vend_phone CHAR(20);
1
2
ALTER TABLE Vendors
DROP COLUMN vend_phone;

如果 DBMS 不支持删除列,那就只能这么搞了:

  1. 用新的列布局建一个新表。
  2. INSERT SELECT 语句从旧表复制数据到新表。有必要的话,可以使用转换函数和计算字段。
  3. 检验包含所需数据的新表
  4. 重命名旧表
  5. 用旧表的名字重命名新表
  6. 根据需要,重新创建触发器、存储过程、索引和外键

删除表

1
DROP TABLE Cust_copy;

重命名表

每个 DBMS 大不相同,请查对应的文档吧。

Chap 18. 使用视图

视图

视图是虚拟的表。与包含数据的表不一样,视图只包含使用时动态检索数据的查询。

视图可以用来简化 SQL 语句,比如 Chap 12. 中曾出现这么一个查询:

1
2
3
4
5
SELECT cust_name, cust_address
FROM Customers, Orders, OrderItems
WHERE Customers.cust_id = Orders.cust_id
AND OrderItems.order_num = Orders.order_num
AND prod_id = 'iPhone13';

但如果我们能把 Customers, Orders, OrderItems 三个表的自然联结做成一个虚拟表 Product_Customer,那岂不是直接:

1
2
3
SELECT cust_name, cust_address
FROM Product_Customer
WHERE prod_id = 'iPhone12';

就能查到另一个产品的信息了?

为什么要使用视图

  • 重用 SQL 语句
  • 简化复杂的 SQL 操作。在编写查询后,可以方便地重用它而不必知道其基本查询细节。
  • 使用表地一部分而不是整个表
  • 保护数据:可以授予用户访问表地特定部分的权限,而不是整个表的访问权限。
  • 更改数据格式和表示。视图可返回与底层表的表示和格式不同的数据。

性能问题

因为视图不包含数据,所以每次使用视图时,都必须处理查询执行时需要的所有检索。如果你用多个联结和过滤创建了复杂的视图或嵌套了视图(用视图1的数据创建视图2),那性能可能会下降地很厉害。

创建视图

1

Chap 19. 存储过程

参考

扩展

Ch2 关系数据库