slacr_

Just to record my life and thoughts.
笔记/编程/杂乱/极简

[SQL]必知

May 9, 2023DB14429 words in 96 min

数据库实验要学SQL, 数据库用的是MySQL Community Sever 8.0.32, DBMS用的Navicat Premium 15.0.1, Navicat只能免费试用七天. 穷学生用的破解版.

认识SQL

SQL(Structured Query Language) 一种专门用来与数据库沟通的语言. 标准 SQL 由 ANSI 标准委员会管理Matirx,从而称为 ANSI SQL。所有主要的 DBMS,即使有自己的扩展,也都支持 ANSI SQL。各个实现有自己的名称,如 Oracle 的 PL/SQL、微软 SQL Server 用的 Transact-SQL 等.

一些概念

数据库(databases) 保存有组织的数据的容器.
数据库管理系统(DBMS) 创建和操纵的数据库的软件.
表(table) 某种特定类型数据的结构化清单。
模式(schema) 关于数据库和表的布局及特性的信息。
列(column) 表中的一个字段。所有表都是由一个或多个列组成的。
数据类型(datatype) 允许什么类型的数据。每个表列都有相应的数据类型,它限制(或允许)该列中存储的数据。
行(row) 表中的一个记录(record)。
主键(primary key) 一列(或几列),其值能够唯一标识表中每一行。

子句(clause) SQL语句由子句构成,有些子句是必需的,有些则是可选的。一个子句通常由一个关键字加上所提供的数据组成。
通配符(wildcard) 用来匹配值的一部分的特殊字符。
搜索模式(search pattern) 由字面值、通配符或两者组合构成的搜索条件。
谓词(predicate) 用于提供一种搜索模式或者说判断共同特征类别的条件.
字段(field) 基本上与列(column)的意思相同,经常互换使用,不过数据库列一般称为列,而字段这个术语通常在计算字段这种场合下使用。
拼接(concatenate) 将值联结到一起(将一个值附加到另一个值)构成单个值
可移植(portable) 所编写的代码可以在多个系统上运行。
聚集函数(aggregate function) 对某些行运行的函数,计算并返回一个值。
查询(query) 任何 SQL 语句都是查询。但此术语一般指 SELECT 语句。
子查询(subquery) 即嵌套在其他查询中的查询。
可伸缩(scale) 能够适应不断增加的工作量而不失败。设计良好的数据库或应用程序
称为可伸缩性好(scale well)。
笛卡儿积(cartesian product) 由没有联结条件的表关系返回的结果为笛卡儿积。有时,返回笛卡儿积的联结,也称叉联结(cross join)
事务(transaction)指一组 SQL 语句;
回退(rollback)指撤销指定 SQL 语句的过程;
提交(commit)指将未存储的 SQL 语句结果写入数据库表;
保留点(savepoint)指事务处理中设置的临时占位符(placeholder),可以对它发布回退(与回退整个事务处理不同)。
结果集(result set) SQL 查询所检索出的结果。

SQL 数据类型

检索数据

注释

1
2
3
4
5
6
7
#  单行注释, 这种形式有些DBMS不支持
-- 单行注释
/*

多行注释

*/

SELECT 选择列

1
2
3
4
5
6
7
8
-- 选择单个列
SELECT prod_name FROM products;

-- 选择多个列
SELECT prod_id, prod_name, prod_price FROM products

-- 选择所有列
SELECT * FROM products;

DISTINCT 限制不同数据

1
2
3
4
5
6
7
8
9
-- 返回列的所有数据, 有重复
SELECT vend_id FROM products;

-- 使用DISTINC限制只返回不同的数据, 无重复
SELECT DISTINCT vend_id FROM products;

-- DISTINC 作用于所用选择的列, 返回只要存在不相同的数据
SELECT vend_id, prod_price FROM products;
SELECT DISTINCT vend_id, prod_price FROM products;

LIMIT 限制结果

返回指定行数的结果, 不同数据库的实现不一样.

1
2
3
4
5
6
7
8
-- MySQL 返回前三行
SELECT prod_name FROM products LIMIT 3;

-- OFFSET 限制从第二行往后开始选择三行
SELECT prod_name FROM products LIMIT 3 OFFSET 2;

-- 简化写法, 和上等同
SELECT prod_name FROM products LIMIT 2,3;

挑战题

  1. 编写 SQL 语句,从 Customers 表中检索所有的 ID(cust_id)。
  2. OrderItems 表包含了所有已订购的产品(有些已被订购多次)。编写SQL 语句,检索并列出已订购产品(prod_id)的清单(不用列每个订单,只列出不同产品的清单)。提示:最终应该显示 7 行。
  3. 编写 SQL语句,检索 Customers 表中所有的列,再编写另外的 SELECT语句,仅检索顾客的 ID。使用注释,注释掉一条 SELECT 语句,以便运行另一条 SELECT 语句。(当然,要测试这两个语句。)
1
2
3
4
5
6
7
8
9
-- 1. 
SELECT cust_id FROM customers;

-- 2.
SELECT DISTINCT prod_id FROM orderitems;

-- 3.
-- SELECT * FROM customers;
SELECT cust_id FROM customers;

排序检索数据

ORDER BY 子句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- ORDER BY 子句 按字典顺序从小到大
SELECT prod_name FROM products ORDER BY prod_name;

-- 按多个列排序, 先按价格, 再按名称
SELECT prod_id, prod_price, prod_name FROM products
ORDER BY prod_price, prod_name;

-- 使用列的相对位置, 2, 3 表示先按第二列, 再按第三列, 和上等同
SELECT prod_id, prod_price, prod_name FROM products
ORDER BY 2, 3;

-- 使用 DESC (DESCENDING) 关键字 降序, 默认是 ASC (ASCENDING)升序, 可以不写
SELECT prod_id, prod_price, prod_name FROM products
ORDER BY prod_price DESC;

-- 多个列排序, 价格降序, 名字升序
SELECT prod_id, prod_price, prod_name FROM products
ORDER BY prod_price DESC, prod_name ASC;

在指定一条 ORDER BY 子句时,应该保证它是 SELECT 语句中最后一条子句。如果它不是最后的子句,将会出错。
通常,ORDER BY 子句中使用的列将是为显示而选择的列。但是, 实际上并不一定要这样,用非检索的列排序数据是完全法的.
DESC只作用于选择的一列, 多选得每一项都加上DESC关键字.

挑战题

  1. 编写 SQL 语句,从 Customers 中检索所有的顾客名称(cust_names),并按从 Z 到 A 的顺序显示结果。
  2. 编写 SQL 语句,从 Orders 表中检索顾客 ID(cust_id)和订单号(order_num),并先按顾客 ID 对结果进行排序,再按订单日期倒序排列。
  3. 显然,我们的虚拟商店更喜欢出售比较贵的物品,而且这类物品有很多。编写 SQL 语句,显示 OrderItems 表中的数量和价格(item_price),并按数量由多到少、价格由高到低排序。
  4. 下面的 SQL 语句有问题吗?(尝试在不运行的情况下指出。)
    SELECT vend_name,
    FROM Vendors
    ORDER vend_name DESC;
1
2
3
4
5
6
7
8
9
10
11
-- 挑战题
-- 1.
SELECT cust_name FROM customers ORDER BY cust_name DESC;

-- 2.
SELECT cust_id, order_num, order_date FROM orders ORDER BY cust_id, order_date DESC;

-- 3.
SELECT order_item, item_price FROM orderitems ORDER BY order_item DESC, item_price DESC;

-- 4. 没写BY

过滤数据

数据库表一般包含大量的数据,很少需要检索表中的所有行。通常只会根据特定操作或报告的需要提取表数据的子集。只检索所需数据需要指定搜索条件(search criteria),搜索条件也称为过滤条件(filter condition)。
在 SELECT 语句中,数据根据 WHERE 子句中指定的搜索条件进行过滤。

WHERE 子句

并非所有 DBMS 都支持这些操作符.

1
2
3
4
5
6
7
8
9
-- WHERE子句
SELECT prod_name, prod_price From products
WHERE prod_price = 3.49;

SELECT prod_name , prod_price FROM products
WHERE prod_price < 10;

SELECT prod_name, prod_price FROM products
WHERE prod_price != 3.49;

在同时使用 ORDER BY 和 WHERE 子句时,应该让 ORDER BY 位于 WHERE 之后,否则将会产生错误.

