目录
博主用的是mysql8 DBMS,附上示例资料:
百度网盘链接: https://pan.baidu.com/s/1XaWi3Y7hpXbs_uHq2cPI6Q
提取码: fpnx
1、联结、关系表
SQL 最强大的功能之一就是能在数据查询的执行中联结(join)表。联结是利用 SQL 的 SELECT 能执行的最重要的操作,很好地理解联结及其语法是学习 SQL 的极为重要的部分。
在能够有效地使用联结前,必须了解关系表以及关系数据库设计的一些基础知识。下面的介绍并不能涵盖这一主题的所有内容,但作为入门已经够了。
(1)关系表
理解关系表,最好是来看个例子。
有一个包含产品目录的数据库表,其中每类物品占一行。对于每一种物品,要存储的信息包括产品描述、价格,以及生产该产品的供应商。现在有同一供应商生产的多种物品,那么在何处存储供应商名、地址、联系方法等供应商信息呢?将这些数据与产品信息分开存储的理由是:
- 同一供应商生产的每个产品,其供应商信息都是相同的,对每个产品重复此信息既浪费时间又浪费存储空间;
- 如果供应商信息发生变化,例如供应商迁址或电话号码变动,只需修改一次即可;
- 如果有重复数据(即每种产品都存储供应商信息),则很难保证每次输入该数据的方式都相同。不一致的数据在报表中就很难利用。
关键是,相同的数据出现多次决不是一件好事,这是关系数据库设计的基础。关系表的设计就是要把信息分解成多个表,一类数据一个表。各表通过某些共同的值互相关联(所以才叫关系数据库)。
在这个例子中可建立两个表:一个存储供应商信息,另一个存储产品信息。Vendors 表包含所有供应商信息,每个供应商占一行,具有唯一的标识。此标识称为主键(primary key),可以是供应商 ID 或任何其他唯一值。
Products 表只存储产品信息,除了存储供应商 ID(Vendors 表的主键)外,它不存储其他有关供应商的信息。Vendors 表的主键将 Vendors 表与 Products 表关联,利用供应商 ID 能从 Vendors 表中找出相应供应商的详细信息。
这样做的好处是:
- 供应商信息不重复,不会浪费时间和空间;
- 如果供应商信息变动,可以只更新 Vendors 表中的单个记录,相关表中的数据不用改动;
- 由于数据不重复,数据显然是一致的,使得处理数据和生成报表更简单。
总之,关系数据可以有效地存储,方便地处理。因此,关系数据库的可伸缩性远比非关系数据库要好。
可伸缩(scale)
能够适应不断增加的工作量而不失败。设计良好的数据库或应用程序称为可伸缩性好(scale well)。
(2)为什么使用联结
如前所述,将数据分解为多个表能更有效地存储,更方便地处理,并且可伸缩性更好。但这些好处是有代价的。 如果数据存储在多个表中,怎样用一条 SELECT 语句就检索出数据呢?
答案是使用联结。简单说,联结是一种机制,用来在一条 SELECT 语句中关联表,因此称为联结。使用特殊的语法,可以联结多个表返回一组输出,联结在运行时关联表中正确的行。
2、如何创建联结
(1)笛卡尔积(叉联结)--用逗号分隔
创建一个联结非常简单,指定要联结的所有表以及关联它们的方式即可。
比如查找供应商名、对应的prod_name、prod_price:
-- 查找供应商名、对应的prod_name、prod_price
SELECT vend_name, prod_name, prod_price
FROM vendors, products
WHERE vendors.vend_id = products.vend_id;
-- 联结的操作在FROM中执行,连接后(笛卡尔积)有6*14行,再经过vend_id进行过滤
SELECT 语句与前面所有语句一样指定要检索的列。这里最大的差别是所指定的两列(prod_name 和 prod_price)在一个表中,而第三列(vend_name)在另一个表中。
这条语句的 FROM子句列出了两个表:Vendors 和 Products。它们就是这条 SELECT 语句联结的两个表的名字。这里的联结方式是逗号“,”,代表笛卡尔积,后续的WHERE用于筛选满足条件的行。
可以看到,要匹配的两列指定为 Vendors.vend_id 和 Products.vend_id。这里需要这种完全限定列名,如果只给出 vend_id,DBMS 就不知道指的是哪一个(每个表中有一个)。
警告:完全限定列名
就像前一课提到的,在引用的列可能出现歧义时,必须使用完全限定列名(用一个句点分隔表名和列名)。如果引用一个没有用表名限制的具有歧义的列名,大多数 DBMS 会返回错误。
(2)where子句的重要性
举例,没有利用where进行过滤时:结果就是两个表的笛卡尔积。
SELECT *
FROM vendors, products
笛卡儿积(cartesian product)
由没有联结条件的表关系返回的结果为笛卡儿积。检索出的行的数目将是第一个表中的行数乘以第二个表中的行数。
相应的笛卡儿积不是我们想要的。这里返回的数据用每个供应商匹配了每个产品,包括了供应商不正确的产品(即使供应商根本就没有产品)。
注意:不要忘了 WHERE 子句
要保证所有联结都有 WHERE 子句,否则 DBMS 将返回比想要的数据多得多的数据。同理,要保证 WHERE 子句的正确性。不正确的过滤条件会导致 DBMS 返回不正确的数据。
提示:叉联结(×):有时,返回笛卡儿积的联结,也称叉联结(cross join)。
(3)内联结--INNER JOIN
上面使用叉联结+WHERE过滤后的结果与等值联结的结果相同。
等值联结(equijoin),它基于两个表之间的相等测试。这种联结也称为内联结(inner join)。其实,可以对这种联结使用稍微不同的语法,明确指定联结的类型。下面的 SELECT 语句返回与前面例子完全相同的数据:
-- 目前为止使用的联结称为等值联结(equijoin),它基于两个表之间的相等测试。这种联结也称为内联结(inner join)。
SELECT vend_name, prod_name, prod_price
FROM Vendors INNER JOIN Products
ON Vendors.vend_id = Products.vend_id
-- 在使用这种语法时,联结条件用特定的 ON 子句而不是 WHERE 子句给出。
WHERE prod_price > 5;
- 内联结使用INNER JOIN来连接两个表,ON子句指定连接的条件,在这里是一种等值条件,意味着满足条件的行才会进行联结。
- 如果没有ON子句,联结的结果就是叉积的结果。
- ON用于指定联结的条件,是在联结之前进行,WHERE只起到过滤的作用,在联结之后进行
(4)联结多个表
- 叉积用逗号分隔即可
- 内联结用INNER JOIN分隔即可
联结多个表,查找20005号订单的各个产品名、生产商名、价格和数量
-- 联结多个表,查找20005号订单的各个产品名、生产商名、价格和数量
-- 内联结+ON条件联结+where过滤方式(只用ON或者where也可)
SELECT prod_name, vend_name, prod_price, quantity
FROM OrderItems INNER JOIN Products INNER JOIN Vendors
ON orderitems.prod_id = products.prod_id
AND products.vend_id = vendors.vend_id
WHERE order_num = 20005;
-- 叉积过滤方式
SELECT prod_name, vend_name, prod_price, quantity
FROM vendors, orderitems, products
WHERE order_num = '20005'
AND vendors.vend_id = products.vend_id
AND orderitems.prod_id = products.prod_id
注意:性能考虑
DBMS 在运行时关联指定的每个表,以处理联结。这种处理可能非常耗费资源,因此应该注意,不要联结不必要的表。联结的表越多,性能下降越厉害。
需要列出订购物品号 ANV01 的所有顾客的名字和联系名
-- 需要列出订购物品号 ANV01 的所有顾客的名字和联系名
SELECT cust_name, cust_contact
FROM orderitems INNER JOIN orders INNER JOIN customers
ON orderitems.order_num = orders.order_num
AND orders.cust_id = customers.cust_id
WHERE prod_id = 'ANV01';
3、使用表别名(对比列别名)
SQL 除了可以对列名和计算字段使用别名,还允许给表名起别名。这样做有两个主要理由:
- 缩短 SQL 语句;
- 允许在一条 SELECT 语句中多次使用相同的表。
请看下面的 SELECT 语句。它与前一课例子中所用的语句基本相同,但改成了使用别名:
联结多个表,查找20005号订单的各个产品名、生产商名、价格和数量
-- 表别名,与列或者计算字段别名不同,在FROM中进行
-- 联结多个表,查找20005号订单的各个产品名、生产商名、价格和数量
SELECT prod_name, vend_name, prod_price, quantity
FROM OrderItems AS O INNER JOIN Products AS P INNER JOIN Vendors AS V
ON O.prod_id = P.prod_id
AND P.vend_id = V.vend_id
WHERE order_num = 20005;
可以看到,FROM 子句中的三个表全都有别名。Customers AS C 使用 C作为 Customers 的别名,如此等等。这样,就可以使用省略的 C 而不用全名 Customers。在这个例子中,表别名只用于 WHERE 子句。其实它不仅能用于 WHERE 子句,还可以用于 SELECT 的列表、ORDER BY 子句以及其他语句部分。
4、自联结、自然联结、外联结
(1)自联结:self-join
前面所提的表别名的一个主要原因就是在一个select语句中多次使用同一个表。
而自联结就是将表自身联结起来:例如在customers中找到与 Jim Jones 同一公司的所有顾客:
- 容易想到使用子查询方式:
-- 找到与 Jim Jones 同一公司的所有顾客
-- 子查询方式
SELECT cust_id, cust_name, cust_contact, cust_email
FROM customers
WHERE cust_name = (SELECT cust_name
FROM customers
WHERE cust_contact = 'Jim Jones');
- 使用自联结呢?--其实就是内联结或者笛卡尔积,不过两个表都是同一个表(使用笛卡尔积也可以)。
-- 自联结,就是将利用表别名将表本身进行内联结或者笛卡尔积
SELECT C1.cust_id, C1.cust_name, C1.cust_contact, C1.cust_email
FROM customers AS C1 INNER JOIN customers AS C2
ON C2.cust_contact = 'Jim Jones'
WHERE C1.cust_name = C2.cust_name;
-- 利用ON先将C2过滤得只剩两行再进行内联结
-- 联结后再利用where进行过滤将C1的cust_name与C2的cust_name进行匹配筛选
此查询中需要的两个表实际上是相同的表,因此 Customers 表在 FROM子句中出现了两次。虽然这是完全合法的,但对 Customers 的引用具有歧义性,因为 DBMS 不知道你引用的是哪个 Customers 表。
解决此问题,需要使用表别名。Customers 第一次出现用了别名 C1,第二次出现用了别名 C2。现在可以将这些别名用作表名。例如,SELECT 语句使用 C1 前缀明确给出所需列的全名。如果不这样,DBMS 将返回错误,因为名为 cust_id、cust_name、cust_contact 的列各有两个。DBMS不知道想要的是哪一列(即使它们其实是同一列)。
提示:用自联结而不用子查询
自联结通常作为外部语句,用来替代从相同表中检索数据的使用子查询语句。虽然最终的结果是相同的,但许多 DBMS 处理联结远比处理子查询快得多。应该试一下两种方法,以确定哪一种的性能更好。
(2)自然联结:natural join
- 经过前面的练习,不难知道联结之后的结果难免出现相同的列,自然联结就是排除列的多次出现,使得每列只返回一次。
- 怎样完成这项工作呢?答案是,系统不完成这项工作,由你自己完成它。也就是自己指定检索出的列名。一般通过对一个表使用通配符(SELECT *),而对其他表的列使用明确的子集来完成。
-- 自然联结,想要消除内联结导致重复的列,系统无法做到,只能人工完成
-- 需要列出订购物品号 ANV01 的所有顾客的信息,包括订单信息,但不重复列
SELECT C.*,
O.order_num, O.order_date,
OI.order_item, OI.prod_id, OI.item_price, OI.quantity
FROM orders AS O
INNER JOIN orderitems AS OI
INNER JOIN customers AS C
ON OI.prod_id = 'ANV01'
AND OI.order_num = O.order_num
AND O.cust_id = C.cust_id;
-- 把ON换成WHERE效果是一样的,但是进行联结的时候不一样哦
-- WHERE是对联结的结果进行过滤,ON是在联结前设置条件
(3)外联结:left join、right join
许多联结将一个表中的行与另一个表中的行相关联,但有时候需要包含没有关联行的那些行。例如,可能需要使用联结完成以下工作:
- 对每个顾客下的订单进行计数,包括那些至今尚未下订单的顾客;
- 列出所有产品以及订购数量,包括没有人订购的产品;
- 计算平均销售规模,包括那些至今尚未下订单的顾客。
在上述例子中,联结包含了那些在相关表中没有关联行的行。这种联结称为外联结。
举例更好理解,比如使用内联结查看已有顾客的订单数量(这些顾客至少有订单)
SELECT customers.cust_id, COUNT(*)
FROM orders INNER JOIN customers
ON customers.cust_id = orders.cust_id
GROUP BY customers.cust_id;
这里的结果是满足的,但是如果我想要所有顾客的订单信息呢?(包括没有订单的顾客)就需要使用外联结了。
- 在使用 OUTER JOIN 语法时,必须使用 RIGHT 或 LEFT 关键字指定包括其所有行的表(RIGHT 指出的是 OUTER JOIN 右边的表,而 LEFT 指出的是 OUTER JOIN左边的表)
-- 由于这些顾客在orders中没有行记录相应的id,就需要联结在相关表中没有关联的行,这就是外联结
SELECT customers.cust_id, COUNT(orders.order_num)
FROM customers LEFT JOIN orders
ON customers.cust_id = orders.cust_id
GROUP BY customers.cust_id;
-- 因为有些顾客实际上没有订单,所以应该计数order_num
-- 还有一种叫全联结,但MySQL不允许
上面的例子使用 LEFT JOIN 从 FROM 子句左边的表(Customers 表)中选择所有行。为了从右边的表中选择所有行,需要使用 RIGHT JOIN,如下例所示:
SELECT customers.cust_id, COUNT(orders.order_num)
FROM orders RIGHT JOIN customers
ON customers.cust_id = orders.cust_id
GROUP BY customers.cust_id;
还存在另一种外联结,就是全外联结(full outer join),它检索两个表中的所有行并关联那些可以关联的行。与左外联结或右外联结包含一个表的不关联的行不同,全外联结包含两个表的不关联的行。但是MYSQL不支持全联结。
5、使用带聚集函数的联结
- 上面的例子已经可以看到我们使用了GROUP BY语句。也就是说,聚集函数也可以方便地与其他联结一起使用。
- 粗糙一点的话,将联结后的结果看作表就好啦。
6、使用联结和联结条件
汇总一下联结及其使用的要点。
- 注意所使用的联结类型。一般我们使用内联结,但使用外联结也有效。
- 关于确切的联结语法,应该查看具体的文档,看相应的 DBMS 支持何种语法(大多数 DBMS 使用这两课中描述的某种语法)。
- 保证使用正确的联结条件(不管采用哪种语法),否则会返回不正确的数据。
- 应该总是提供联结条件,否则会得出笛卡儿积。
- 在一个联结中可以包含多个表,甚至可以对每个联结采用不同的联结类型。虽然这样做是合法的,一般也很有用,但应该在一起测试它们前分别测试每个联结。这会使故障排除更为简单
7、小结
- 联结表时尽量用完全限定名,避免歧义;有时候经常需要联结多个表,注意数量。
- 叉积使用逗号分隔,where用于过滤联结之后的行,没有指定条件的内联结就是笛卡尔积。
- 内联结使用INNER JOIN分隔,联结前ON子句可指定联结条件,注意与where区分。
- 使用表别名,可以缩短我们的代码,也可以用于自联结。
- 自联结就是对相同的表利用表别名来进行联结操作,至于采用何种联结取决于自己。
- 在进行联结时难免出现重复的列,自然联结的要求就是消除重复列,不过只能自己操作,一般是取一个表的*和其他表的列。
- 外联结用于解决统计某些不包含在统计表中的场景,比如统计所有顾客的订单量(即使顾客没有订单)。
- 使用联结后同样可以进行分组操作,也能使用聚集函数。