/

《SQL必知必会(第四版)》学习笔记

本来是看第五版的,结果拿错书了,等发现时候已经看了好多了就没再换书

数据库基本概念

数据库:保存有组织的数据的容器(通常是一个文件或一组文件)。

我们常说的数据库软件是“数据库管理系统(DBMS)”,而非数据库,数据库是通过 DBMS 创建和操纵的容器,而具体它究竟是什么,形式如何,各种数据库都不一样。

:某种特定类型数据的结构化清单。

模式:关于数据库和表的布局及特性的信息。包括表具有一些特性,这些特性定义了数据在表中如何存储,包含存储什么 样的数据,数据如何分解,各部分信息如何命名等信息。

(column):表中的一个字段。所有表都是由一个或多个列组成的。

数据类型:所允许的数据的类型。每个表列都有相应的数据类型,它限制(或允许)该列中存储的数据。

(row):表中的一个记录。

主键(primary key):一列(或一组列),其值能够唯一标识表中每一行。

提示:应当把主键作为每行必须项

主键条件:

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

SQL:Structured Query Lanuage (结构化查询语言),是一种专门用来与数据库沟通的语言。

关键字:作为 SQL 组成部分的保留字。关键字不能用作表或列的名字。

SQL 语句特点

  • 多条 SQL 语句必须以分号;分隔。多数 DBMS 不需要在单条 SQL 语句后加分号,但也有 DBMS 可能必须在单条 SQL 语句后加上分号。
  • SQL 语句不区分大小写,但多数人对 SQL 关键字大写,更易于阅读和调试。
  • 在处理 SQL 语句时,其中所有空格都被忽略。SQL 语句可以写成长长的一行,也可以分写在多行。多数 SQL 开发人员认为,将 SQL 语句分成多行更容易阅读和调试。

操作符:用来联结或改变WHERE字句中的字句的关键字,也称为逻辑操作符

聚集函数(aggregate function):对某些行运行的函数,计算并返回一个值。

检索数据

检索数据使用SELECT语句

检索单列

1
SELECT name FROM students;

检索多列

传入多个列名即可,列名之间用,分隔,但最后一个列名后面不可以加。

1
SELECT name,class_id,id FROM students;

检索所有列

使用通配符*

1
SELECT * FROM students;

检索不同的值

使用DISTINCT关键字

用于指示数据库只返回不同的值,即筛选掉重复行,将会应用于所有列

1
2
SELECT DISTINCT class_id
FROM students;

限制前 n 行结果

TOP关键字限制最多返回多少行

1
2
SELECT TOP 5 name
FROM students;

对于 MySQL、MariaDB、PostgreSQL 或者 SQLite,需使用LIMIT

1
2
3
SELECT name
FROM students
LIMIT 5;

对于 DB2 DBMS 的特定语句

1
2
3
SELECT name
FROM students
FETCH FIRST 5 ROWS ONLY;

对于 Oracle DBMS

1
2
3
SELECT name
FROM students
WHERE ROWNUM <=5;

限制第 m 行开始输出 n 行结果

使用OFFSET关键字调整检索位移

1
2
3
SELECT name
FROM students
LIMIT 5 OFFSET 3;

输出从第 3 行开始的 5 行数据

行数从第0行开始计算

简化版
MySQK 和 MariaDB 支持简化版语句,例如LIMIT 4 OFFSET 3,即为LIMIT 3,4

使用注释

行尾注释

在指令后面加--即可

多行指令也可以

1
2
3
SELECT name     -- 注释1
FROM students -- 注释2
LIMIT 5; -- 注释3

行内注释

1
2
3
# 这是一个注释
SELECT name
FROM students;

多行注释

1
2
3
4
/* 这是一个
注释 */
SELECT name
FROM students;

过滤数据

使用WHERE语句过滤数据

基本用法

1
2
3
SELECT name, id, class_id
FROM students
WHERE class_id = 1;

注意:WHERE 子句的位置
在同时使用ORDER BYWHERE子句时,应当让ORDER BY位于WHERE之后,否则将会产生错误

操作符

WHERE字句支持的操作符:

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

eg

小于等于

1
2
3
SELECT name, id, class_id
FROM students
WHERE score <= 60;

等于

1
2
3
SELECT name, id, class_id
FROM students
WHERE name = "小红";

当比较字符串时应当使用引号

范围

1
2
3
SELECT name, id, class_id
FROM students
WHERE score BETWEEN 60 AND 90;