BETWEEN AND 范围值检查

1
2
3
-- BETWEEN AND 范围值检查
SELECT prod_name, prod_price FROM products
WHERE prod_price BETWEEN 5 AND 10;

IS NULL 空值检查

1
2
3
-- IS NULL 空值检查
SELECT cust_name FROM customers
WHERE cust_email IS NULL;

挑战题

  1. 编写 SQL 语句,从 Products 表中检索产品 ID(prod_id)和产品名称(prod_name),只返回价格为 9.49 美元的产品。
  2. 编写 SQL 语句,从 Products 表中检索产品 ID(prod_id)和产品名称(prod_name),只返回价格为 9 美元或更高的产品。
  3. 结合第 3 课和第 4 课编写 SQL 语句,从 OrderItems 表中检索出所有不同订单号(order_num),其中包含 100 个或更多的产品。
  4. 编写 SQL 语句,返回 Products 表中所有价格在 3 美元到 6 美元之间的产品的名称(prod_name)和价格(prod_price),然后按价格对结果进行排序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 1.
SELECT prod_id, prod_name FROM products
WHERE prod_price = 9.49

-- 2.
SELECT prod_id, prod_name FROM products
WHERE prod_price >= 9;

-- 3.
SELECT DISTINCT order_num FROM orderitems
WHERE order_num >= 100;

-- 4.
SELECT prod_name, prod_price FROM products
WHERE prod_price BETWEEN 3 AND 6 ORDER BY prod_price;

高级数据过滤

AND 和 OR 操作符

1
2
3
4
5
6
7
-- 添加多个过滤条件
SELECT prod_id, prod_price, prod_name FROM products
WHERE vend_id = 'DLL01' AND prod_price <= 4;

-- 满足其中一个过滤条件
SELECT prod_id, prod_price, prod_name FROM products
WHERE vend_id = 'DLL01' OR vend_id = 'BRS01';

IN 操作符

1
2
3
-- IN 限定为值为数组内的数据, 也可以用OR实现
SELECT prod_name, prod_price FROM products
WHERE vend_id IN ('DLL01', 'BRS01') ORDER BY prod_name;

IN对比OR的优势:

  1. 在有很多合法选项时,IN 操作符的语法更清楚,更直观。
  2. 在与其他 AND 和 OR 操作符组合使用 IN 时,求值顺序更容易管理。
  3. IN 操作符一般比一组 OR 操作符执行得更快.
  4. IN 的最大优点是可以包含其他 SELECT 语句,能够更动态地建立WHERE 子句。

NOT 操作符

1
2
3
4
5
6
7
-- NOT 否定条件
SELECT prod_name FROM products
WHERE NOT vend_id = 'DLL01' ORDER BY prod_name;

-- 等同于
SELECT prod_name FROM products
WHERE vend_id <> 'DLL01' ORDER BY prod_name;

在复杂的子句中,NOT 是非常有用的。例如,在与 IN 操作符联合使用时,NOT 可以非常简单地找出与条件列表不匹配的行.

挑战题

  1. 编写 SQL 语句,从 Vendors 表中检索供应商名称(vend_name),仅返回加利福尼亚州的供应商(这需要按国家[USA]和州[CA]进行过滤,没准其他国家也存在一个加利福尼亚州)。提示:过滤器需要匹配字符串。
  2. 编写 SQL 语句,查找所有至少订购了总量 100 个的 BR01、BR02 或BR03 的订单。你需要返回 OrderItems 表的订单号(order_num)、产品 ID(prod_id)和数量,并按产品 ID 和数量进行过滤。提示:根据编写过滤器的方式,可能需要特别注意求值顺序
  3. 现在,我们回顾上一课的挑战题。编写 SQL 语句,返回所有价格在 3美元到 6美元之间的产品的名称(prod_name)和价格(prod_price)。使用 AND,然后按价格对结果进行排序。
  4. 下面的 SQL 语句有问题吗?(尝试在不运行的情况下指出。)
    SELECT vend_name
    FROM Vendors
    ORDER BY vend_name
    WHERE vend_country = ‘USA’ AND vend_state = ‘CA’;
1
2
3
4
5
6
7
8
9
10
11
12
13
-- 1. 
SELECT vend_name FROM vendors
WHERE vend_country = 'USA' AND vend_state = 'CA';

-- 2.
SELECT order_num, prod_id, quantity FROM orderitems
WHERE quantity >= 100 AND prod_id IN('BR01', 'BR02', 'BR03');

-- 3.
SELECT prod_name, prod_price FROM products
WHERE prod_price >= 3 AND prod_price <= 6 ORDER BY prod_price;

-- 4. ORDER BY 应该放子句最后

使用通配符进行过滤

LIKE 操作符

%通配符

1
2
3
4
5
6
7
8
9
10
11
-- '%' 通配符表示任何字符出现任意次数,包括0次, 比如匹配Fish开头
SELECT prod_id, prod_name FROM products
WHERE prod_name LIKE 'Fish%';

-- 模式匹配中间为 特定字符串
SELECT prod_id, prod_name FROM products
WHERE prod_name LIKE '%bean bag%';

-- 模式匹配两端为 特定字符串
SELECT prod_id, prod_name FROM products
WHERE prod_name LIKE 'F%y';

_通配符

1
2
3
4
5
6
7
-- '_' 匹配单个字符, 多少个 '_' 就匹配多少个字符
SELECT prod_id, prod_name FROM products
WHERE prod_name LIKE '__ inch teddy bear';

-- SELECT cust_contact FROM customers
-- WHERE cust_contact LIKE '[^JM]%';
-- MySql 不支持 '[]'集合匹配
  1. 不要过度使用通配符。如果其他操作符能达到相同的目的,应该使用其他操作符。
  2. 在确实需要使用通配符时,也尽量不要把它们用在搜索模式的开始处。把通配符置于开始处,搜索起来是最慢的。
  3. 仔细注意通配符的位置。如果放错地方,可能不会返回想要的数据.

挑战题

  1. 编写 SQL 语句,从 Products 表中检索产品名称(prod_name)和描述(prod_desc),仅返回描述中包含 toy 一词的产品。
  2. 反过来再来一次。编写 SQL 语句,从 Products 表中检索产品名称(prod_name)和描述(prod_desc),仅返回描述中未出现 toy 一词的产品。这次,按产品名称对结果进行排序。
  3. 编写 SQL 语句,从 Products 表中检索产品名称(prod_name)和描述(prod_desc),仅返回描述中同时出现 toy 和 carrots 的产品。有好几种方法可以执行此操作,但对于这个挑战题,请使用 AND 和两个 LIKE 比较。
  4. 来个比较棘手的。我没有特别向你展示这个语法,而是想看看你根据目前已学的知识是否可以找到答案。编写 SQL 语句,从 Products 表中检索产品名称(prod_name)和描述(prod_desc),仅返回在描述中以先后顺序同时出现 toy 和 carrots 的产品。提示:只需要用带有三个 % 符号的 LIKE 即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 1.
SELECT prod_name, prod_desc FROM products
WHERE prod_desc LIKE '%toy%' ORDER BY prod_name;

-- 2.
SELECT prod_name, prod_desc FROM products
WHERE NOT prod_desc Like '%toy%' ORDER BY prod_name;

-- 3.
SELECT prod_name, prod_desc FROM products
WHERE prod_desc LIKE '%toy%' AND prod_desc LIKE '%carrots%';

-- or
SELECT prod_name, prod_desc FROM products
WHERE prod_desc LIKE '%toy%carrots%' OR prod_desc LIKE '%carrots%toy%';

-- 4.
SELECT prod_name, prod_desc FROM products
WHERE prod_desc LIKE '%toy%carrots%';

创建计算字段

拼接字段和别名

1
2
3
4
5
6
7
-- CONCAT 函数拼接字段, 不同数据库实现不同SQL Server使用+号。DB2、Oracle、PostgreSQL 和 SQLite 使用||
SELECT CONCAT(vend_name, '(', vend_country, ')')
FROM vendors ORDER BY vend_name;

-- AS 指定别名(alias)
SELECT CONCAT(vend_name, '(', vend_country, ')') AS vend_title
FROM vendors ORDER BY vend_name;

别名的名字既可以是一个单词,也可以是一个字符串。
别名有时也称为导出列(derived column)。

算术计算

