1. 官方文档
MySQL:https://dev.mysql.com/doc/refman/8.4/en/
SQL Server:What is SQL Server? - SQL Server | Microsoft Learn
Oracle:https://docs.oracle.com/en/database/oracle/oracle-database/23/lnpls/loe.html
2. 术语
- SQL Structured Query Language -> 从数据库中读写数据。
- SQL 语句忽略所有的空格。SQL 语句可以用一行给出,也可以分成许多行(分行更容易阅读和调试);
- 多条 SQL 语句必须以分号(;)分隔;
- SQL 语句不区分大小写(为了使代码更易于阅读和调试,通常对所有 SQL 关键字,如 SELECT,使用大写,而对所有列和表名使用小写)。
- 数据库 database -> 保存有组织的数据的容器,比如保存在硬盘设备上的一个文件或一组文件。
- 数据库管理系统(数据库软件)DBMS -> 用于访问数据库。
- 表 -> 某种特定类型数据的结构化清单。表名是唯一的,在同一个数据库中,不能两次使用相同的名字,但在不同的数据库可以使用相同的表名。在有可能混淆列名时,应使用完全限定列名(用一个点分隔表名和列名),如:Orders.cust_id 表示 Orders 表中的 cust_id 列。
- 列 -> 表中的一个字段。每列都有相应的数据类型,限制该列中存储的数据。数据类型能够帮助正确地分类数据,以及优化磁盘的使用。
- 行 -> 表中的一个记录。
- 关键字 -> SQL 语句组成部分。关键字不能用作表或列的名字。
- 子句 -> SQL 语句由子句构成,一个子句通常由一个关键字加上数据组成,如 SELECT 语句的 FROM 子句。
- 主键 -> 一列或一组列,用于唯一标识表中每行。使用多列作为主键时,所有列值的组合必须是唯一的,但单个列可以不唯一。
- 主键使用关键字 PRIMARY KEY 定义。
- 作为主键的列需要满足以下条件:
- 任意两行都不具有相同的主键值;
- 每个行都必须具有一个主键值,主键列不允许 NULL 值;
- 主键列的值不允许修改或更新;
- 主键值不能重用,如果某行从表中删除,它的主键值不能赋给以后的新行。
- 外键 -> 定义一个表中的一个列(或一组列),其值必须匹配另一个表中的主键或唯一键。举个例子:订单表 Orders 中的订单通过客户 ID 与客户表 Customers 中的特定行相关联。在 Orders 的客户 ID 列上定义一个外键,使这列只能接受 Customers 表的主键值。
- 外键使用关键字 REFERENCES 定义
- 外键可以用来确保表之间的数据一致性和完整性,同时可以帮助防止意外删除(在定义外键后,不能删除在另一个表中具有关联行的行,例如,不能删除具有订单的客户 ID,只有先删除订单后,才能删除该客户)。
3. SELECT 语句:检索一个或多个数据列
(1)检索单个列
SELECT prod_name FROM Products;
如果没有明确地对查询结果排序,返回的数据的顺序没有特殊意义,可能是数据最初被添加到表中的顺序(如果数据后来进行过更新和删除,此顺序也会受到 DBMS 重用回收存储空间的影响),也可能不是,只要返回的行数对,就是正常的。
(2)检索多个列 -> 列名间用逗号分隔
SELECT prod_id, prod_name, prod_price FROM Products;
(3)检索所有列 -> 使用星号(*)通配符
SELECT * FROM Products;
4. ORDER BY 子句:排序检索出的数据
- 指定 ORDER BY 子句时,应保证它是 SELECT 语句的最后一条子句,如果次序不对会出现错误消息;
- ORDER BY 子句中使用的列可以是非检索的列;
- 默认按升序排序(A-Z),可以通过指定 DESC 关键字(descending)进行降序排序(Z-A),DESC 关键字只作用到直接位于其前面的列名;
- DESC 的反面是 ASC(ascending),但实际上 ASC 没有太大用处,因为默认就是升序的。
(1)取一个列或多个列(列名间用逗号分隔)的名字,据此对输出进行排序
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)按选择列的相对位置排序(如果进行排序的列不在 SELECT 清单中,不能使用这种方法)
SELECT prod_id, prod_price, prod_price FROM Products
ORDER BY 2, 3;
(3)指定排序方向
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;
如果对 prod_price 列指定 DESC,对 prod_name 列不指定,则 prod_price 列将以降序排序,prod_name 列将仍按升序排序。如果想在多个列上进行降序排序,必须对每个列指定 DESC 关键字。
5. WHERE 子句:过滤数据
5.1 基础:搜索条件
数据库种通常含有大量数据,实践中很少需要检索表中所有行,通常会根据报告需要,提取表数据的子集。只检索所需数据,需要指定搜索条件(search criteria,也称过滤条件 filter condition)。
- SELECT 语句将根据 WHERE 子句指定的搜索条件进行过滤;
- WHERE 语句在表名(FROM 子句)之后给出。
- WHERE 语句中,用来与数值列进行比较的值不用引号,但如果是与串类型的列进行比较的值,要把这个值放在单引号内。
- WHERE 子句可以使用非选择列。
比较操作符(=\!=\>\<\>=\<=\BETWEEN AND)
操作符 | 说明 |
= | 等于 |
<> 或 != | 不等于 |
> | 大于 |
< | 小于 |
>= 或 !< | 大于等于(不小于) |
<= 或 !> | 小于等于(不大于) |
BETWEEN AND | 在指定的两个值之间 BETWEEN 需要指定两个值,开始值和结束值,这两个值必须用 AND 关键字分隔;BETWEEN 匹配范围中的所有值,包括指定的开始和结束值。 |
SELECT prod_name, prod_price
FROM Products
WHERE prod_price = 3.12;
SELECT prod_name, prod_price
FROM Products
WHERE prod_price < 10;
SELECT prod_name, prod_price
FROM Products
WHERE prod_price <= 10;
SELECT prod_name, prod_price
FROM Products
WHERE prod_price BETWEEN 5 AND 10;
SELECT vend_id, prod_price
FROM Products
WHERE vend_id <> 'DLL01';
IS NULL\IS NOT NULL 操作符
SELECT DISTINCT user_id
FROM orders
WHERE EXISTS
(SELECT * FROM users WHERE users.user_id = orders.user_id);
IN 操作符 和 NOT 操作符
- IN -> WHERE 子句中用来指定要匹配值的清单的关键字,功能与 OR 相当。IN 操作符后跟一个圆括号括起来,由逗号分隔的合法值清单。
- NOT -> WHERE 子句中用来来否定后跟条件的关键字。NOT 否定它之后所有的任何条件。
SELECT prod_name, prod_price
FROM Products
WHERE vend_id IN ('DLL01', 'BRS01');
SELECT prod_name, prod_price
FROM Products
WHERE vend_id = 'DLL01' OR vend_id = 'BRS01';
SELECT prod_name
FROM Products
WHERE NOT vend_id = 'DLL01';
SELECT * FROM Customers
WHERE Country IN ('Germany', 'France', 'UK');
返回 Orders 表中有订单的客户的所有信息:
SELECT * FROM Customers
WHERE CustomerID IN (SELECT CustomerID FROM Orders);
5.2 进阶:组合 WHERE 子句构建功能更强的搜索条件
AND 操作符和 OR 操作符
- AND -> WHERE 子句中的关键字,用于指示检索满足所有给定条件的行。
- OR -> WHERE 子句中的关键字,用于检索匹配任一给定条件的行。通常,在 OR 子句的第一个条件满足时,将不再计算第二个条件。
- 任何时候使用具有 AND 和 OR 操作符的 WHERE 子句,都应该使用圆括号明确地分组操作符,不能过分依赖默认计算次序。
SQL 中,AND 操作符的优先级高于 OR 操作符,如 WHERE 子句:WHERE vend_id = 'DLL01' OR vend_id = 'BRS01' AND prod_price >=10 表示:由供应商 BRS01 制造的任何价格为 10 美元(含)以上的产品,或者由供应商 DLL01 制造的任何产品,无论价格如何。
圆括号具有比 AND 或 OR 操作符更高的优先级,如 WHERE 子句:WHERE (vend_id = 'DLL01' OR vend_id = 'BRS01') AND prod_price >=10 表示:由供应商 DLL01 或 BRS01 制造的且价格在 10 美元(含)以上的产品。
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 = 'BSR01';
SELECT prod_name, prod_price
FROM Products
WHERE (vend_id = 'DLL01' OR vend_id = 'BSR01') AND prod_price >= 10;
5.3 进阶:在 WHERE 子句中使用通配符进行过滤
LIKE 操作符
LIKE 指示 DBMS,后跟搜索模式,利用通配符匹配(模糊匹配字符串),筛选符合特定模式的行。搜索模式是由字面值、通配符或两者组合构成的搜索条件。
通配符(% _ [])
通配符是用于在LIKE操作符中匹配模式的特殊字符。通配符搜索只能用于文本字段,非文本字段不能用。
- 百分号 % 通配符:表示任何字符出现任意次数,能匹配 0 格、1 个或多个字符。 如 'Fish%' 表示以 Fish 开头的任意字符,Fish 之后不管有多少字符都可以。
- 下划线 _ 通配符:匹配1个字符(不能多也不能少,1个)
- 方括号 [] 通配符:匹配方括号中任意一个字符,在方括号中指定字符集。如 '[JM]%' 表示以 J 或 M 开头的任意字符;可以用前缀字符 ^ 表示否定,如 '[^JM]%' 表示不以 J 或 M 开头的任意字符。
SELECT prod_id, prod_name
FROM Products
WHERE prod_name LIKE 'Fish%';
SELECT prod_id, prod_name
FROM Products
WHERE prod_name LIKE '__ inch teddy bear';
SELECT cust_contact
FROM Customers
WHERE cust_contact LIKE '[JM]%';
5.4 进阶:EXISTS\NOT EXISTS 操作符
EXISTS 用于检查一个子查询是否返回任何行。如果子查询返回至少一行,则 EXISTS 返回TRUE,否则返回 FALSE。
EXISTS 只关心子查询是否返回了结果,不关心实际返回的数据是什么。因此,子查询可以返回任何类型的数据,只要它至少返回一行(即便是 NULL),EXISTS 就会返回 TRUE。
The EXISTS operator is used to test for the existence of any record in a subquery. The EXISTS operator returns TRUE if the subquery returns one or more records.
WHERE EXISTS(SELECT...) 子句会针对表中的每一行进行判断,判断的条件是 SELECT 子查询的结果是否存在。WHERE EXISTS 子句常用于检查一个表中是否存在符合某个条件的行,以便在查询结果中选择相应的行。
SELECT column1 FROM t1 WHERE EXISTS (SELECT * FROM t2);
子查询可以以 SELECT * 开头,也可以以 SELECT 1 或 SELECT column1 或其他内容开头;在这样的子查询中, SELECT 列表不影响 EXISTS,所以以上几种写法没有任何区别。
SELECT SupplierName
FROM Suppliers
WHERE EXISTS (SELECT ProductName FROM Products
WHERE Products.SupplierID = Suppliers.supplierID
AND Price < 20);
-- 哪种门店在一个或多个城市经营
SELECT DISTINCT store_type FROM stores
WHERE NOT EXISTS (SELECT * FROM cities_stores
WHERE cities_stores.store_type = stores.store_type);
下面的例子使用了双重嵌套的 NOT EXISTS(double-nested NOT EXISTS),从 stores 表中检索 DISTINCT store_type,这些 store_type 位于所有 cities 中,也就是说,如果一个 city 没有任何一家该 store_type,那么这种 store_type 就不会出现在结果中。简单来说,嵌套的 NOT EXISTS 回答了:“是否对所有 y,x 都为真?” 的问题。
-- cities 记录所有城市信息
-- cities_stores 记录门店所在城市
-- stores 记录门店类型
SELECT DISTINCT store_type FROM stores
WHERE NOT EXISTS ( -- 仅保留没有'没有该store_type的city'的store_type
SELECT * FROM cities WHERE NOT EXISTS ( -- 检索没有该store_type的city
SELECT * FROM cities_stores
WHERE cities_stores.city = cities.city
AND cities_stores.store_type = stores.store_type));
6. 创建计算字段
计算字段应用场景:数据库中原始数据不是应用程序要的格式,需要直接从数据库中检索出转换、计算或格式化过的数据,而不是检索出数据后,在客户机应用程序中重新格式化。因为一般来说,在数据库服务器上完成这些操作,比在客户机中完成要快得多,DBMS 就是设计用来高效的完成这些操作的。
6.1 拼接字段(+ || CONAT)
可使用两个竖杠 ||(Oracle,PostgreSQL,Sybase), + 操作符(Access,SQL Server,Sybase),或 CONCAT()(MySQL)拼接两个列,|| 为首选语法。
SELECT vend_name + ' (' + vend_country + ') '
FROM Vendors
ORDER BY vend_name;
SELECT vend_name || ' (' || vend_country || ') '
FROM Vendors
ORDER BY vend_name;
6.2 设置别名(AS)
可用 AS 关键字为创建的列赋予别名。
别名最常见的用法是将多个单词的列名重命名为一个单词的名字。
SELECT vend_name + ' (' + vend_country + ') ' AS vend_title
FROM Vendors
ORDER BY vend_name;
SELECT vend_name || ' (' || vend_country || ') ' AS vend_title
FROM Vendors
ORDER BY vend_name;
6.3 执行算术计算(+-*/)
SELECT prod_id
quantity,
item_price,
quantity*item_price AS expanded_price
FROM OrderItems
WHERE order_num = 20000
6.4 CASE 表达式
语法 CASE [WHEN THEN] END CASE
CASE
WHEN search_condition THEN statement_list
[WHEN search_condition THEN statement_list] ...
[ELSE statement_list]
END CASE
在 SELECT 语句中,CASE 可以作为一个表达式,用于根据不同的条件返回不同的结果。CASE 表达式类似于其他程序语言中的 if-then-elif-then-else-end 语句,遍历条件并在满足第一个条件时返回值。因此,一旦条件为真,它将停止读取并返回结果;如果没有条件为真,则它将返回 else 子句中的值;如果没有 else 部分且没有条件为 true,返回 NULL。
SELECT OrderID, Quantity,
CASE
WHEN Quantity > 30 THEN 'The quantity is greater than 30'
WHEN Quantity = 30 THEN 'The quantity is 30'
ELSE 'The quantity is under 30'
END CASE AS QuantityText
FROM OrderDetails;
6.5 数据处理函数
6.5.1 数值处理函数
函数 | 说明 |
ABS() | 返回一个数的绝对值 |
SQRT() | 返回一个数的平方根 |
EXP() | 返回一个数的指数值 |
PI() | 返回圆周率 |
SIN() | 返回一个角度的正弦 |
COS() | 返回一个角度的余弦 |
TAN() | 返回一个角度的正切 |
6.5.2 文本处理函数
函数 | 说明 |
UPPER()(UCASE()) | 将字符串转换为大写 |
LOWER()(LCASE()) | 将字符串转换为小写 |
LTRIM() | 去掉左边的空格 |
RTRIM() | 去掉右边的空格 |
TRIM() | 去掉左右两端的空格 |
LENGTH() ( LEN() 或 DATALENGTH()) | 返回字符串的长度 |
SUBSTRING() (SUBSTR() 或 MID()) | 获取一个字符串的子字符串。 |
LEFT(string, length) string 是要提取字符的字符串,length 是要提取的字符数。 | 获取字符串左边的指定长度的子字符串。 |
RIGHT(string, length) string 是要提取字符的字符串,length 是要提取的字符数。 | 获取字符串右边的指定长度的子字符串。 |
SOUNDEX() | 返回字符串的 SOUNDEX 值 SOUNDEX 是一个将文本串转换为描述其发音的字母数字模式算法,它能够用来比较字符串的发音而不是比较字母,多数 DBMS 都支持 SOUNDEX。 具体来说,如:WHERE SOUNDEX(cust_contact) = SOUNDEX(‘Michael Green’) 表示搜素发音类似于 Michael Green 的联系名,如果联系名字段中存在 Michelle Green,返回结果中显然将会有这 Michelle Green 一项(二者发音接近)。 |
有些数据库保存文本值时会将其填充为列宽,在 5.1 的例子中,可以使用 RTRIM 函数去掉多余的空格。
SELECT RTRIM(vend_name) + ' (' + RTRIM(vend_country) + ') '
FROM Vendors
ORDER BY vend_name;
SELECT RTRIM(vend_name) || ' (' || RTRIM(vend_country) || ') '
FROM Vendors
ORDER BY vend_name;
6.5.3 日期和时间处理函数
数据库中,日期和时间值将以特殊格式存储,以便快速和有效地进行排序或过滤,同时节省物理存储时间。日期和时间函数在 SQL 中具有重要作用,但是它们可移植性最差,DBMS 间差异很大,因此具体 DBMS 支持的日期时间处理函数需要参考相关文档。
示例:从 Orders 表中检索 2004 年的所有订单。
-- SQL Server, Sybase
SELECT order_num
FROM Orders
WHERE DATEPART(yy, order_date) = 2004;
-- Access
SELECT order_num
FROM Orders
WHERE DATEPART('yyyy', order_date) = 2004;
--PostgreSQL
SELECT order_num
FROM Orders
WHERE DATE_PART('year', order_date) = 2004
-- MySQL
SELECT order_num
FROM Orders
WHERE YEAR(order_date) = 2004
-- Oracle
SELECT order_num
FROM Orders
WHERE to_number(to_char(order_data, 'YY')) = 2004
SELECT order_num
FROM Orders
WHERE order_date BETWEEN to_date('01-JAN-2004') AND to_date('31-DEC-2004')
MYSQL | SQL SERVER | ORACLE | |
返回当前日期和时间 | NOW() | GETDATE() | SYSDATE |
提取日期的特定部分, 如年、月、日、小时等。 | DATE():提取日期部分 TIME():提取时间部分 YEAR():提取年份部分 MONTH():提取月份部分 DAY():提取日份部分 HOUR():提取小时部分 MINUTE():提取分钟部分 SECOND():提取秒钟部分 | DATEPART() | |
将日期格式化为指定的格式 | DATE_FORMAT() | CONVERT() | TO_CHAR() |
将字符串转换为日期格式 | STR_TO_DATE | CONVERT() | TO_DATE() |
7. 聚集函数 AVG\MAX\MIN\SUM\COUNT:汇总数据
7.1 聚集函数
聚集函数:运行在行组上,计算和返回单个值的函数。
应用场景:需要汇总数据而不用把它们实际检索出来。如:确定表中满足某个特定条件或包含某个特定值的行数、获得表中行组的值、找出表列的最大、最小、平均值。
- AVG() -> 返回某列的平均值。AVG 只能用来确定数值列的平均值,列名必须作为参数给出;如果想获得多个列的平均值,必须使用多个 AVG 函数。
- COUNT() -> 返回某列的行数。
- MAX() -> 返回某列的最大值。许多(非所有)DBMS 允许将其用于返回任意列中的最大值(包括文本列),应用于文本列时,返回按此列排序的最后一行。
- MIN() -> 返回某列的最小值。许多(非所有)DBMS 允许将其用于返回任意列中的最小值(包括文本列),应用于文本列时,返回按此列排序的第一行。
- SUM() -> 返回某列的值之和。
- AVG\MAX\MIN\SUM 均要求指定列名,切自动忽略 NULL行;COUNT(*) 表示对表中行的数目进行计数,统计 NULL 行;COUNT(column) 表示对某列中具有值的行进行计数,忽略 NULL 行。
- 利用标准的算术操作符,所有的聚集函数都可以用来执行多个列上的计算。
SELECT * FROM Products
WHERE price > (SELECT AVG(price) FROM Products);
SELECT SUM(Price * Quantity)
FROM OrderDetails;
7.2 SELECT DISTINCT
SQL DISTINCT 关键字用于从表中选择唯一的不同值。例如,有一个表 UserInfo 包含以下数据:
| Name | Address |
|-------|----------|
| 张三 | 北京市 |
| 李四 | 上海市 |
| 王五 | 北京市 |
| 李四 | 广州市 |
| 张三 | 北京市 |
SELECT DISTINCT Address FROM UserInfo;
将返回以下结果:
| 地址 |
|----------|
| 北京市 |
| 上海市 |
| 广州市 |
7.3 聚集函数 + DISTINCT 参数
AVG DISTINCT
SELECT AVG(prod_price) AS avg_price
FROM Products
WHERE vend_id = 'DLL01';
--输出
--avg_price
---------------
--6.82333333
SELECT AVG(DISTINCT prod_price) AS avg_price
FROM Products
WHERE vend_id = 'DLL01';
--输出
--avg_price
---------------
--4.2400
COUNT DISTINCT
在 COUNT() 函数中使用 DISTINCT 关键字,可以返回指定列中有多少种不同值。
SELECT COUNT(DISTINCT Country) FROM Customers;
7.4 组合聚集函数
SELECT 语句可根据需要,包含多个聚集函数。
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
8. GROUP BY 子句和 HAVING 子句:分组数据
8.1 GROUP BY
GROUP BY 语句按一列或多列对数据进行分组,将具有相同值的行分组为汇总行。GROUP BY 常与聚合函数 COUNT()、MAX()、MIN()、SUM()、AVG() 一起使用,GROUP BY 指示 DBMS 分组数据,然后对每个组而不是整个数据集进行聚集。
SELECT Country, COUNT(CustomerID)
FROM Customers
GROUP BY Country
ORDER BY COUNT(CustomerID) DESC;
--输出结果
--Country Expr1001
---------------
--USA 13
--France 11
--Germany 11
--Brazil 9
--UK 7
--Mexico 5
--Spain 5
--Venezuela 4
- GROUP BY 子句可以包含任意数目的列。含有多个列时,即对分组进行嵌套,能为数据分组提供更细致的控制,数据将在最后规定的分组上进行汇总。
- 如果分组列种含有 NULL 值,NULL 将作为一个分组返回,如果列中有多行 NULL,它们将分为一组。
- 除聚集计算语句外,SELECT 语句种的每个列都必须在 GROUP BY 子句中给出;GROUP BY 子句中的每个列,都必须时检索列或非聚集函数的有效表达式,如果在 SELECT 中使用表达式,GROUP BY 子句中应使用相同的表达式,不能使用别名。
- GROUP BY 子句必须在 WHERE 语句之后,ORDER BY 语句之前,一般在使用 GROUP BY 子句时,也会给出 ORDER BY 子句,从而保证数据正确排序。
8.2 HAVING
- HAVING 类似于 WHERE,不过 HAVING 过滤分组,WHERE 过滤行。HAVING 基于完整的分组,而不是行进行过滤。
- WHERE 在数据分组前进行过滤,HAVING 在数据分组后进行顾虑,WHERE 排除的行不会出现在分组中。
- HAVING 支持所有的 WHERE 操作符。
示例:列出了每个国家的客户数量,从高到低排序(仅考虑客户超过5个的国家)。
SELECT COUNT(CustomerID), Country
FROM Customers
GROUP BY Country
HAVING COUNT(CustomerID) > 5
ORDER BY COUNT(CustomerID) DESC;
--Expr1000 Country
---------------
--13 USA
--11 Germany
--11 France
--9 Brazil
--7 UK
示例:统计每个供应商提供的价格大于 4 的产品的产品数,返回至少提供 2 个产品的供应商及它蹄冻的产品数,输出结果按产品数排序。
SELECT vend_id, COUNT(*) AS num_prods
FROM Products
WHERE prod_price >= 4
GROUP BY vend_id
HAVING COUNT(*) >= 2
ORDER BY COUNT(*)
9. 子查询
- 子查询:嵌套在其他查询中的查询。
- SELECT 语句中,子查询总是从内向外处理。作为子查询的 SELECT 语句只能查询单个列,企图检索多个列会报错。
- 包含子查询的 SQL 代码较难阅读和调试,书写时最好把子查询分解为多行,并适当进行缩进。
- 对于能嵌套的子查询数目没有限制,但实际使用时,由于性能限制,不建议嵌套太多的子查询。
假设有三张表:Orders 表含有订单编号 order_num、客户ID cust_id(不储存客户信息,仅储存客户 ID)、订单日期 order_date;OrdersItems 表含有订单中的物品 prod_id;Customers 表记录客户信息 cust_name。
9.1 利用子查询过滤
问题1. 列出订购物品 RGAN01 的所有客户。应该怎么做呢?很容易想到可以分成三步完成统计:1. 检索含有物品 RGAN01 的订单编号;2. 检索上述订单编号对应的客户 ID;3. 检索上一步得到的客户 ID 对应的客户信息。
SELECT DISTINCT cust_name
FROM Customers
WHERE cust_id IN (
SELECT DISTINCT cust_id
FROM Orders
WHERE order_num IN (
SELECT DISTINCT order_num
FROM OrderItems
WHERE prod_id = 'RGAN01'
)
);
看过 <9. 联结表> 后,可以采用更高效的代码完成此查询操作。下述代码中,有 3 个 WHERE 子句条件,前两个关联联结中的表,最后一个过滤产品名。
SELECT cust_name
FROM Customers, Orders, OrderItems
WHERE Orders.order_num = OrdersItems.order_num
AND Orders.cust_id = Customers.cust_id
AND OrderItems.prod_id = 'RGAN01';
9.2 子查询作为计算字段
问题2. 显示 Customers 表中每个客户的订单总数。可以用两步完成统计:1. 从 Customers 表中检索客户列表;2. 对于检索出的每个客户,在 Orders 表中统计其订单总数。
SELECT cust_name, (
SELECT COUNT(*) FROM Orders
WHERE Orders.cust_id = Customers.cust_id
) AS orders
FROM Customers
ORDER BY cust_name;
10. 联结表(WHERE 和 JOIN 子句)
10.1 关系表和联结
10.1.1 关系表
关系表:把信息分解成多个表,一类数据一个表,各表通过某些常用值(关系 relational)互相关联。
以一个现实生活中的场景为例:假设有一个关于产品信息的数据库,每种产品占一行,每行除了记录产品的功能、价格等信息,还记录了生产该产品的供应商信息。同一个供应商有可能生产多种产品,如果把产品信息和供应商信息记录在同一个表中,会存在如下问题:1. 对于同一个供应商生产的每个产品,供应商信息都是相同的,重复记录此信息浪费时间和存储空间;2. 如果供应商信息发生变化,需要修改很多条目;3. 由于存在重复数据,很难保证在每次输入时,输入的内容是完全一致的,不一致的数据后续很难处理。
因此,考虑创建两个表,Vendors 表存储供应商信息,每个供应商占一行,每个供应商具有唯一的表示标识(供应商 ID),称为主键;Products 表存储产品信息,它存储供应商 ID,不存储其他供应商信息。Vendors 表的主键(供应商 ID)将两个表关联起来。这样做,1. 供应商信息不重复,节省时间和空间;2. 如果供应商信息变动,只需要修改 Vendors 表中的单个记录;3. 数据无重复,数据是一致的。
10.1.2 联结
分解数据具有高效存储,处理方便的优点,但数据存储在多个表中,检索时怎么办呢?答案是使用联结。
联结:在一条 SELECT 语句中,关联多个表,返回一组输出。联结是在运行时进行的,它存在于查询的执行当中,不存在物理实体。
10.2 建立联结关系(内部联结或等值联结)
创建联结的基本规则:列出所有的表,用 WHERE 子句定义表之间的关系。
10.2.1 使用 WHERE 子句联结两个表
SELECT vend_name, prod_name, prod_price
From Vendors, Products
WHERE Vendors.vend_id = Products.vend_id;
FROM 子句列出两个表,它们即为 SELECT 语句联结的两个表的名字,这两个表用 WHERE 子句正确联结。最终 SELECT 语句返回了来自两个不同的表的数据。
注:联结两个表,实际上就是将第一个表的每一行与第二个表的每一行配对,WHERE 子句作为过滤条件,起到只保留能匹配给定条件的行的作用。如果没有 WHERE 子句,第一个表中的每个行都会与第二个表中的每个行配对,将返回笛卡尔积,即检索出的行数是两个表行数的乘积。
所有的联结,都应该有 WHERE 子句。数据库中不存在能指示 DBMS 如何对表进行联结的信息,需要你自己做这件事,否则将返回比预想多得多的数据。
10.2.2 使用 WHERE 子句联结多个表
SQL 对 SELECT 语句可以联结的表的数目没有限制。
示例:显示订单编号为 20007 的订单中的物品的信息。
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;
10.2.3 INNER JOIN 语法(首选 INNER JOIN)
下述代码返回结果同 9.2,区别在于两个表之间的关系是 FROM 子句的组成部分,用 INNER JOIN 指定。
SELECT vend_name, prod_name, prod_price
FROM Vendors INNER JOIN Products
ON Vendors.vend_id = Products.vend_id;
10.3 设置表别名
AS 关键字除了可以用于给列取别名,还可以用于给表取别名。表别名可以用于 SELECT 的列表、WHERE 子句、ORDER BY 子句,以及语句的其他部分。
示例:检索购买了产品 RGAN01 的用户。
SELECT cust_name, cust_contact
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 语句
- 允许在单条 SELECT 语句中多次使用相同的表(自关联,参考 9.5.1)
10.4 高级联结
10.4.1 自联结
通常来说,在单条 SELECT 语句中,一个表只能引用一次;如果想多次引用同一个表,需要在每次引用时,对其设置不同的别名。
示例:客户信息表 Customers 记录了客户id cust_id,客户名 cust_name,客户所在公司名cust_comp_name,要求检索与 Jim 同公司的所有客户。
最基础的想法是:先查询 Jim 所在公司的名字,然后查询这个公司名下的所有客户名。可以使用子查询完成检索:
SELECT cust_id, cust_name
FROM Customers
WHERE cust_comp_name = (
SELECT cust_comp_name
FROM Customers
WHERE cust_name = 'Jim'
)
DBMS 处理联结远比处理子查询快得多,如果使用联结,代码如下:
SELECT cust_id, cust_name
FROM Customers AS C1, Customers AS C2
WHERE C1.cust_comp_name = C2.cust_comp_name
C2.cust_name = 'Jim'
10.4.2 自然联结
使用联结时,至少有一个列(联结列)会出现在多个表中。标准的联结返回所有数据,有可能相同的列出现多次;自然联结要求每个列只出现一次。自然联结的实现方式:对一个表使用通配符 SELECT *,对其他列明确列出列名,从而保证没有重复的列被检索出来。
10.4.3 外部联结 LEFT\RIGHT\FULL OUTER JOIN
内部联结将一个表中的行与另一个表中的行相关联,但有时候需要包含没有关联的行,比如:检索每个客户的订单数(包括未下单的客户)、检索所有产品的订购数(包括没有人订购的产品),这种包含没有关联行的行的联结行为,称为外部联结。
外部联结包含:左外部联结(LEFT OUTER JOIN,*=操作符,包含 OUTER JOIN 左边的表的所有行)、右外部联结(RIGHTT OUTER JOIN,=* 操作符,包含 OUTER JOIN 右边的表的所有行)、全外部联结(FULL OUTER JOIN,包含来自两个表的不关联的行)。
-- 检索所有客户及其订单编号
SELECT Customers.cust_id, Orders.order_num
FROM Customers OUTER JOIN Orders
WHERE Customers.cust_id = Orders.cust_id;
-- 检索所有客户及其订单编号,包括登记了客户信息但没有创建订单的客户
SELECT Customers.cust_id, Orders.order_num
FROM Customers LEFT OUTER JOIN Orders
WHERE Customers.cust_id = Orders.cust_id;
-- 检索所有客户及其订单编号,包括创建了订单但没有登记客户信息的客户
SELECT Customers.cust_id, Orders.order_num
FROM Customers RIGHT OUTER JOIN Orders
WHERE Customers.cust_id = Orders.cust_id;
-- 检索所有客户及其订单编号,包括所有创建了订单但的客户,以及所有登记了客户信息的客户
SELECT Customers.cust_id, Orders.order_num
FROM Customers FULL OUTER JOIN Orders
WHERE Customers.cust_id = Orders.cust_id;
*= 为左外部联结操作符,表示返回操作符左边表所有的行;=* 为右外部联结操作符,表示返回操作符右边表的所有行。
SELECT Customers.cust_id, Orders.order_num
FROM Customers, Orders
WHERE Customers.cust_id *= Orders.cust_id
SELECT Customers.cust_id, Orders.order_num
FROM Customers, Orders
WHERE Customers.cust_id =* Orders.cust_id
10.5 带聚集函数的联结
示例:检索所有客户以及每个客户的订单数。
SELECT Customers.cust_id, COUNT(Orders.order_num) AS num_ord
FROM Customers INNER JOIN Orders
GROUP BY Customers.cust_id;
11. UNION 子句:组合查询
UNION 使用方法:在各语句之间放上关键字 UNION 即可。概括来说,有以下两种情况需要使用组合查询:1. 对单个表执行多个查询,每个查询都返回类似的数据结构,最后按单个查询返回结果;2. 在单个查询中,从不同表返回类似的数据结构。
示例:Customers 表中有以下字段:客户名 cust_name,客户所在公司名 cust_comp_name,客户所在地区 cust_state。要求列出位于 Indiana,Michigan 和 Illionis 的客户,以及在 Fun4ALL 公司工作的客户。
使用 UNION:
SELECT cust_name, cust_state
FROM Customers
WHERE cust_state in ('IL', 'IN', 'MI')
UNION
SELECT cust_name, cust_state
FROM Customers
WHERE cust_comp_name = 'Fun4ALL';
使用 WHERE:
SELECT cust_name, cust_state
FROM Customers
WHERE cust_state in ('IL', 'IN', 'MI')
OR cust_comp_name = 'Fun4ALL';
UNION 使用规则总结
- UNION 必须由两条或两条以上的 SELECT 语句组成,语句之间用关键字 UNION 分隔。比如,组合 4 条 SELECT 语句,需要使用 3 个 UNION 关键字。
- UNION 中的每个查询必须包含相同的列、表达式或聚集函数;列数据的类型必须兼容(可以不必完全相同,但必须是 DBMS 可以隐式转换的类型);列的次序可以不完全相同。
- UNION 会从查询结果集中自动去除重复行(效果同单条 SELECT 语句中使用多个 WHERE 子句一样),如果想保留重复的行,可以使用 UNION ALL(从这个角度来说,UNION ALL 可以完成 WHERE 子句做不到的工作)。
- 多数情况下,UNION 相同表的多个查询与具有多个 WHERE 子句的单条查询,可以完成相同的工作。多数 DBMS 都有内部查询优化程序,能在处理各条 SELECT 语句前对它们进行组合,因此从性能角度来看,使用 UNION 与使用多个 WHERE 子句应该没有实际的差别,不过还是建议在实践中测试两种方法,看那种更好。
- 排序:使用 UNION 组合查询时,只能使用一条 ORDER BY 子句,需要把它放在最后一条 SELECT 语句之后,它看似是最后一条 SELECT 语句的组成部分,但实际上 DBMS 将用它来对所有 SELECT 语句返回的结果排序。
对组合排序结果排序:
SELECT cust_name, cust_state
FROM Customers
WHERE cust_state in ('IL', 'IN', 'MI')
UNION
SELECT cust_name, cust_state
FROM Customers
WHERE cust_comp_name = 'Fun4ALL'
ORDER BY cust_name;
12. 创建、更新和删除表
12.1 创建表 CREATE TABEL
一般有两种创建表的方式:1. 多数 DBMS 都具有交互式创建和管理表的工具(本质上也是使用 SQL 语句,只不过这些语句不是用户输入的,而是界面工具自动生成并执行的);2. 可以直接用 SQL 语句创建和操纵表。各 DBMS 的 CREATE TABLE 语句可能有所差异,此处只提供基础的用法,更详细的内容需要参阅具体的 DBMS 文档。
- 使用 GREATE TABLE 创建表
- 新表的名字在关键字 GREATE TABEL 之后给出
- 表定义的所有列括在圆括号中,表中每列的名字(以及参数,如数据类型,是否允许缺失,默认值)用逗号分隔。
示例(NOT NULL 和 DEFAULT 为约束 Contraint,可参考 12.5)
CREATE TABLE OrderItems
(
order_num INTEGER NOT NULL,
order_item INTEGER NOT NULL,
prod_id CHAR(10) NOT NULL,
quantity INTEGER NOT NULL DEFAULT 1,
item_price DECIMAL(8,2) NOT NULL,
item_desc VARCHAR(1000) NULL
);
12.2 更新表 ALTER TABEL(增加列\删除列\修改列\重命名列\添加约束\索引)
一般来说,在表中有数据时,最好不要对其进行更新,应该在表的设计过程中充分考虑未来可能的需求,以便今后不会对表结构做大的改动。对表的结构能进行何种更改,以及如何更改,具有 DBMS 差异性,需要参阅具体的 DBMS 文档。
务必谨慎使用 ALTER TABLE,数据库表的更改不能撤销,如果增加了不需要的列,可能无法删除它们;如果删除了不应该删除的列,也无法恢复,会完全丢失该列中所有的数据。
- 所有 DBMS 都支持给现有的表增加列,不过可能对增加列的数据类型,以及 NULL 和 DEFAULT 的使用有所限制;
- 多数 DBMS 允许对表中的列重命名;
- 许多 DBMS 不允许删除或更改表中的列;有些 DBMS 对已经填有数据的列的更改有限制,对未填有数据的列几乎没有限制。
示例:复杂的表结构修改过程
使用新的布局创建新表;
使用 INSERT SELECT 语句从旧表复制数据到新表;
检验包含所需数据的新表;
重命名旧表(或删除),用旧表的名字重命名新表;
根据需要,重新创建触发器、存储过程、索引和外键。
12.3 约束
约束 -> 管理如何插入或处理数据库数据的规则。
约束通常是在 CREATE TABLE 或 ALTER TABLE 中定义的。不同 DBMS 支持约束的方式可能有差异,实践前请参考对应文档。
Constraints can be specified when the table is created with the CREATE TABLE statement, or after the table is created with the ALTER TABLE statement.
是否允许缺失值 NULL\NOT NULL
创建表时,关键字 NULL\NOT NULL 用于规定列中是否允许有缺失值。
- NULL 列表示允许缺失值,即在插入行时,可以不给出该列的值,不指定时默认为 NULL;
- NOT NULL 列表示在插入或更新行时,该列必须有值,否则会报错。
默认值 DEFAULT
创建表时,关键字 DEFAULT 用于规定默认值,在插入行时,如果不提供此列的值,DBMS 将自动采用默认值。默认值经常用于日期或时间戳列。
主键 PRIMARY KEY 和外键 FOREIGN KEY
CREATE TABLE Orders
(
order_num INTEGER NOT NULL PRIMARY KEY,
order_data DATETIME NOT NULL,
cust_id CHAR(10) NOT NULL REFERENCES Customers(cust_id)
);
ALTER TABLE Vendors
ADD CONSTRAINT PRIMARY KEY (vend_id)
ALTER TABLE Customers
ADD CONSTRAINS FOREIGN KEY (cust_id) REFERENCES Customers (cust_id)
唯一约束 UNIQUE
唯一约束用来保证一个列(或一组列)中的数据是唯一的。比如,雇员表 Employees 中,每个雇员都有唯一的身份证号,由于它太长了,并不打算把它作为主键,但可以对其设置唯一约束,从而确保不会因为输入错误,出现重复的身份证号。
这类似于主键,但有几个重要区别:
- 表可以包含多个唯一约束,但每个表只能有一个主键;
- 唯一约束列可以包含 NULL 值;
- 唯一约束列的值可以修改和更新;
- 唯一约束列的值可以重复使用。
检查约束 CHECK
检查约束用来保证一个列(或一组列)中的数据满足一组指定的条件。检查约束的常见用途为:
- 检查最小值和最大值。例如:防止出现 0 个物品的订单。
- 指定范围,如:保证发货日期大于今天的日期,且不大于从今年开始的一年的日期。
- 只允许特定的值,如:性别字段只允许为 'M' 和 'F'
CREATE TABLE OrderItems
(
order_num INTEGER NOT NULL,
order_item INTEGER NOT NULL,
prod_id CHAR(10) NOT NULL,
quantity INTEGER NOT NULL CHECK(quantity > 0)
item_price MONEY NOT NULL
)
ALTER TABLE OrderItems
ADD CONSTRAINT CHECK (gender LIKE '[MF]')
12.4 删除表 DROP TABLE
删除表使用 DROP TABLE 语句。务必慎重使用此语句,删除表没有确认,也不能撤销,执行这条语句将永久删除此表
DROP TABLE COPYTABLE
许多 DBMS 允许强制实施 “防止删除与其他表关联的表” 的规则,这样可以防止意外删除有用的表。比如,对某个表发布 DROP TABLE 语句,而这个表是某个关系的组成部分,则 DBMS 将阻塞这条语句,直到该关系被删除。
12.5 重命名表 RENAME
表的重命名操作存在 DBMS 实现差异,比如 MySql 和 Oracle 使用 RENAME 语句;SQL Server 使用 sp_rename 存储过程,实践时,请参阅具体的 DBMS 文档。
13. 插入行、更新行和删除行
13.1 插入行数据
13.1.1 插入完整行 INSERT INTO
某些 DBMS 可以省略 INTO 关键字,不过为了增强 SQL 代码在 DBMS 之间的可移植性,建议提供这个关键字。
方式一 (不推荐)仅指定表名和插入到新行的值
缺失值需要使用 NULL 值(假定表中该列允许为空值)
INSERT INTO Customers
VALUES(
'1000000006',
'Toy Land',
'123 Any Street',
'New York',
'NY',
'11111',
NULL,
NULL
);
方式二(推荐)明确给出列的列表(表结构发生变化,该语句也能正常工作)
INSERT INTO Customers(
cust_id,
cust_name,
cust_address,
cust_city,
cust_state,
cust_zip
cust_contact,
cust_email
)
VALUES(
'1000000006',
'Toy Land',
'123 Any Street',
'New York',
'NY',
'11111'
NULL,
NULL,
);
如果提供了列名列表,数值次序按照列名列表顺序即可,可以不同于表格中列的顺序,依然能正确插入数据。
INSERT INTO Customers(
cust_id,
cust_contact,
cust_name,
cust_address,
cust_city,
cust_state,
cust_zip
)
VALUES(
'1000000006',
NULL,
NULL,
'Toy Land',
'123 Any Street',
'New York',
'NY',
'11111'
);
插入多行
INSERT INTO Customers (CustomerName, ContactName, Address, City, PostalCode, Country)
VALUES
('Cardinal', 'Tom B. Erichsen', 'Skagen 21', 'Stavanger', '4006', 'Norway'),
('Greasy Burger', 'Per Olsen', 'Gateveien 15', 'Sandnes', '4306', 'Norway'),
('Tasty Tee', 'Finn Egan', 'Streetroad 19B', 'Liverpool', 'L1 0AA', 'UK');
方式二(应用)插入仅含有部分列行
使用明确列的列表的 INSERT 语句还可以达到省略某些列的效果:只给部分列提供值,其他列省略,不提供值。省略的列必须满足以下两个条件之一,如果表中某列不运行 NULL 值,且没有默认值,插入行时省略此列将产生错误消息,并且相应的行插入失败。
- 该列允许 NULL 值;
- 该列定义了默认值。
INSERT INTO Customers(
cust_id,
cust_name,
cust_address,
cust_city,
cust_state,
cust_zip
)
VALUES(
'1000000006',
'Toy Land',
'123 Any Street',
'New York',
'NY',
'11111'
);
13.1.2 插入检索出的数据 INSERT INTO SELECT
INSERT INTO SELECT:由一条 INSERT INTO 语句和一条 SELECT 语句组成,将 SELECT 语句的结果插入到表中。
INSERT INTO Customers(
cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_contact, cust_email
)
SELECT (
cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_contact, cust_email
)
FROM Old_Customers;
- INSERT INTO SELECT 中,SELECT 出的新行,主键值不能与原表中内容重复,否则会操作失败;
- INSERT INTO SELECT 不关心 SELECT 返回的列名,它使用的是列的位置,SELECT 的第一列(无论列名是什么)将用来填充 INSERT 指定的列的列表中的第一列;
- INSERT INTO SELECT 中的 SELECT 语句可以包含 WHERE 子句过滤插入的数据;如果没有检索出行,即插入 0 行,也不会报错;
- INSERT INTO 通常只插入一行,如果想插入多行,必须执行多个 INSERT INTO 语句。但 INSERT INTO SELECT 是个例外,它可以用单条 INSERT INTO 语句插入多行,不管 SELECT 语句返回多少行,都将被 INSERT INTO 插入。
13.1.3 从一个表复制到另一个表 SELECT INTO
SELECT INTO:用于复制数据到一个新表,它将创建并填充,与查询结果中每个列相同的列;
- 可以使用任何 SELCET 子句,如 WHERE 和 GROUP BY 等;可使用联结从多个表复制数据,但无论从多少个表中取数据,都只能插入到单个表中。
- SELECT INTO 可以创建新表,INSERT SELECT 只能向已存在的表中插入数据;SELECT INTO 只能执行一次,INSERT SELECT 可以执行多次。
SELECT *
INTO Old_Customers
FROM Customers
13.2 更新数据 UPDATE 和 删除数据 DELETE FROM
接下来将介绍更新语句 UPDATE 和删除语句 DELETE,需要注意的是,使用这两个语句时都应带有 WHERE 子句,否者它们将被应用到表中所有行,即 UPDATE 将更新表中所有行,DELETE 将删除表中所有行。
13.2.1 更新某些行的某些列的值 UPDATE
- UPDATE 语句由三部分组成:要更新的表名,要更新的列名即新的值,用于确定更新哪些行的过滤条件;
- 更新多个列时,使用单个 SET 命令,每个“列=值”对之间用逗号分隔(最后一列的后面不需要使用逗号);
- 如果想删除某列的值,可将其设置为 NULL(假如表定义该列时指定其允许 NULL )。
UPDATE Customers
SET cust_contact = 'Sam'
cust_email = '[email protected]'
WHERE cust_id = '1000000006';
UPDATE Customers
SET cust_email = NULL
WHERE cust_id = '1000000006';
13.2.2 删除行 DELETE FROM
- 在某些 DBMS 中,DELETE 后面的关键字 FROM 是可选的,不过为了增强代码可移植性,建议保持提供这个关键字。
- DELETE 语句删除表中行,但是不会删除表本身。
DELETE FROM Customers
WHERE cust_id = '1000000006'
14. 视图
所有 DBMS 非常一致地支持视图创建语法。
14.1 什么是视图
如前文所述,可以用如下代码查询订购了某个产品的所有客户。如果想检索其他产品相同的数据,修改最后的 WHERE 子句。
SELECT cust_name, cust_contact
FROM Customers, Orders, OrderItems
WHERE Customers.cust_id = Orders.cust_id
AND OrderItems.order_num = Order.order_num
AND prod_id = 'RGAN01';
可以把整个查询包装成一个名为 ProductCustomers 的虚拟表,这样,对于不了解表结构的人,他不需要知道如何联结表,也能够检索到需要的数据。
SELECT cust_name, cust_contact
FROM ProductCustomers
WHERE prod_id = 'RGAN01';
什么是视图:
在SQL中,视图是一个虚拟的表,它是基于一个或多个表的查询结果集。视图与物理表类似,它也有列和行,但是它并不存储数据,而是根据定义的查询每次动态生成结果。视图可以被视为一个虚拟的表,可以像常规表一样使用,例如查询、插入、更新和删除数据。视图可以隐藏复杂的查询逻辑,简化数据访问,并提高查询性能。视图还可以提供安全性,限制用户只能访问需要的数据。
应用视图的优点:
- 重用 SQL 语句,简化复杂的 SQL 操作。通过使用视图,用户可以很方便地重用 SQL 语句,不必知道查询细节,不必考虑底层数据表的结构和关系;
- 使用表的组成部分而不是整个表;
- 更改数据格式和表示。视图可以返回与底层表的表示和格式不同的数据;
- 保护数据。
视图的使用规则:
- 与表一样,视图必须唯一命名,不能给视图取与别的表或者视图相同的名字。
- 视图可以嵌套,可以利用从其他视图中检索数据的查询来构造一个视图。
- 对于可以创建的视图数目没有限制。
- 有的 DBMS 要求命名返回所有列,因此需要对计算字段设置别名。
- 有的 DBMS 把视图作为只读的查询:可以从视图中检索数据,但不能将数据写回到底层表。
- 有的 DBMS 可以创建有特殊要求的视图:不允许进行导致行不再属于该视图的插入或更新。比如,某个视图的内容为所有含邮件地址信息的客户,如果更新某个客户的数据,删除了他的邮件地址,该客户将不再属于视图,某些 DBMS 下的视图可以禁止这种情况发生。
在视图(虚拟表)创建后,可以用与表基本相同的方法利用它们,可以对视图执行 SELECT 操作,过滤和排序数据,将视图联结到其他视图或表。
视图本身不包含数据,它仅仅是用来查看存储在别处的数据的一种设施,每次使用视图时,都必须处理查询执行时所需的检索。如果你嵌套了视图或者视图结构很复杂,可能会发现性能下降得很厉害,因此,在部署使用大量视图的应用时,应该提前进行测试。
14.2 创建视图 CREATE VIEW,删除视图 DROP VIEW
CREATE VIEW ProductCustomers 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;
SELECT cust_name, cust_contact
FROM ProductCustomers
WHERE prod_id = 'RGAN01';
DROP VIEW ProductCustomers
如果需要覆盖(更新)视图,只能先 DROP 它,然后再重新创建它。
示例1:使用视图格式化检索出的数据
CREATE VIEW VendorLocations AS
SELECT RIRIM(vend_name) + ' (' + RTRIM(vend_country) + ')' AS vend_title
FROM Vendors;
SELECT *
FROM VendorLocations
示例2:使用视图过滤不想要的数据
CREATE VIEW CustomersEmailList AS
SELECT cust_id, cust_name, cust_email
FROM Customers
WHERE cust_email IS NOT NULL;
SELECT *
FROM CustomersEmailList;
示例3:使用视图计算字段
CREATE VIEW OrderItemsExpanded AS
SELECT order_num,
prod_id,
quantity,
item_price,
quantity*item_price AS expanded_price
FROM OrderItems;
SELECT *
FROM OrderItemsExpanded
WHERE order_num = 20000008;
15. 存储过程
不同 DBMS 中的存储过程语法有所不同,实际编写代码时需要参考对应的 DBMS 文档。一般来说,存储过程的编写比基本 SQL 语句复杂,编写存储过程需要更高的技能,因此,许多数据库管理员会限制存储过程的创建,以确保安全性。这可能包括限制存储过程的访问权限、审查存储过程的代码和确保存储过程不会被滥用或攻击。
存储过程和 View 都是数据库中的对象,它们可以访问数据库中的表和其他对象。存储过程可以执行复杂的数据操作和业务逻辑,View可以简化复杂查询的编写。
15.1 什么是存储过程
存储过程是一条或多条SQL语句集合,被存储在数据库中,可以被多次调用和重复使用。它是一个封装了一系列SQL语句和程序逻辑的代码块,可以接收参数,并返回结果集。
简单来说,存储过程就是为以后的使用而保存的一条或多条 SQL 语句的集合。存储过程可以被视为一种数据库对象,可以在数据库中创建、修改、删除和执行。它可以提高数据库的性能和安全性,减少网络通信的开销,同时也可以简化应用程序的开发和维护。
使用存储过程的优点:
- 把操作封装在容易使用的单元中,简化复杂操作。
- 由于不必反复建立一系列的处理步骤,能够防止错误,保证数据的一致性。
- 简化对变动的管理。如果表名、列名或业务逻辑等内容发生变化,只需要修改存储过程中的代码,使用它的人员无需知道这些变化。
- 存储过程通常以编译的形式存储,减少了 DBMS 处理命令时重复编译的时间和开销,从而提高了查询和操作的速度。
- 可以编写功能更强更灵活的代码。
15.2 创建存储过程 CREATE PROCEDURE
15.2.1 MySQL
MySQL:https://dev.mysql.com/doc/refman/8.4/en/create-procedure.html
语法
CREATE [DEFINER = user]
PROCEDURE [IF NOT EXISTS] sp_name ([proc_parameter[,...]])
routine_body
关键点总结
- IF NOT EXISTS 一个可选的关键字,用于防止在已经存在同名存储过程时出现错误。如果已存在同名的存储过程,则不会创建新的存储过程,而是返回一个警告信息;如果存储过程不存在,那么将会正常创建新的存储过程。
- 如果存储过程的名称与内置 SQL 函数的名称相同,除非在定义存储过程名称以及后续调用时在名称和后续括号之间使用空格(IGNORE_SPACE 适用于内置函数,但不适用于存储例程;无论是否启用忽略空格模式,存储过程的名称后面都可以有空格),否则会引发语法错误。尽量避免将存储过程命名为现有 SQL 函数的名称。
- 参数列表要用括号括起来,如果没有参数,则应该使用空参数列表 (),参数名称不区分大小写。
- proc_parameter:
[ IN | OUT | INOUT ] param_name type- 默认情况下,每个参数都是 IN 参数,若要指定参数为 OUT 或 INOUT,需要在参数名称之前使用关键字 OUT 或 INOUT。IN 参数将值传递给存储过程,在过程可能会修改该值,但当该过程返回时,该修改对调用者不可见;OUT 参数将从存储过程返一个值给调用者,在过程中,它的初始值为 NULL,当过程返回时,它的值对调用者可见;INOUT 参数由调用者初始化,可以由过程修改,过程所做的任何更改在过程返回时对调用者可见。
- 对于每个 OUT 或 INOUT 参数,都需要在调用过程的 CALL 语句中传递一个用户定义的变量,以便在过程返回时获取其值。
- 使用 CALL 语句调用存储过程。
示例
15.2.2 Oracle
语法
CREATE [OR REPLACE] [EDITIONABLE | NONEDITIONABLE]
PROCEDURE
[IF NOT EXITS] procedure_name [(parameter_name [IN | OUT | IN OUT] type [, ...])]
{IS | AS} [local_variable_declarations;]
BEGIN
executable_statements;
[EXCEPTION exception_handlers;]
END [procedure_name];
关键点总结
- OR REPLACE:如果存储过程已经存在,则替换它。
- EDITIONABLE | NONEDITIONABLE:指定创建的存储过程是否可以被修改。EDITIONABLE 表示存储过程可以被修改; NONEDITIONABLE 表示存储过程不能被修改。这个选项通常在多版本控制中使用,用于控制存储过程的版本管理。
- IF NOT EXISTS:用于在创建存储过程之前检查是否已经存在同名的存储过程。如果已经存在,则不会创建新的存储过程,而是直接返回一个错误。
- parameter_name 是存储过程中定义的参数名称,参数分为 IN 参数、OUT 参数和 IN OUT 参数:IN参数,需要在参数名称后面指定数据类型;OUT 参数或 IN OUT 参数,需要在参数名称后面指定数据类型,并在数据类型后面加上关键字 OUT 或 IN OUT
- IS 和 AS 关键字在功能上没有区别,它们是等效的。它们只是用于分隔存储过程头部和主体部分的不同语法,表示将开始存储过程的主体部分。在实际使用中,可以根据个人喜好或团队规范选择使用其中之一。
- BEGIN:开始存储过程的执行部分;END:结束存储过程的主体部分。
- 使用 EXEC 执行存储过程。
示例
示例1:从 employees 表中删除指定 employee_id
调用时,可以使用命令:EXEC remove_emp(100);
示例2:统计邮件发送清单中具有邮件地址的客户
-- Oracle 版本
CREATE PROCEDURE MailingListCount
(ListCount OUT NUMBER) -- ListCount 参数,关键字 OUT 表示从存储过程返回一个值
IS
BEGIN
SELECT * FROM Customers
WHERE NOT cust_email IS NULL;
LISTCount := SQL%ROWCOUNT;
END;
15.2.3 SQL Server
示例1:统计邮件发送清单中具有邮件地址的客户
-- SQL Server 版本
CREATE PROCEDURE MailingListCount
AS
DECLARE @cnt INTEGER -- DECLARE 语句声明一个名为 @cnt 的局部变量,SQL SERVER 中所有局部变量都以 @ 开头
SELECT @cnt = COUNT(*)
FROM Customers
WHERE NOT cust_email IS NULL;
RETURN @cnt;
示例2:在 Orders 表中插入一个新订单
-- SQL Server
CREATE PROCEDURE NewOrder @cust_id CHAR(10)
AS
DECLARE @order_num INTEGER
SELECT @order_num=MAX(order_num)
FROM Orders
SELECT @order_num=@order_num + 1
INSERT INTO Orders(order_num, order_date, cust_id)
VALUES(@order_num, GETDATE(), @cust_id)
RETURN @order_num
16. 注释
- 单行注释以 -- 开头。
- 多行注释以 /* 开始,以 */ 结束。
-- Select all:
SELECT * FROM Customers;
/*Select all the columns
of all the records
in the Customers table:*/
SELECT * FROM Customers;
17. 总结:SELECT 子句顺序
SELECT
FROM
WHERE
GROUP BY
HAVING
UNION
ORDER BY
参考文献
1. SQL 必知必会(第 3 版)Ben Forta
2. SQL Tutorial