空值检查

1
2
3
SELECT name, id, class_id
FROM students
WHERE name IS NULL;

NULL
无值,它与字段包含 0、空字符串或仅仅包含空格不同
检查值是否为 NULL,不可以用= NULL判断,只能用IS NULL语句

高级数据过滤

AND操作符

用在WHERE子句中的关键字,用来指示检索满足所有给定条件的行(且)。

1
2
3
SELECT name, id, class_id, score
FROM students
WHERE class_id = 1 AND score >= 60;

OR操作符

WHERE子句中使用的关键字,用来表示检索匹配任一给定条件的行。在部分 DBMS 里会出现逻辑短路

1
2
3
SELECT name, id, class_id, score
FROM students
WHERE class_id = 1 AND name = '小红';

操作符优先级

ANDOR更先运算,在使用时可以通过圆括号对操作符进行分组,控制运算顺序

IN操作符

IN用于WHERE子句中用来指定要匹配值的清单的关键字,功能与OR相当。

1
2
3
SELECT name, id, class_id, score
FROM students
WHERE class_id IN (1, 2, 3);

IN操作符与OR类似

下面为等价代码

1
2
3
SELECT name, id, class_id, score
FROM students
WHERE class_id = 1 OR class_id = 2 OR class_id = 3;

NOT操作符

WHERE子句中用来否定其后条件的关键字

1
2
3
SELECT name, id, class_id, score
FROM students
WHERE NOT class_id = 1 ;

用通配符进行过滤

LIKE操作符

通配符:用来匹配值的一部分特殊字符

搜索模式:由字面值、通配符或两者组合构成的搜索条件

谓词:从技术上说,LIKE是谓词而不是操作符

%通配符