1
2
3
-- 计算新列 quantity*item_price 并取别名 expanded_price
SELECT prod_id, quantity, item_price, quantity*item_price AS expanded_price
FROM orderitems WHERE order_num = 20008;

SELECT 检验计算

1
2
3
4
5
6
-- SELECT 语句 省略了 FORM 还可以仅访问和处理表达式
-- CURDATE() 函数返回日期
SELECT CURDATE();

-- RTRIM TRIM LTRIM 函数能去除空格
SELECT TRIM(' ab c '); -- 'ab c'

挑战题

  1. 别名的常见用法是在检索出的结果中重命名表的列字段(为了符合特定的报表要求或客户需求)。编写 SQL 语句,从 Vendors 表中检索vend_id、vend_name、vend_address 和 vend_city,将 vend_name重命名为 vname,将 vend_city 重命名为 vcity,将 vend_address
    重命名为 vaddress。按供应商名称对结果进行排序(可以使用原始名称或新的名称)。
  2. 我们的示例商店正在进行打折促销,所有产品均降价 10%。编写 SQL语句,从 Products 表中返回 prod_id、prod_price 和 sale_price。sale_price 是一个包含促销价格的计算字段。提示:可以乘以 0.9,得到原价的 90%(即 10%的折扣)。
1
2
3
4
5
6
7
8
9
10
11
12
-- 挑战题
-- 1.
SELECT vend_id AS vid,
vend_name AS vname,
vend_address AS vaddress,
vend_city AS vcity
FROM vendors ORDER BY vname;

-- 2.
SELECT prod_id, prod_price,
prod_price*0.9 AS sale_price
FROM products;

使用函数处理数据

不同数据库提供的函数各有不同, 一条SQL并不是通用的, 使用时要注意.

大多数 SQL 实现支持以下类型的函数:

  1. 用于处理文本字符串(如删除或填充值,转换值为大写或小写)的文本函数。
  2. 用于在数值数据上进行算术操作(如返回绝对值,进行代数运算)的数值函数。
  3. 用于处理日期和时间值并从这些值中提取特定成分(如返回两个日期之差,检查日期有效性)的日期和时间函数。
  4. 用于生成美观好懂的输出内容的格式化函数(如用语言形式表达出日期,用货币符号和千分位表示金额)。
  5. 返回 DBMS 正使用的特殊信息(如返回用户登录信息)的系统函数

文本处理函数

SOUNDEX 是一个将任何文
本串转换为描述其语音表示的字母数字模式的算法。SOUNDEX 考虑了使得能对字符串进行发音比较而不是字母比较。虽然 SOUNDEX 不是 SQL 概念,但多数 DBMS 都提供对 SOUNDEX 的支持.

1
2
3
4
5
6
7
8
9
10
11
12
13

-- UPPER()
SELECT vend_name, UPPER(vend_name) AS vend_name_upcase FROM vendors
ORDER BY vend_name;

-- SOUNDEX(), 对发音进行匹配
SELECT cust_name, cust_contact FROM customers
WHERE SOUNDEX(cust_contact) = SOUNDEX('Michael Green');


-- SUBSTR(str FROM pos FOR len)
SELECT CONCAT(SUBSTR("go000ogle", 1, 2), SUBSTR("go000ogle", 6, 4)) AS correct;
-- 这个函数下标从 1 开始左闭合取子串真的反常识, 第一次发现一门语言下标从1开始.

日期时间处理

1
2
3
4
-- MySQL 中用 YEAR() 从字符串中提取年份, 不难想到还有 MONTH() DAY()...
-- 不同数据库提供了非常多而全的函数, 用的时候直接查就行了
SELECT order_num, order_date FROM orders
WHERE YEAR(order_date) = '2020';

数值处理函数

1
2
-- 计算squareroot, e^x
SELECT SQRT(16), EXP(1);

挑战题

  1. 我们的商店已经上线了,正在创建顾客账户。所有用户都需要登录名,默认登录名是其名称和所在城市的组合。编写 SQL 语句,返回顾客 ID(cust_id)、顾客名称(customer_name)和登录名(user_login),其中登录名全部为大写字母,并由顾客联系人的前两个字符(cust_
    contact)和其所在城市的前三个字符(cust_city)组成。例如,我的登录名是 BEOAK(Ben Forta,居住在 Oak Park)。提示:需要使用函数、拼接和别名。
  2. 编写 SQL 语句,返回 2020 年 1 月的所有订单的订单号(order_num)和订单日期(order_date),并按订单日期排序。你应该能够根据目前已学的知识来解决此问题,但也可以开卷查阅 DBMS 文档。
1
2
3
4
5
6
7
8
9
10
11
12
-- 挑战题
-- 1.
SELECT cust_id, cust_name,
UPPER(CONCAT(SUBSTR(cust_contact FROM 1 FOR 2), SUBSTR(cust_city FROM 1 FOR 3)))AS user_login FROM customers;

-- or
SELECT cust_id, cust_name,
CONCAT(UPPER(LEFT(cust_contact, 2)), UPPER(LEFT(cust_city, 3))) AS user_login
FROM customers;

-- 2.
SELECT order_num, order_date FROM orders WHERE YEAR(order_date) = '2020' AND MONTH(order_date) = '1' ORDER BY order_date;

汇总数据

聚集函数

AVG()

1
2
3
-- 对特定行求平均
SELECT AVG(prod_price) AS avg_price FROM products
WHERE vend_id = 'DLL01';

AVG()函数忽略列值为 NULL 的行。
为了获得多个列的平均值,必须使用多个 AVG()函数。只有一个例外是要从多个列计算出一个值时.

COUNT()

1
2
3
4
5
-- COUNT(*) 对表中行数计算, 不论是否值为 NULL
SELECT COUNT(*) AS num_cust FROM customers;

-- 对指定列统计个数, 会忽略NULL
SELECT COUNT(cust_email) AS num_cust FROM customers;

MAX() MIN()

1
2
3
SELECT MAX(prod_price) AS max_price FROM products;

SELECT MIN(prod_price) AS min_price FROM products;

MIN(), MAX()函数忽略列值为 NULL 的行。
MIN()/MAX()在用于文本数据时, 返回按该列排序后的第一/最后行。

SUM()

1
2
3
4
5
SELECT SUM(quantity) AS item_ordered FROM orderitems
WHERE order_num = 20005;

SELECT SUM(item_price*quantity) AS total_price FROM orderitems
WHERE order_num = 20005;

SUM()函数忽略列值为 NULL 的行。

聚集不同的值

1
2
SELECT AVG(DISTINCT prod_price) AS avg_price FROM products
WHERE vend_id = 'DLL01';

ALL 参数不需要指定,因为它是默认行为。如果不指定 DISTINCT,则假定为 ALL.
DISTINCT 只能指定列名用于 COUNT(), 不能用于 COUNT(*)。DISTINCT 必须使用列名,不能用于计算或表达式。
有的 DBMS 还支持其他参数,如支持对查询结果的子集进行计算的 TOP 和 TOP PERCENT。

组合聚集函数

1
2
3
4
SELECT COUNT(*) AS num_items,
MIN(prod_price) AS price_min,
MAX(prod_price) AS price_max,
AVG(prod_price) AS price_avg FROM products;

在指定别名以包含某个聚集函数的结果时,不应该使用表中实际的列名。虽然这样做也算合法,但许多 SQL 实现不支持,可能会产生模糊的错误消息。

挑战题

  1. 编写 SQL 语句,确定已售出产品的总数(使用 OrderItems 中的quantity 列)。
  2. 修改刚刚创建的语句,确定已售出产品项(prod_item)BR01 的总数。
  3. 编写 SQL 语句,确定 Products 表中价格不超过 10 美元的最贵产品的价格(prod_price)。将计算所得的字段命名为 max_price。
1
2
3
4
5
6
7
8
9
10
11
-- 挑战题
-- 1.
SELECT SUM(quantity) AS total_quantity FROM orderitems;

-- 2.
SELECT SUM(order_item) AS total_item_BRO1 FROM orderitems
WHERE prod_id = 'BR01';

-- 3.
SELECT MAX(prod_price) AS max_price FROM products
WHERE prod_price <= 10;

分组数据

创建分组

1
2
3
--  GROUP BY 子句分组 对每个组数据进行聚集
SELECT vend_id, COUNT(*) AS num_prods FROM products
GROUP BY vend_id;
  1. GROUP BY 子句可以包含任意数目的列,因而可以对分组进行嵌套,更细致地进行数据分组
  2. 如果在 GROUP BY 子句中嵌套了分组,数据将在最后指定的分组上进行汇总。
  3. GROUP BY 子句中列出的每一列都必须是检索列或有效的表达式(但不能是聚集函数)。如果SELECT 中使用表达式,则必须在 GROUP BY子句中指定相同的表达式。不能使用别名。
  4. 大多数 SQL 实现不允许 GROUP BY 列带有长度可变的数据类型(如文本或备注型字段)。
  5. 除聚集计算语句外,SELECT 语句中的每一列都必须在 GROUP BY 子句中给出。
  6. 如果分组列中包含具有 NULL 值的行,则 NULL 将作为一个分组返回。如果列中有多行 NULL 值,它们将分为一组。
  7. GROUP BY 子句必须出现在 WHERE 子句之后,ORDER BY 子句之前。

过滤分组

1
2
3
4
5
6
7
8
-- HVAING 过滤分组
SELECT cust_id, COUNT(*) AS orders FROM orders
GROUP BY cust_id HAVING COUNT(*) = 1;

-- 先用WHERE过滤行, 再用HAVING 过滤分组
SELECT vend_id, COUNT(*) AS num_prods FROM products
WHERE prod_price >= 4
GROUP BY vend_id HAVING COUNT(*) >= 2;
  1. WHERE过滤行,而 HAVING 过滤分组。
  2. 有关 WHERE 的所有技术和选项都适用于 HAVING.
  3. 有另一种理解方法,WHERE 在数据分组前进行过滤,HAVING 在数据分组后进行过滤。WHERE 排除的行不包括在分组中。这可能会改变计算值,从而影响 HAVING 子句中基于这些值过滤掉的分组.

分组和排序

1
2
3
4

SELECT order_num, COUNT(*) AS items FROM orderitems
GROUP BY order_num HAVING COUNT(*) >= 3
ORDER BY items, order_num;

SELECT 子句顺序

挑战题

  1. OrderItems 表包含每个订单的每个产品。编写 SQL 语句,返回每个订单号(order_num)各有多少行数(order_lines),并按 order_lines对结果进行排序。
  2. 编写 SQL 语句,返回名为 cheapest_item 的字段,该字段包含每个供应商成本最低的产品(使用 Products 表中的 prod_price),然后从最低成本到最高成本对结果进行排序。
  3. 确定最佳顾客非常重要,请编写 SQL 语句,返回至少含 100 项的所有
    订单的订单号(OrderItems 表中的 order_num)。
  4. 确定最佳顾客的另一种方式是看他们花了多少钱。编写 SQL 语句,返回总价至少为 1000 的所有订单的订单号(OrderItems 表中的order_num)。提示:需要计算总和(item_price 乘以 quantity)。按订单号对结果进行排序。
  5. 下面的 SQL 语句有问题吗?(尝试在不运行的情况下指出。)
    SELECT order_num, COUNT() AS items
    FROM OrderItems
    GROUP BY items
    HAVING COUNT(
    ) >= 3
    ORDER BY items, order_num;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-- 挑战题
-- 1.
SELECT order_num, COUNT(*) AS order_lines FROM orderitems
GROUP BY order_num ORDER BY order_lines;

-- 2.
SELECT vend_id, MIN(prod_price) AS cheapest_item FROM products
GROUP BY vend_id ORDER BY cheapest_item DESC;

-- 3.
SELECT order_num, SUM(quantity) AS total_quantity FROM orderitems
GROUP BY order_num HAVING SUM(quantity) >= 100 ORDER BY order_num;

-- 4.
SELECT order_num, SUM(item_price*quantity) AS total_price FROM orderitems
GROUP BY order_num HAVING total_price >= 1000;
-- GROUP BY clause must constains all nonaggregated column

-- 5.
-- GROUP BY clause must constains all nonaggregated column

使用子查询

子查询过滤

1
2
3
4
5
6
7
8
SELECT cust_name, cust_contact FROM customers
WHERE cust_id IN (
SELECT cust_id FROM orders
WHERE order_num IN (
SELECT order_num FROM orderitems
WHERE prod_id = 'RGAN01'
)
);

作为子查询的 SELECT 语句只能查询单个列。企图检索多个列将返回错误。
使用子查询并不总是执行这类数据检索的最有效方法.

作为计算字段使用子查询

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;

-- orders 是一个计算字段,它是由圆括号中的子查询建立的。该子查询对检索出的每个顾客执行一次。
-- WHERE子句 比较Orders表中的cust_id和当前正从Customers表中检索的cust_id

SELECT 语句中操作多个表,就应使用完全限定列名来避免歧义。

挑战题

  1. 使用子查询,返回购买价格为 10 美元或以上产品的顾客列表。你需要使用 OrderItems 表查找匹配的订单号(order_num),然后使用Order 表检索这些匹配订单的顾客 ID(cust_id)。
  2. 你想知道订购 BR01 产品的日期。编写 SQL 语句,使用子查询来确定哪些订单(在 OrderItems 中)购买了 prod_id 为 BR01 的产品,然后从 Orders 表中返回每个产品对应的顾客 ID(cust_id)和订单日期(order_date)。按订购日期对结果进行排序。
  3. 现在我们让它更具挑战性。在上一个挑战题,返回购买 prod_id 为BR01 的产品的所有顾客的电子邮件(Customers 表中的 cust_email)。提示:这涉及 SELECT 语句,最内层的从 OrderItems 表返回 order_num,中间的从 Customers 表返回 cust_id。
  4. 我们需要一个顾客 ID 列表,其中包含他们已订购的总金额。编写 SQL语句,返回顾客 ID(Orders 表中的 cust_id),并使用子查询返回total_ordered 以便返回每个顾客的订单总数。将结果按金额从大到小排序。提示:你之前已经使用 SUM()计算订单总数。
  5. 再来。编写 SQL 语句,从 Products 表中检索所有的产品名称(prod_name),以及名为 quant_sold 的计算列,其中包含所售产品的总数(在 OrderItems 表上使用子查询和 SUM(quantity)检索)。
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
-- 挑战题
-- 1.
SELECT cust_id FROM orders
WHERE order_num IN
(SELECT order_num FROM orderitems
WHERE item_price >= 10);

-- 2.
SELECT cust_id, order_date FROM orders
WHERE order_num IN
-- 在orderitem中拿到'BR01'的 order_num
(SELECT order_num FROM orderitems
WHERE prod_id = 'BR01')
ORDER BY order_date;

-- 3.
SELECT cust_id, cust_email FROM customers
WHERE cust_id IN(
SELECT cust_id FROM orders
WHERE order_num IN(
SELECT order_num FROM orderitems
WHERE prod_id = 'BR01'))

-- 4.
SELECT cust_id,
(SELECT SUM(quantity * item_price)
FROM OrderItems
WHERE order_num IN (SELECT order_num
FROM Orders
WHERE Orders.cust_id = Customers.cust_id)) AS total_ordered
FROM Customers
ORDER BY total_ordered DESC;
-- 5.
SELECT prod_name,
(SELECT SUM(quantity) FROM orderitems
WHERE orderitems.prod_id = products.prod_id
) AS quant_sold
FROM products;

联结表

创建联结

1
2
3
4
5
6
7
SELECT vend_name, prod_name, prod_price
FROM vendors, products
WHERE vendors.vend_id = products.vend_id;

-- 直接这么写会得到笛卡尔积, WHERE 子句是查找过程中的过滤条件
SELECT *
FROM vendors, products;

内联结

等值联结(equijoin),基于两个表之间的相等测试。这种联结也称为内联结(inner join)。

1
2
3
4
5
SELECT vend_name, prod_name, prod_price 
FROM vendors
INNER JOIN products ON vendors.vend_id = products.vend_id;

-- 与上面的WHERE建立联结效果相同, ANSI SQL 规范首选 INNER JOIN 语法.

联结多个表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;


-- 子查询和联结比较
SELECT cust_name, cust_contact
FROM customers
WHERE cust_id IN (
SELECT cust_id FROM orders
WHERE order_num IN (
SELECT order_num FROM orderitems
WHERE prod_id = 'RGAN01'));

SELECT cust_name, cust_contact
FROM customers, orders, orderitems
WHERE customers.cust_id = orders.cust_id
AND orderitems.order_num = orders.order_num
AND prod_id = 'RGAN01';