表示任意字符出现任意次数(包括 0 次)(在 MS Access 中,通配符为*

筛选李姓的同学

1
2
3
SELECT *
FROM students
WHERE name LIKE '李%'

LIKE搜索区分大小写

空格问题:
在 MS Access 等 DBMS 中,会在字段内容尾部填补空格,所以像WHERE name LIKE 'F%y'的语句,匹配以 y 结尾的数据时,无法匹配到尾部补全空格的数据,解决办法是使用'F%y%'

NULL 问题
WHERE name LIKE '%'不会匹配 NULL

_通配符

用于匹配单个字符

DB2 不支持_通配符

在 MS Access 中应使用?

1
2
3
SELECT prod_id, prod_name
FROM Products
WHERE prod_name LIKE '__ inch teddy bear';

[]通配符

表示匹配方括号内的任意一个字符

1
2
3
4
SELECT cust_contact
FROM Customers
WHERE cust_contact LIKE '[JM]%'
ORDER BY cust_contact;

只有 MS Access 和 SQL Server 支持[]

否定方法:方括号里面加^前缀表示否定,例如[^JM]表示匹配除 J、M 以外的字符

在 MS Access 中,应将^替换为!使用,即[^JM]换为[!JM]

也可以使用NOT来否定

创建计算字段

字段:基本上与列的意思相同

拼接字段

拼接:将值联结到一起构成单个值。

可以使用一个特殊的操作符用来拼接两个列

  • +加号:MS Access、SQL Server(多数 DBMS 使用此方法)
  • ||两个竖杠:DB2、Oracle、PostgreSQL、SQLite、Open Office Base

样例代码拼接了名称和国家,并在国家两边加了括号

使用+的样例:

1
2
3
SELECT vend_name + ' (' + vend_country + ')'
FROM Vendors
ORDER BY vend_name;

使用||的样例

1
2
3
SELECT vend_name || ' (' || vend_country || ')'
FROM Vendors
ORDER BY vend_name;

在 MySQL 或 MariaDB 中需要使用的语句有所不同

使用 Concat()函数完成

1
2
3
SELECT Concat(vend_name, ' (', vend_country, ')')
FROM Vendors
ORDER BY vend_name;

在上述代码中,有的 DBMS 会将字段用空格填充使其宽度保持一致,而我们并不需要空格,可以使用 SQL 的RTRIM()函数完成

+形式代码为例:

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

TRIM 函数
RTRIM():去掉右边空格
LTRIM():去掉左边空格
TRIM():去掉左右两边空格

使用别名

拼接后的字符串需要有一个名字才能正常用于客户端,所以可以给拼接后字符串起一个别名

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

即 在Select后的表达式后面加上 AS title

建议在使用拼接字符串后最好加一个AS

别名有时也称为导出列

执行算术计算

例:

1
2
3
4
5
6
SELECT prod_id,
quantity,
item_price,
quantity * item_price AS expanded_price
FROM OrderItems
WHERE order_num = 20008;

这段代码输出了order_num = 20008的数据,并且每行数据加一个expanded_price字段显示quantity * item_price

支持的算数操作符:

  • +
  • -
  • *
  • /

测试计算表达式
虽然SELECT常用于检索数据,但如果省略FROM字句,则用与简单的访问和处理表达式
例如:

  • SELECT 3 * 2将返回6
  • SELECT Trim(' abc ')将返回abc
  • SELECT Now()将返回当前日期和时间

使用函数处理数据

文本处理函数

RTRIM():去掉字符串右边空格
LTRIM():去掉字符串左边空格
TRIM():去掉字符串左右两边空格

UPPER():将字符串转换为大写(在 MS Access 中使用UCASE()
LOWER():将字符串转换为小写(在 MS Access 中使用LCASE()

LEFT():返回字符串左边的字符
RIGHT():返回字符串右边的字符

LENGTH()DATALENGTH()LEN():返回字符串的长度

SOUNDEX():返回字符串的 SOUNDEX 值。

关于 SOUNDEX
SOUNDEX 是一个将任何文本串转换为描述其语音表示的字母数字模式的算法,SOUNDEX 考虑了类似的发音音符和音节,使得能对字符串进行发音比较而不是字母比较。
多数 DBMS 都提供对 SOUNDEX 的支持。MS Access 和 PostgreSQL 不支持SOUNDEX()
如果在创建 SQLite 时使用了 SQLITE_SOUNDEX 编译时选项,那么SOUNDEX()在 SQLite 中就可用,因为 SQLITE_SOUNDEX 不是默认选项,所以多数 SQLite 实现不支持SOUNDEX()

使用 SOUNDEX 可以匹配发音相同的单词,在拼错时很有用

1
2
3
SELECT cust_name, cust_contact
FROM Customers
WHERE SOUNDEX(cust_contact) = SOUNDEX('Michael Green');

(查询到Michelle Green的字段)

日期和时间处理函数

不同的 DBMS 都有自己特殊的日期和时间处理函数

举例:
Orders表中包含的订单都有订单日期,检索 2012 年的所有订单

SQL Server 和 Sybase

1
2
3
SELECT order_num
FROM Orders
WHERE DATEPART(yy, order_date) = 2012;

MS Access

1
2
3
SELECT order_num
FROM Orders
WHERE DATEPART('yyyy', order_date) = 2012;

通过上面的两个例子,可以看出DATEPART()函数接收两个参数,分别是返回的成分标识和从中返回成分的日期

类似的例子:
PostgreSQL

1
2
3
SELECT order_num
FROM Order
WHERE DATE_PART('year', order_date) = 2012;

Oracle

1
2
3
SELECT order_num
FROM Orders
WHERE to_number(to_char(order_date, 'YYYY')) = 2012;

这里to_char函数用于提取日期的成分,to_number函数用于将提取出来的成分转为数值,以便与 2012 进行比较

也可以使用BETWEEN操作符:

1
2
3
4
SELECT order_num
FROM Orders
WHERE order_date
BETWEEN to_date('01-01-2012') AND to_date('12-31-2012');

使用BETWEEN判断时间区间

此代码在 SQL Server 中不起作用,因为其不支持to_date函数

MySQL 和 MariaDB

MySQL 和 MariaDB 没有DATEPART()函数,但可以使用YEAR()函数提取年份

1
2
3
SELECT order_num
FROM Orders
WHERE YEAR(order_date) = 2012;

SQLite

在 SQLite 中有一个小技巧,可以直接用strftime()时间格式化函数提取年份比较

1
2
3
SELECT order_num
FROM Orders
WHERE strftime('%Y', order_date) = '2012';

数值处理函数

函数 说明
ABS() 返回一个数的绝对值
COS() 返回一个角度的余弦
EXP() 返回一个数的指数值
PI() 返回圆周率
SIN() 返回一个角度的正弦
SQRT() 返回一个数的平方根
TAN() 返回一个角度的正切

汇总数据

聚集函数

用于汇总表中的数据,而不需要数据本身。

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

AVG()

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

计算 prod_price 的平均值,并以 avg_price 别名返回。

注意

  • AVG()只能用于单个列
  • 会自动忽略值为 NULL 的行

COUNT()

有两种使用方式:

  • COUNT(*)对表中行的数目进行技术,不管列中是否包含 NULL

    1
    2
    SELECT COUNT(*) AS num_cust
    FROM Customers;
  • COUNT(column)对特定列中具有值的行进行计数,并且忽略 NULL

    1
    2
    SELECT COUNT(cust_email) AS num_cust
    FROM Customers;

MAX()

返回指定列中的最大值,需要指定列名。

1
2
SELECT MAX(prod_price) AS max_price
FROM Products;

该函数同样也忽略 NULL 值。

MIN()

返回指定列中的最小值,需要指定列名。

1
2
SELECT MIN(prod_price) AS min_price
FROM Products;

该函数同样也忽略 NULL 值。

SUM()

返回指定列值的和

1
2
3
SELECT SUM(quantity) AS items_ordered
FROM OrderItems
WHERE order_num = 20005;

也可以在其中使用表达式:

1
2
3
SELECT SUM(item_price*quantity) AS total_price
FROM OrderItems
WHERE order_num = 20005;

该函数同样也忽略 NULL 值。

聚集不同值

所有聚集函数都可以有如下两种用法:

  • ALL(默认):对所有行操作(无需指定,默认行为)
  • DISTINCT:只包含不同的值(需要用DISTINCT指定)

MS Access中不支持DISTINCT,要实现类似结果,需要将DISTINCT数据返回到外部SELECT COUNT(*)语句。

下面的例子只计算了不同数据的平均值:

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

**在COUNT中使用DISTINCT**时必须指定列名(COUNT(column))不能用于计算或表达式。

部分 DBMS 还支持其他聚集参数,如支持对查询结果的子集进行计算的TOPTOP PERCENT

组合聚集函数

SELECT语句可根据需要包含多个聚集函数:

1
2
3
4
5
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;

分组数据

创建分组

1
2
3
SELECT vend_id, COUNT(*) AS num_prods
FROM Products
GROUP BY vend_id;

上例表示计算不同vend_id对应的num_prods的总和。

GROUP BY的一些规定:

  • GROUP BY子句可以包含任意数目的列,因而可以对分组进行嵌套, 更细致地进行数据分组。
  • 如果在GROUP BY子句中嵌套了分组,数据将在最后指定的分组上进行汇总
  • GROUP BY子句中列出的每一列都必须事检索列或有效的表达式(但不能是聚集函数)。如果在SELECT中使用表达式,则必须在GROUP BY子句中指定相同的表达式。不能使用别名。
  • 大多数 SQL 实现不允许GROUP BY列带有长度可变的数据类型(如文本或备注型字段)。
  • 除聚集计算语句外,SELECT语句中的每一列都必须在GROUP BY子句中给出。
  • GROUP BY子句必须出现在WHERE子句之后,ORDER BY子句之前。

ALL子句

MS SQL Server 等 SQL 实现 在GROUP BY中支持可选ALL子句,这个子句可用来返回所有分组,即使没有匹配行的分组也返回 NULL。

过滤分组

使用HAVING子句过滤分组,它与WHERE类似,但是WHERE不能过滤分组。

所有类型的WHERE子句都可以用HAVING来替代,句法也相同。

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

上面这段代码筛选所有分组中行数大于 2 的分组。

WHEREHAVING之间的区别
有一种说法,WHERE是在分组前过滤,HAVING是在分组后过滤

SELECT 子句顺序

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

使用子查询

子查询

查询(query):任何 SQL 语句都是查询,但此属于一般指 SELECT 语句。

子查询(subquery):嵌套在其他查询中的查询。

利用子查询过滤

1
2
3
4
5
6
7
SELECT cust_id
FROM Orders
WHERE order_num IN (
SELECT order_num
FROM OrderItems
WHERE prod_id = 'RGAN01'
)

这段代码的意思是:
从 OrderItems 找到 prod_id = ‘RGAN01’对应的 order_num,并在 Orders 里面找其对应的 cust_id

子查询限制

  • 子查询只能查询单列
  • 性能方面,使用子查询并不总是执行这类数据检索的最有效方法

作为计算字段使用子查询

子查询语句可以放在计算字段使用:

1
2
3
4
5
6
7
8
9
SELECT cust_name,
cust_state,
(
SELECT COUNT(*)
FROM Orders
WHERE Orders.cust_id = Customers.cust_id
) AS orders
FROM Customers
ORDER BY cust_name;

这段代码意思是从 Customers 表中输出三个字段,并按 cust_name 排序,其中 orders 字段使用子查询从 Orders 表中查询 Orders.cust_id = Customers.cust_id 得到。

这里的 WHERE 子句不同的是:它使用了完全限定别名,而不是别名(cust_id),它指定表名和列名(Orders.cust_id)。用一个句点分隔表名和列名,在有可能混淆列名时必须使用这种语法。

完全限定列名:指定表名和列名(Orders.cust_id),避免出现冲突列名而导致的歧义性,不这样写会导致 DBMS 抛出错误信息。 好的做法是,如果在 SELECT 语句中操作多个表,就应使用完全限定列名来避免歧义。

联结表

联结

SQL 最强大的功能之一就是能在数据查询的执行中联结(join)表。

关系表

关系表将数据分解为多个表能更有效地存储,更方便地处理,并且可伸缩性更好。但是有代价的。

使用联结可以在一条 SELECT 语句中导出关联数据。

使用交互式 DBMS 工具
要先理解联结在数据库中并不是实际存在的,DBMS 会根据需要建立联结,它在查询执行期间一直存在。
有的 DBMS 会提供图形界面来交互式定义表关系,以及管理联结关系的合法性。

创建联结

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

通过 vend_id 字段创建 Vendors 和 Products 两个表的联结关系。

WHERE 子句重要性:用于给定联结条件来联结两个表。所以一定要有 WHERE 语句,并保证其准确性。

笛卡尔积
由没有联结条件的表关系返回的结果为笛卡尔积,即检索出的行的数目将是两表行数乘积。

联结多个表

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

使用 JOIN 子句创建联结

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

内联结也可以使用INNER JOIN``ON创建:

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

联结注意

性能考虑:DBMS 在运行时关联指定的每个表,以处理联结。这种处理可能非常耗费资源,因此应该注意,不要联结不必要的表。联结的表越多,性能下降越厉害。

许多 DBMS 都有联结中表最大数目限制。

联结替代子查询

子查询并不总是执行复杂 SELECT 操作的最有效方法,使用联结会更有效。

子查询:

1
2
3
4
5
6
7
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'));

联结:

1
2
3
4
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';

创建高级联结

为表起别名

SQL 除了可以对列名起别名( AS XXX ),还可以给表名起别名

1
2
3
4
5
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’

Oracle 中使用别名不可以使用AS,直接省略AS即可,例如Customers AS C写为Customers C

自联结

在同一个表中查询两次

子查询写法:

1
2
3
4
5
6
7
SELECT cust_id, cust_name, cust_contact
FROM Customers
WHERE cust_name = (
SELECT cust_name
FROM Customers
WHERE cust_contact = 'Jim'
)

自联结写法:

1
2
3
4
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';

必须将 Customers 表分别起 c1 和 c2 别名来使用

在很多 DBMS 中,自联结比子查询快得多

自然联结

自然联结排除多次出现,使每一列只返回一次。

我们迄今为止建立的每个内联结都是自然联结,很可能永远都不会用到不是自然联结的内联结。

外联结

即内联结包括没有匹配的数据,但要选择一个表展示所有数据

内联结(不包含没有匹配的数据):

1
2
3
SELECT Customers.cust_id, Orders.order_num
FROM Customers INNER JOIN Orders
ON Customers.cust_id = Orders.cust_id;

外联结(包含没有匹配的数据):

1
2
3
SELECT Customers.cust_id, Orders.order_num
FROM Customers LEFT OUTER JOIN Orders
ON Customers.cust_id = Orders.cust_id;

在这个代码中,没匹配的数据 order_num 表示为 NULL

外联结的关键字有LEFT OUTER JOINRIGHT OUTER JOIN两种(左外联结和右外联结),通过左右选择展示哪个表的全部数据(包含没有匹配的数据)

SQLite 中:
SQLite 仅支持LEFT OUTER JOIN一个方向

还有一种外联结是“全外联结”FULL OUTER JOIN包含两个表的全部数据。

1
2
3
SELECT Customers.cust_id, Orders.order_num
FROM Orders FULL OUTER JOIN Customers
ON Orders.cust_id = Customers.cust_id;

全外联结支持性很差:
Access、MariaDB、MySQL、Open Office Base 和 SQLite 不支持 FULL OUTER JOIN 语法。

使用带聚集函数的联结