挑战题

  1. 编写 SQL 语句,返回 Customers 表中的顾客名称(cust_name)和Orders 表中的相关订单号(order_num),并 按顾客名称再按订单号对结果进行排序。实际上是尝试两次,一次使用简单的等联结语法,一次使用 INNER JOIN。
  2. 我们来让上一题变得更有用些。除了返回顾客名称和订单号,添加第三列 OrderTotal,其中包含每个订单的总价。有两种方法可以执行此操作:使用 OrderItems 表的子查询来创建 OrderTotal 列,或者将 OrderItems 表与现有表联结并使用聚合函数。提示:请注意需要使用完全限定列名的地方。
  3. 我们重新看一下第 11 课的挑战题 2。编写 SQL 语句,检索订购产品BR01 的日期,这一次使用联结和简单的等联结语法。输出应该与第11 课的输出相同。
  4. 很有趣,我们再试一次。重新创建为第 11 课挑战题 3 编写的 SQL 语句,这次使用 ANSI 的 INNER JOIN 语法。在之前编写的代码中使用了两个嵌套的子查询。要重新创建它,需要两个 INNER JOIN 语句,每个语句的格式类似于本课讲到的 INNER JOIN 示例,而且不要忘记WHERE 子句可以通过 prod_id 进行过滤。
  5. 再让事情变得更加有趣些,我们将混合使用联结、聚合函数和分组。准备好了吗?回到第 10 课,当时的挑战是要求查找值等于或大于 1000的所有订单号。这些结果很有用,但更有用的是订单数量至少达到这个数的顾客名称。因此,编写 SQL 语句,使用联结从 Customers表返回顾客名称(cust_name),并从 OrderItems 表返回所有订单的总价。
    提示:要联结这些表,还需要包括 Orders 表(因为 Customers 表与 OrderItems 表不直接相关,Customers 表与 Orders 表相关,而Orders 表与 OrderItems 表相关)。不要忘记 GROUP BY 和 HAVING,并按顾客名称对结果进行排序。你可以使用简单的等联结或 ANSI 的INNER JOIN 语法。或者,如果你很勇敢,请尝试使用两种方式编写。
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
-- 挑战题
-- 1.
SELECT cust_name, order_num FROM customers, orders
WHERE customers.cust_id = orders.cust_id
ORDER BY cust_name, order_num;

SELECT cust_name, order_num FROM customers
INNER JOIN orders ON customers.cust_id = orders.cust_id
ORDER BY cust_name, order_num;

-- 2.
SELECT cust_name, order_num,
(SELECT SUM(quantity*item_price) FROM orderitems
WHERE orderitems.order_num = orders.order_num)
AS order_total FROM customers, orders
WHERE customers.cust_id = orders.cust_id
ORDER BY cust_name, order_num;

-- 时不我待
SELECT cust_name, orders.order_num, SUM(quantity*item_price) AS order_total
FROM orders, orderitems, customers
WHERE customers.cust_id = orders.cust_id
AND orders.order_num = orderitems.order_num
GROUP BY cust_name, orders.order_num
ORDER BY cust_name, orders.order_num;

-- 这里的 GROUP BY 中 cust_name 其实可以不加
-- 可以这么理解:

-- a. 先进行笛卡尔积
SELECT *
FROM orders, orderitems, customers
WHERE customers.cust_id = orders.cust_id
AND orders.order_num = orderitems.order_num
-- b. 执行 WHERE 子句的过滤联结条件
WHERE customers.cust_id = orders.cust_id
AND orders.order_num = orderitems.order_num
-- c. 然后按cust_name 和 order_num 分组, 不分先后 由于order_num是结果中的主键, 按这种分组方式可映射多个 cust_name, 所以cust_name可有可无
GROUP BY cust_name, orders.order_num
-- d. 返回选择的行, SUM()函数负责计算每个分组中的值
SELECT cust_name, orders.order_num, SUM(quantity*item_price) AS order_total


-- 3.
SELECT order_date, cust_id
FROM orders, orderitems
WHERE orders.order_num = orderitems.order_num
AND prod_id = 'BR01'
ORDER BY order_date;

SELECT order_date, cust_id
FROM orders
INNER JOIN orderitems
ON orderitems.order_num = orders.order_num
AND prod_id = 'BR01'
ORDER BY order_date;
-- 有点意思

-- 4.
SELECT customers.cust_id, cust_email
FROM customers
INNER JOIN orders ON orders.cust_id = customers.cust_id
INNER JOIN orderitems ON orderitems.order_num = orders.order_num
WHERE prod_id = 'BR01';

-- 5.
SELECT cust_name, SUM(item_price*quantity) AS total_price
FROM orderitems
INNER JOIN orders ON orders.order_num = orderitems.order_num
INNER JOIN customers ON customers.cust_id = orders.cust_id
GROUP BY cust_name HAVING SUM(item_price*quantity) >= 1000;


SELECT cust_name, SUM(item_price*quantity) AS total_price
FROM orderitems, orders, customers
WHERE orders.order_num = orderitems.order_num
AND customers.cust_id = orders.cust_id
GROUP BY cust_name HAVING SUM(item_price*quantity) >= 1000;

创建高级联结

使用表别名

1
2
3
4
5
6
SELECT cust_name, cust_contact
-- 给表起 别名 AS, ORACLE的DB中可以不加AS
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 = 'RGAN01';

SQL 除了可以对列名和计算字段使用别名,还允许给表名起别名。这样做有两个主要理由:

  1. 缩短 SQL 语句;
  2. 允许在一条 SELECT 语句中多次使用相同的表。
  3. 表别名只在查询执行中使用。与列别名不一样,表别名不返回到客户端。

使用不同类型的联结

自联结(self-join)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- 使用自联结
SELECT c1.cust_id, c1.cust_name, c1.cust_contact
FROM customers AS c1, customers AS c2
WHERE c1.cust_name = c2.cust_name
AND c2.cust_contact = 'Jim Jones';

-- OR 使用标准的内联结自联结
SELECT c1.cust_id, c1.cust_name, c1.cust_contact
FROM customers AS c1
INNER JOIN customers AS c2 ON c1.cust_name = c2.cust_name
AND c2.cust_contact = 'Jim Jones';

-- 相同效果子查询, 在一张表中, 查找 cust_contact为某值的cust_name, 再由cust_name查找对应的信息
SELECT cust_id, cust_name, cust_contact
FROM customers
WHERE cust_name =
(SELECT cust_name FROM customers
WHERE cust_contact = 'Jim Jones');

自然联结(natural join)

1
2
3
4
5
6
7
8
9
10
11
12
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 = 'RGAN01';

-- 等同于使用 JOIN
SELECT customers.*, orders.order_num, orders.order_date, orderitems.prod_id,orderitems.quantity, orderitems.item_price
FROM customers
JOIN orders ON customers.cust_id = orders.cust_id
JOIN orderitems ON orders.order_num = orderitems.order_num
AND prod_id = 'RGAN01';

自然联结返回结果只存在一个主键.
无论何时对表进行联结,应该至少有一列不止出现在一个表中.标准的联结返回所有数据,相同的列甚至多次出现。自然联结排除多次出现,使每一列只返回一次。
自然联结要求你只能选择那些唯一的列,一般通过对一个表使用通配符(SELECT *),而对其他表的列使用明确的子集来完成。

外联结(outer join)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- 外连接, LEFT/RIGHT 指定包括其所有行的表
-- 比如这里的 LEFT 指定的就是保留左边的表 customers
SELECT customers.cust_id, orders.order_num
FROM customers
LEFT OUTER JOIN orders ON customers.cust_id = orders.cust_id;

-- 上面等同于
SELECT customers.cust_id, orders.order_num
FROM orders
RIGHT OUTER JOIN customers ON customers.cust_id = orders.cust_id;

-- 可以比较下区别, 就算左边没有匹配条件的行也会保留, 右表的值置NULL
SELECT customers.cust_id, orders.order_num, customers.cust_id, orders.cust_id
FROM customers, orders ORDER BY customers.cust_id;

SELECT customers.cust_id, orders.order_num
FROM customers
INNER JOIN orders ON customers.cust_id = orders.cust_id;

联结包含了那些在相关表中没有关联行的行。这种联结称为外联结.
左外联结和右外联结能相互转换.

使用带聚集函数的联结

1
2
3
4
5
6
7
8
9
SELECT customers.cust_id, COUNT(orders.order_num) AS num_ord
FROM customers
INNER JOIN orders ON customers.cust_id = orders.cust_id
GROUP BY customers.cust_id;

SELECT customers.cust_id, COUNT(orders.order_num) AS num_ord
FROM customers
LEFT OUTER JOIN orders ON customers.cust_id = orders.cust_id
GROUP BY customers.cust_id;

使用联结和联结条件

  1. 注意所使用的联结类型。一般我们使用内联结,但使用外联结也有效。
  2. 关于确切的联结语法,应该查看具体的文档,看相应的 DBMS 支持何种语法.
  3. 保证使用正确的联结条件(不管采用哪种语法),否则会返回不正确的数据。
  4. 应该总是提供联结条件,否则会得出笛卡儿积。
  5. 在一个联结中可以包含多个表,甚至可以对每个联结采用不同的联结类型。虽然这样做是合法的,一般也很有用,但应该在一起测试它们前分别测试每个联结。这会使故障排除更为简单。

挑战题

  1. 使用 INNER JOIN 编写 SQL语句,以检索每个顾客的名称(Customers表中的 cust_name)和所有的订单号(Orders 表中的 order_num)
  2. 修改刚刚创建的 SQL 语句,仅列出所有顾客,即使他们没有下过订单。
  3. 使用 OUTER JOIN 联结 Products 表和 OrderItems 表,返回产品名称(prod_name)和与之相关的订单号(order_num)的列表,并按商品名称排序。
  4. 修改上一题中创建的 SQL 语句,使其返回每一项产品的总订单数(不是订单号)。
  5. 编写 SQL语句,列出供应商(Vendors 表中的 vend_id)及其可供产品的数量,包括没有产品的供应商。你需要使用 OUTER JOIN 和 COUNT()聚合函数来计算 Products 表中每种产品的数量。注意:vend_id 列会显示在多个表中,因此在每次引用它时都需要完全限定它
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
-- 挑战题
-- 1.
SELECT cust_name, order_num
FROM customers
INNER JOIN orders
ON customers.cust_id = orders.cust_id;

-- 2.
SELECT cust_name, order_num
FROM customers
LEFT OUTER JOIN orders
ON customers.cust_id = orders.cust_id;

-- 3.
SELECT prod_name, order_num
FROM products
LEFT OUTER JOIN orderitems
ON products.prod_id = orderitems.prod_id;

-- 4.
SELECT prod_name, COUNT(order_num) AS orders
FROM products
LEFT OUTER JOIN orderitems
ON products.prod_id = orderitems.prod_id
GROUP BY prod_name
ORDER BY prod_name;

-- 5.
SELECT vendors.vend_id, COUNT(prod_id)
FROM vendors
LEFT OUTER JOIN products
ON products.vend_id = vendors.vend_id
GROUP BY vendors.vend_id;

组合查询

多数 SQL 查询只包含从一个或多个表中返回数据的单条 SELECT 语句。但是,SQL 也允许执行多个查询(多条 SELECT 语句),并将结果作为一个查询结果集返回。这些组合查询通常称为并(union)或复合查询(compound query)。
主要有两种情况需要使用组合查询:

  1. 在一个查询中从不同的表返回结构数据;
  2. 对一个表执行多个查询,按一个查询返回数据。
  3. 任何具有多个WHERE 子句的 SELECT 语句都可以作为一个组合查询.

创建组合查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- UNION 将两条SELECT的查询结果合并成一条
-- 等同于多个条件的 WHERE, 性能可能分情况有差别
SELECT cust_name, cust_contact, cust_email
FROM customers
WHERE cust_state IN ('IL', 'IN', 'MI')
UNION
SELECT cust_name, cust_contact, cust_email
FROM customers
WHERE cust_name = 'Fun4ALL';

-- UNION ALL 不取消重复行
SELECT cust_name, cust_contact, cust_email
FROM customers
WHERE cust_state IN ('IL', 'IN', 'MI')
UNION ALL
SELECT cust_name, cust_contact, cust_email
FROM customers
WHERE cust_name = 'Fun4All';
  1. UNION 必须由两条或两条以上的 SELECT 语句组成,语句之间用关键字 UNION 分隔
  2. UNION 从查询结果集中自动去除了重复的行, 行为与一条 SELECT 语句中使用多个 WHERE 子句一样.
  3. 如果确实需要每个条件的匹配行全部出现(包括重复行),就必须使用 UNION ALL,而不是 WHERE。
  4. UNION 中的每个查询必须包含相同的列、表达式或聚集函数(不过,各个列不需要以相同的次序列出)
  5. 列数据类型必须兼容:类型不必完全相同,但必须是 DBMS 可以隐含转换的类型(例如,不同的数值类型或不同的日期类型)
  6. 如果结合 UNION 使用的 SELECT 语句遇到不同的列名, 会返回第一个名字.

对组合查询结果排序

1
2
3
4
5
6
7
8
9
-- 组合查询只允许一条 ORDER BY, 放最后排序整个结果集
SELECT cust_name, cust_contact, cust_email
FROM Customers
WHERE cust_state IN ('IL','IN','MI')
UNION
SELECT cust_name, cust_contact, cust_email
FROM Customers
WHERE cust_name = 'Fun4All'
ORDER BY cust_name, cust_contact;

挑战题

  1. 编写 SQL 语句,将两个 SELECT 语句结合起来,以便从 OrderItems表中检索产品 ID(prod_id)和 quantity。其中,一个 SELECT 语句过滤数量为 100 的行,另一个 SELECT 语句过滤 ID 以 BNBG 开头的产品。按产品 ID 对结果进行排序。
  2. 重写刚刚创建的 SQL 语句,仅使用单个 SELECT 语句。
  3. 我知道这有点荒谬,但这节课中的一个注释提到过。编写 SQL 语句,组合 Products 表中的产品名称(prod_name)和 Customers 表中的顾客名称(cust_name)并返回,然后按产品名称对结果进行排序。
  4. 下面的 SQL 语句有问题吗?(尝试在不运行的情况下指出。)
    SELECT cust_name, cust_contact, cust_email
    FROM Customers
    WHERE cust_state = ‘MI’
    ORDER BY cust_name;
    UNION
    SELECT cust_name, cust_contact, cust_email
    FROM Customers
    WHERE cust_state = 'IL’ORDER BY cust_name;
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
-- 挑战题
-- 1.
SELECT prod_id, quantity FROM orderitems
WHERE quantity = 100
UNION
SELECT prod_id, quantity FROM orderitems
WHERE prod_id LIKE 'BNBG%'
ORDER BY prod_id;

-- 2.
SELECT prod_id, quantity FROM orderitems
WHERE quantity = 100
ORDER BY prod_id;

SELECT prod_id, quantity FROM orderitems
WHERE prod_id LIKE 'BNBG%'
ORDER BY prod_id;

-- 3.
SELECT prod_name
FROM products
UNION
SELECT cust_name
FROM customers
ORDER BY prod_name;
-- that's nonsensial indeed, cust_name list in the list of prod_name

-- 4. ORDER BY 只能存在一个放在最后

插入数据

INSERT 用来将行插入(或添加)到数据库表。插入有几种方式:

  1. 插入完整的行
  2. 插入行的一部分
  3. 插入某些查询的结果

使用 INSERT 语句可能需要客户端/服务器 DBMS 中的特定安全权限.

插入完整的行

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
-- 插入一行, VALUES数组按模式顺序填充
INSERT INTO customers
VALUES(1000000006,
'Slacr Land',
'Fuck Street',
'New York',
'NY',
'11111',
'USB',
NULL,
NULL);

-- 更安全的方法, 给出明确的列名, 次序可以变更
INSERT INTO customers(cust_name,
cust_id,
cust_address,
cust_city,
cust_state,
cust_zip,
cust_country,
cust_contact,
cust_email)
VALUES('Slacr Land',
1000000006,
'Fuck Street',
'New York',
'NY',
'11111',
'USA',
NULL,
NULL);

编写依赖于特定列次序的 SQL 语句很不安全, 不能保证各列在下一次表结构变动后保持完全相同的次序

插入部分行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
INSERT INTO Customers(cust_id,
cust_name,
cust_address,
cust_city,
cust_state,
cust_zip,
cust_country)
VALUES(1000000006,
'Toy Land',
'123 Any Street',
'New York',
'NY',
'11111',
'USA');

可以在 INSERT 操作中省略某些列。省略的列必须满足以下某个条件。

  1. 该列定义为允许 NULL 值(无值或空值)。
  2. 在表定义中给出默认值。这表示如果不给出值,将使用默认值。

插入检索出的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 -- 选择插入, 从 cust_new 表中选择数据插入customer表
INSERT INTO customers(cust_id,
cust_contact,
cust_email,
cust_name,
cust_address,
cust_city,
cust_state,
cust_zip,
cust_country)
SELECT cust_id,
cust_contact,
cust_email,
cust_name,
cust_address,
cust_city,
cust_state,
cust_zip,
cust_country
FROM cust_new;
  1. 列名不一定要相同.
  2. INSERT SELECT 中 SELECT 语句可以包含 WHERE 子句,以过滤插入的数据。
  3. INSERT SELECT是个例外,它可以用一条INSERT插入多行.

从一个表复制到另一个表

1
2
3
4
5
-- 创建新表, 用原有表的数据, 可以使用更多条件的SELECT
CREATE TABLE cust_copy AS SELECT * FROM customers;

-- SQL SERVER
SELECT * INTO CustCopy FROM Customers;

挑战题

  1. 使用 INSERT 和指定的列,将你自己添加到 Customers 表中。明确列出要添加哪几列,且仅需列出你需要的列。
  2. 备份 Orders 表和 OrderItems 表.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-- 挑战题
-- 1.
INSERT INTO customers(cust_name,
cust_id,
cust_address,
cust_city,
cust_state,
cust_zip,
cust_country,
cust_contact,
cust_email)
VALUES('slacr',
'001',
'CY',
'CQ',
'NAN',
'1024',
'CN',
'hacr',
'xxx');

-- 2.
CREATE TABLE backup_orders AS SELECT * FROM orders;
CREATE TABLE backup_orderitems AS SELECT * FROM orderitems;

更新和删除数据

更新数据

1
2
3
4
5
6
7
8
9
UPDATE customers 
SET cust_email = 'shit@shool.edu'
WHERE cust_id = 1000000005;

-- 更新多个列
UPDATE customers
SET cust_contact = 'slacr',
cust_email = NULL
WHERE cust_id = 1000000006;

删除数据

1
2
3
4
5
-- 删除指定条目
DELETE FROM customers
WHERE cust_id = 'slacr';

-- 省略 WHERE 子句会删除所有
  1. DELETE 语句从表中删除行,甚至是删除表中所有行。但是,DELETE不删除表本身。
  2. 如果想从表中删除所有行,不要使用 DELETE。可使用 TRUNCATE TABLE语句,它完成相同的工作,而速度更快(因为不记录数据的变动)

下面是许多 SQL 程序员使用 UPDATE 或 DELETE 时所遵循的重要原则:

  1. 除非确实打算更新和删除每一行,否则绝对不要使用不带 WHERE 子句的 UPDATE 或 DELETE 语句。
  2. 保证每个表都有主键,尽可能像 WHERE 子句那样使用它(可以指定各主键、多个值或值的范围)。
  3. 在 UPDATE 或 DELETE 语句使用 WHERE 子句前,应该先用 SELECT 进行测试,保证它过滤的是正确的记录,以防编写的 WHERE 子句不正确。
  4. 使用强制实施引用完整性的数据库, 这样 DBMS 将不允许删除其数据与其他表相关联的行。
  5. 有的 DBMS 允许数据库管理员施加约束,防止执行不带 WHERE 子句的 UPDATE 或 DELETE 语句。如果所采用的 DBMS 支持这个特性,应该使用它。

挑战题

  1. 美国各州的缩写应始终用大写。编写 SQL语句来更新所有美国地址,包括供应商状态(Vendors 表中的 vend_state)和顾客状态(Customers表中的 cust_state),使它们均为大写。
  2. 第 15 课的挑战题 1 要求你将自己添加到 Customers 表中。现在请删除自己。确保使用 WHERE 子句(在 DELETE 中使用它之前,先用 SELECT对其进行测试),否则你会删除所有顾客!
1
2
3
4
5
6
7
8
9
10
11
12
-- 挑战题
-- 1.
UPDATE customers
SET cust_state = UPPER(cust_state);
UPDATE vendors
SET vend_state = UPPER(vend_state);

-- 2.
SELECT * FROM customers WHERE cust_id = 1000000006;

DELETE FROM customers
WHERE cust_id = 1000000006;

创建和操纵表

创建表

1
2
3
4
5
6
7
8
9
10
CREATE TABLE products
(
prod_id CHAR(10) NOT NULL,
vend_id CHAR(10) NOT NULL,
prod_name CHAR(254) NOT NULL,
prod_price DECIMAL(8,2) NOT NULL DEFAULT 1,
prod_desc VARCHAR(1000) NULL
-- NULL 可以不写为默认值, 设置了NOT NULL 限定的列不可为空, 插入时也必须给值
-- DEFAULT 设置默认值
)

使用 DBMS 创建表实际上是 程序帮你自动执行 SQL.
NULL 值是没有值,不是空字符串。
primary key 一定 NOT NULL

日期或时间戳通常使用系统值, 可以调用函数给出默认值 DEFAULT CURRENT_DATE().

更新表

1
2
3
4
5
ALTER TABLE vendors
ADD vend_phone CHAR(20);

ALTER TABLE vendors
DROP COLUMN vend_phone;

使用 ALTER TABLE 时需要考虑:

  1. 理想情况下,不要在表中包含数据时对其进行更新。应该在表的设计过程中充分考虑未来可能的需求,避免今后对表的结构做大改动。
  2. 所有的 DBMS 都允许给现有的表增加列,不过对所增加列的数据类型(以及 NULL 和 DEFAULT 的使用)有所限制。
  3. 许多 DBMS 不允许删除或更改表中的列。
  4. 多数 DBMS 允许重新命名表中的列。
  5. 许多 DBMS 限制对已经填有数据的列进行更改,对未填有数据的列几乎没有限制

复杂的表结构更改一般需要手动删除过程,它涉及以下步骤:

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

删除表

1
DROP TABLE cust_copy;

重命名表

1
ALTER TABLE backup_orders RENAME copy_orders;

挑战题

  1. 在 Vendors 表中添加一个网站列(vend_web)。你需要一个足以容纳 URL 的大文本字段。
  2. 使用 UPDATE 语句更新 Vendor 记录,以便加入网站(你可以编造任何地址)。
1
2
3
4
5
6
7
-- 挑战题
ALTER TABLE vendors
ADD COLUMN vend_web VARCHAR(1000);

UPDATE vendors
SET vend_web = 'www.slacr.site'
WHERE vend_country = 'USA';

使用视图

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

为什么使用视图:

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

创建视图之后,可以用与表基本相同的方式使用它们。可以对视图执行SELECT 操作,过滤和排序数据,将视图联结到其他视图或表,甚至添加和更新数据.
视图仅仅是用来查看存储在别处数据的一种设施。视图本身不包含数据,因此返回的数据是从其他表中检索出来的。

视图创建和使用的一些最常见的规则和限制:

  1. 与表一样,视图必须唯一命名(不能给视图取与别的视图或表相同的名字)。
  2. 对于可以创建的视图数目没有限制。
  3. 创建视图,必须具有足够的访问权限。这些权限通常由数据库管理人员授予。
  4. 视图可以嵌套,即可以利用从其他视图中检索数据的查询来构造视图。所允许的嵌套层数在不同的 DBMS中有所不同(嵌套视图可能会严重降低查询的性能,因此在产品环境中使用之前,应该对其进行全面测试).
  5. 许多 DBMS 禁止在视图查询中使用 ORDER BY 子句。
  6. 有些 DBMS 要求对返回的所有列进行命名,如果列是计算字段,则需要使用别名
  7. 视图不能索引,也不能有关联的触发器或默认值。
  8. 有些 DBMS 把视图作为只读的查询,这表示可以从视图检索数据,但不能将数据写回底层表。详情请参阅具体的 DBMS 文档

创建使用视图

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
-- 利用视图简化复杂联结
CREATE VIEW prod_cust
AS SELECT cust_name, cust_contact, prod_id
FROM customers, orders, orderitems
WHERE customers.cust_id = orders.cust_id
AND orderitems.order_num = orders.order_num;

-- 删除视图
DROP VIEW prod_cust;

-- 对视图检索
SELECT cust_name, cust_contact
FROM prod_cust
WHERE prod_id = 'RGAN01';

-- 使用视图重新格式化选择出来的数据
-- 在SELECT前面补上CREATE VIEW 语句就行了
CREATE VIEW vendor_locations AS
SELECT CONCAT(vend_name, ' [ ', vend_country, ' ]')
AS vend_title
FROM vendors;

-- 使用视图过滤不想要的数据
CREATE VIEW cust_email_list AS
SELECT cust_id, cust_name, cust_email
FROM customers
WHERE cust_email IS NOT NULL;

SELECT * FROM cust_email_list;

-- 使用视图与计算字段
CREATE VIEW order_items_expanded AS
SELECT prod_id, quantity, item_price, quantity*item_price
AS expanded_price FROM orderitems
WHERE order_num = 20008;

SELECT * FROM order_items_expanded;

挑战题

  1. 创建一个名为 CustomersWithOrders 的视图,其中包含 Customers表中的所有列,但仅仅是那些已下订单的列。提示:可以在 Orders表上使用 JOIN 来仅仅过滤所需的顾客,然后使用 SELECT 来确保拥
    有正确的数据。
  2. 下面的 SQL 语句有问题吗?(尝试在不运行的情况下指出。)
    CREATE VIEW OrderItemsExpanded AS
    SELECT order_num,
    prod_id,
    quantity,
    item_price,
    quantity*item_price AS expanded_price
    FROM OrderItems
    ORDER BY order_num;
1
2
3
4
5
6
7
8
9
-- 挑战题
-- 1.
CREATE VIEW cust_with_orders AS
SELECT customers.* FROM customers
JOIN orders ON customers.cust_id = orders.cust_id;

SELECT * FROM cust_with_orders;

-- 2. MYSQL中没问题, 但有些数据库创建VIEW不能加ORDER BY字段, 可以创建后再检索视图

存储过程和函数

存储过程就是为以后使用而保存的一条或多条 SQL 语句, store routine。可将其视为批文件,虽然它们的作用不仅限于批处理。

为什么要使用存储过程:

  1. 通过把处理封装在一个易用的单元中,可以简化复杂的操作
  2. 由于不要求反复建立一系列处理步骤,因而保证了数据的一致性。如果所有开发人员和应用程序都使用同一存储过程,则所使用的代码都是相同的。
  3. 上一点的延伸就是防止错误。需要执行的步骤越多,出错的可能性就越大。防止错误保证了数据的一致性
  4. 简化对变动的管理。如果表名、列名或业务逻辑(或别的内容)有变化,那么只需要更改存储过程的代码。使用它的人员甚至不需要知道这些变化
  5. 上一点的延伸就是安全性。通过存储过程限制对基础数据的访问,减少了数据讹误(无意识的或别的原因所导致的数据讹误)的机会。
  6. 因为存储过程通常以编译过的形式存储,所以 DBMS 处理命令所需的工作量少,提高了性能。
  7. 存在一些只能用在单个请求中的 SQL 元素和特性,存储过程可以使用它们来编写功能更强更灵活的代码。

不同DB实现的语法相差很大, MySQL参见link

存储过程

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
CREATE PROCEDURE IF NOT EXISTS HighPriceProd(IN price DECIMAL(8, 2))
BEGIN
SELECT prod_id, prod_price FROM products
WHERE prod_price >= price
ORDER BY prod_price;
END
-- 删除
DROP PROCEDURE HighPriceProd;
-- 调用
CALL HighPriceProd(5);

--
CREATE PROCEDURE SetPrice(IN pid CHAR(10), IN price DECIMAL(8,2))
BEGIN
UPDATE products
SET prod_price = price WHERE prod_id = pid;
SELECT prod_id, prod_price FROM products;
END

DROP PROCEDURE SetPrice;
CALL SetPrice('BR01', 100);

-- 利用 @ 标识全局变量
SET @price = 200, @pid = 'BR01';
CALL SetPrice(@pid, @price);

-- OUT 输出参数
CREATE PROCEDURE outTest(OUT test INT)
BEGIN
SELECT test;
SET test = 99;
SELECT test;
END

SET @test = 111;
CALL outTest(@test); -- NULL, 不接受参数

SELECT @test; -- 99, 调用存储过程

IN 输入参数:表示调用者向过程传入值(传入值可以是字面量或变量)
OUT 输出参数:表示过程向调用者传出值(可以返回多个值)(传出值只能是变量)
INOUT 输入输出参数:既表示调用者向过程传入值,又表示过程向调用者传出值(值只能是变量)

存储函数

1
2
3
4
5
6
7
8
-- 创建存储函数
CREATE FUNCTION hello (s CHAR(20)) RETURNS CHAR(50)
DETERMINISTIC
RETURN CONCAT('Hello,', s, '!');

SELECT hello('world');

DROP FUNCTION hello;

管理事务处理(transaction processing)

使用事务处理(transaction processing),通过确保成批的 SQL 操作要么完全执行,要么完全不执行,来维护数据库的完整性。

事务处理是一种机制,用来管理必须成批执行的 SQL 操作,保证数据库不包含不完整的操作结果。利用事务处理,可以保证一组操作不会中途停止,它们要么完全执行,要么完全不执行(除非明确指示)。如果没有错误发生,整组语句提交给(写到)数据库表;如果发生错误,则进行回退(撤销),将数据库恢复到某个已知且安全的状态。

事务处理用来管理 INSERT、UPDATE 和 DELETE 语句。不能回退 SELECT, CREATE 或 DROP 操作。事务处理中可以使用这些语句,但进行回退时,这些操作也不撤销。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
INSERT INTO orders VALUES( 200010, '2023-02-08 00:00:00', '1000000001');

START TRANSACTION;
SAVEPOINT del_1;
DELETE FROM orders WHERE order_num = 200010;
-- ROLLBACK; -- ROLLBACK可以撤销上一句SQL
INSERT INTO orders VALUES( 200111, '2023-02-10 00:00:00', '1000000001');
ROLLBACK TO del_1; -- 回退到保存点
COMMIT;
-- 只有COMMIT结束 才会完成整个TRANSACTION, 默认自动COMMIT


-- By default, MySQL runs with autocommit mode enabled.
-- SET autocommit = {0|1}

使用游标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CREATE PROCEDURE test()
BEGIN
DECLARE str CHAR(50);
DECLARE name_cursor CURSOR FOR SELECT cust_name FROM customers;

OPEN name_cursor;

read_loop: LOOP
FETCH name_cursor INTO str;
IF str LIKE "%T%" THEN
LEAVE read_loop;
END IF;
END LOOP;

CLOSE name_cursor;

SELECT * FROM customers WHERE cust_name = str;
END

CALL test();
  1. 在使用游标前,必须声明(定义)它。这个过程实际上没有检索数据,它只是定义要使用的 SELECT 语句和游标选项。
  2. 一旦声明,就必须打开游标以供使用。这个过程用前面定义的 SELECT语句把数据实际检索出来。
  3. 对于填有数据的游标,根据需要取出(检索)各行。
  4. 在结束游标使用时,必须关闭游标,可能的话,释放游标(有赖于具体的 DBMS)

知识点

  1. SQL不区分大小写, 但是表名、列名和值可能有所不同(这有赖于具体的 DBMS 及其如何配置)。
  2. 并非所有数据库的SQL实现都一样, 基本语句往往向通.
  3. SQL忽略换行, 可以一条语句分多段, 分号结尾.
  4. SQL 语句一般返回原始的、无格式的数据,不同的 DBMS 和客户端显示数据的方式略有不同(如对齐格式不同、小数位数不同)。数据的格式化是表示问题,而不是检索问题。因此,如何表示一般会在显示该数据的应用程序中规定。通常很少直接使用实际检索出的数据(没有应用程序提供的格式)。

参考

  1. 《SQL必知必会 第五版》 Ben Forta IBSN 9787115539168
  2. Oracle Live SQL
  3. 教材答案
  4. MySql中文文档
  5. MySql官方文档
  6. 菜鸟教程-存储过程
  7. MySQL存储函数
  • Author:

    slacr_

  • Copyright:

  • Published:

    May 9, 2023

  • Updated:

    May 9, 2023

Buy me a cup of coffee ☕.

1000000