快速回顾 MySQL:联表查询

前提要述:参考书籍《MySQL必知必会》

文章目录

    • 12.1 联结
      • 12.1.1 关系表
      • 12.1.2 联结的作用
      • 12.1.3 创建联结
      • 12.1.4 WHERE子句的重要性
      • 12.1.5 内部联结
      • 12.1.6 联结多个表
    • 12.2 使用不同类型的联结
      • 12.2.1 自联结
      • 12.2.2 自然联结
      • 12.2.3 外部联结
      • 12.2.4 使用带聚集函数的联结
    • 总结

12.1 联结

SQL最强大的功能之一就是能在数据检索查询的执行中联结(join)表。即两张表通过外键进行的联表查询。使用联表前,需要了解关系表。

12.1.1 关系表

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

现在,假设有由同一个供应商生产的多种物品,那么在何处存储供应商信息(如,供应商名,地址,联系方式等)?将供应商信息跟产品信息存储在同一张表产生的缺点如下:

  • 因为同一个供应商生产的每个产品的供应商信息都是相同的,对每个产品重复此信息即浪费时间又浪费存储空间。
  • 如果供应商信息改变(例如,供应商搬家或电话号码改变),那么表中改动得供应商信息都要一个一个改变。
  • 如果有重复数据(即每种产品都要存储供应商信息),很难保证每次输入该数据得方式都相同。,不一致的数据在报表中很难利用。

关系表的设计就是要保证把信息分解成多个表,一类数据一张表。各表通过某些常用的值(即关系设计中的关系(relational))互相关联。

所以,对于这个例子,我们创建两张表,一张存储供应商信息(vendors),一张存储产品信息(products)。

vendors表存储所有的供应商信息,每个供应商具有唯一的标识,即主键,可以是供应商ID或者其他,这里就假设是供应商ID为主键。
products表只存储产品信息,它除了存储供应商ID(vendors表中的主键)外不存储其他供应商信息。vendors表的主键又叫做products的外键,它将vendors表与products表关联,利用供应商ID能从vendors表中找出相应供应商的详细信息。

外键:是某张表的一列,它包含另一张表的主键值,定义了两张表之间的关系。

这样做的好处:

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

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

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

12.1.2 联结的作用

分解数据为多张表能更有效地存储,更方便地处理,并且具有更大的可伸缩性。

如果数据存储在多张表中,怎样使用单条SELECT语句检索出数据?

那就是使用联结,用来在一条SELECT语句中关联表。可以使联结多个表返回一组输出。

联结不是物理实体,它在实际的数据库表中不存在。


注意:在使用关系表时,仅在关系列中插入合法的数据非常重要。就像上面的例子,如果在products表中插入拥有非法供应商ID(即该供应商ID不存在于vendors表中)的供应商生产的产品,则这些产品是不可访问的,因为它们没有关联到某个供应商。

所以解决的方式,就是在创建表时,指定好主键和外键。比如只允许在products表中的供应商ID列中出现合法值,那么就得通过在表定义中指定主键和外键。

这叫做:维护引用完整性


12.1.3 创建联结

联结方式有很多种,待我一一讲述。

在前一篇的子查询中也有看到过了一种了,就是那个相关子查询,即涉及外部表查询的子查询。而且我们使用完全限定名,为了更好的区分两个表的相同列名。

比如:按上面的例子:

SELECT vend_name, prod_name, prod_price
FROM vendors, products
WHERE vendors.vend_id = products.vend_id
ORDER BY vend_name, prod_name;

输出:

+-------------+----------------+------------+
| vend_name   | prod_name      | prod_price |
+-------------+----------------+------------+
| ACME        | Bird seed      | 10.00      |
| ACME        | Carrots        | 2.50       |
| ACME        | Detonator      | 13.00      |
| ACME        | Safe           | 50.00      |
| ACME        | Sling          | 4.49       |
| ACME        | TNT (1 stick)  | 2.50       |
| ACME        | TNT (5 sticks) | 10.00      |
| Anvils R Us | .5 ton anvil   | 5.99       |
| Anvils R Us | 1 ton anvil    | 9.99       |
| Anvils R Us | 2 ton anvil    | 14.99      |
| Jet Set     | JetPack 1000   | 35.00      |
| Jet Set     | JetPack 2000   | 55.00      |
| LT Supplies | Fuses          | 3.42       |
| LT Supplies | Oil can        | 8.99       |
+-------------+----------------+------------+
14 rows in set (0.04 sec)

解释:

  • 跟普通SELECT语句的最大差别是所指定的两个列(prod_name, prod_price)在一个表中,而另一个列(vend_name)在另一张表中。
  • FROM子句后面可以跟上多张表
  • WHERE子句匹配vendors表中的vend_id和products表中的vend_id。
  • 并且WHERE子句中,因为两张表都有vend_id,当引用时,那么就需要使用完全限定名,即表名 + . + 列名。防止二义性。

12.1.4 WHERE子句的重要性

++上面的语句,在不设置外键的情况下,还是可以查询,就因为WHERE子句的原因。++ 上面也有说缺点了:就是没有外键的情况下,查询是可以查询的,通过WHERE子句,但是有非法值,就会查询失败。要维护引用完整性

在使用SELECT语句中联结几张表时,相应的关系是在运行中构造的。在数据库表的定义中不存在能指示MySQL如何对表进行联结的东西。所以必须自己做。

在联结两个表时,实际上做的是将第一张表中的每一行与第二张表中的每一行配对,即笛卡尔积。而WHERE子句作为过滤条件,它只包含那些匹配的条件(这里指联结条件)的行。

所以如果没有WHERE子句,那么第一张表中的每一张行将与第二张表中的每一行配对,而不管它们逻辑上是否可以配对在一起。

笛卡尔积(cartesian product):由没有联结条件的表关系返回的结果为笛卡尔积,检索出的行的数目将是第一张表中的行数乘以第二张表中的行数。

可以试试:

SELECT vend_name, prod_name, prod_price
FROM vendors, products
ORDER BY vend_name, prod_name;

所以,应该保证所有的联结都有WHERE子句,否则MySQL将返回比想象中的数据还要多的数据,并且联结条件要正确。

有时会听到返回称为叉联结(corss join)的笛卡尔积的联结类型。

12.1.5 内部联结

上面所用的联结其实叫做等值联结(equijoin),它基于两张表之间的相等测试,也可以叫内部联结。其实,对于这种联结可以使用稍微不同的语法来明确指定联结类型。如下:

SELECT vend_name, prod_name, prod_price
FROM vendors INNER JOIN products
ON vendors.vend_id = products.vend_id
ORDER BY vend_name, prod_name;

解释:此SELECT语句跟前面的SELECT语句相同,但FROM子句不同。这里,两张表之间的关系是FROM子句的组成部分,以INNER JOIN指定。在使用这种语法时,联结条件用特定的ON子句而不是WHERE子句给出。传递给ON的实际条件跟与传递给WHERE的相同。

那么使用哪种语法好?ANSI SQL规范首选INNER JOIN语法。尽管使用WHERE子句定义联结的确比较简单,但是使用明确的联结语法能够确保不会忘记联结条件,有时候这样做也能影响性能。

12.1.6 联结多个表

SQL对一条SELECT语句中可以联结的表的数目没有限制。创建联结的规则基本相同。即首先列出所有表,然后定义表之间的关系。例如:

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 = 20005;

解释:此例子显示编号为20005的订单中的物品。订单物品存储在orderitems表中。每个产品按其产品ID存储,它引用products表中的产品。这些产品在通过供应商ID联结到vendors表中相应的供应商,供应商ID存储在每个产品的记录中。然后FROM语句列出3张表,WHERE子句定义两条联结条件和一条过滤条件。

下面是使用INNER JOIN的语法:

SELECT prod_name, vend_name, prod_price, quantity 
FROM orderitems INNER JOIN products  
    INNER JOIN vendors
    ON products.vend_id = vendors.vend_id 
        AND orderitems.prod_id = products.prod_id
        AND order_num = 20005;

即:把,(逗号)改成INNER JOIN,把WHERE改成ON。

MySQL在运行时关键指定的每个表以处理联结。这种处理可能是非常耗费资源的,因此应该仔细,不要联结不必要的表。联结的表越多,性能下降越厉害。

回顾以前的子查询的一个例子:

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 = 'TNT2'));

子查询并不总是执行复杂SELECT操作的最有效的方法,下面是使用联结的相同查询:

SELECT cust_name, cust_contact 
FROM customers INNER JOIN orders
    INNER JOIN orderitems
    ON customers.cust_id = orders.cust_id
        AND orderitems.order_num = orders.order_num
        AND prod_id = 'TNT2';

为执行任意给定的SQL操作,一般存在不止一种方法。很少有绝对正确或绝对错误的方法。性能可能或受到操作类型、表中数据量、是否存在索引或键以及其他一些条件的影响。 所以,多次尝试,找找最合适的结果。

小结:介绍了关系数据库设计的一些基本知识,包括等值联结(也叫内部联结),后面还有其他的联结。

12.2 使用不同类型的联结

先说一下表别名,除了列别名,还可以使用表别名。无论是表别名,列别名,都是使用关键字AS,而且AS还可以省略,即比如在表名后面空一格再加上别名就是表别名,列别名也是一样。

# 表
FROM customers c
# 或
FROM customers AS c

# 列
SELECT stu_id 学号
# 或
SELECT stu_id AS 学号

别名的优点:能缩短SQL语句;允许在单句SELECT语句中多次使用简化别名使用相同的表。

前面使用的是内部联结(等值联结),限制介绍另外三种:自联结,自然联结和外部联结。

12.2.1 自联结

举个例子:假如发现某物品(其ID为DTNTR)存在问题,因此向知道生产该物品的供应商生产的其他物品是否也存在这些问题。

分析:此查询要求首先找到生产ID为DTNTR的物品的供应商,然后找出这个供应商生产的其他物品。

下面是解决此问题的方式之一:

SELECT prod_id, prod_name
FROM products
WHERE vend_id = (
    SELECT vend_id
    FROM products
    WHERE prod_id = 'DTNTR'
);

这是使用了子查询,下面是使用联结的方式:(使用了表别名)

SELECT p1.prod_id, p1.prod_name
FROM products p1, products p2
WHERE p1.vend_id = p2.vend_id
    AND p2.prod_id = 'DTNTR';

此查询中需要的两个表实际上是相同的表,可以看到FROM子句出现了2次products表。这虽然是合法的,但是对products的引用具有二义性,因为MySQL都不知道你引用的是products表的哪个实例。因此使用表别名来区分,并且列需要明确的给出全名(完全限定名)。

所以自联结是使用自联结可以将自身表的一个镜像当作另一个表来对待,从而能够得到一些特殊的数据。

自联结通常作为外部语句用来替代从相同表中检索数据时使用的子查询语句。虽然结果是相同的,但是有时候处理联结远比处理子查询快得多。

12.2.2 自然联结

自然联结(Natural join)是一种特殊的等值联结,它要求两个关系中进行比较的分量必须是相同的属性组,并且在结果中把重复的属性列去掉。而等值联结并不去掉重复的属性列。(数据库概论)

比如有下面两张表:

table1:


A B C
1 a1 b1 2
2 a2 b2 3
3 a3 b3 5

table2:


B E D
1 b1 e1 2
2 b2 e2 2
3 b3 e3 8

等值联结使用公共属性联结:

SELECT  table1.*, table2.*
FROM table1, table2
WHERE table1.B = table2.B;

结果:(注意是先笛卡尔积,再过滤)

A table1.B C table2.B E D
a1 b1 2 b1 e1 2
a2 b2 3 b2 e2 5
a3 b3 5 b3 e3 8

等值联结还可以不用公共属性联结:(当然前提是数据类型要一致)

SELECT  table1.*, table2.*
FROM table1, table2
WHERE table1.C = table2.D;

结果:(注意是先笛卡尔积,再过滤)

A table1.B C table2.B E D
a1 b1 2 b1 e1 2
a1 b1 2 b2 e2 2

那么自然联结必须是公共属性联结,并且去掉重复列:

SELECT  table1.*, table2.E, table2.D
FROM table1, table2
WHERE table1.B = table2.B;

结果:(注意是先笛卡尔积,再过滤,再去重)

A B C E D
a1 b1 2 e1 2
a2 b2 3 e2 5
a3 b3 5 e3 8

像以前的例子建立的每个内部联结其实都是自然联结。


转载
笛卡尔积、等值连接、自然连接三者有什么区别?

笛卡尔积对两个关系 R 和 S 进行操作,产生的关系中元组个数为两个关系中元组个 数之积。
等值联接则是在笛卡尔积的结果上再进行选择操作,挑选关系第 i 个分量与第(r+j) 个分量值相等的元组;
自然连接则是在等值联接(以公共属性值相等为条件)的基础上再行投 影操作,去掉 S 中的公共属性列,当两个关系没有公共属性时,自然连接就转化成笛卡尔 积。

1、自然连接一定是等值连接,但等值连接不一定是自然连接。
2、等值连接要求相等的分量,不一定是公共属性;而自然连接要求相等的分量必须是公共属性。
3、等值连接不把重复的属性除去;而自然连接要把重复的属性除去。

12.2.3 外部联结

许多联结将一个表中的行与另一个表中的行相关联。但有时会需要包含没有关联的行的那些行。例如下面的例子:

  • 对每个客户下了多少订单进行计数,包括那些至今尚未下订单的客户;
  • 列出所有产品以及订购数量,包括没有人订购的产品;
  • 计算平均销售规模,包括那些至今尚未下订单的客户。

在上面这些例子中,联结包含了那些在相关表中没有关联的行。这种类型的联结称为外部联结。

下面是一个内部联结,它检索所有客户及其订单:

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

输出:

+---------+-----------+
| cust_id | order_num |
+---------+-----------+
|   10001 |     20005 |
|   10001 |     20009 |
|   10003 |     20006 |
|   10004 |     20007 |
|   10005 |     20008 |
+---------+-----------+

而外部联结的语法类似,为了检索所有客户,包括那些没有订单的客户:

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

输出:

+---------+-----------+
| cust_id | order_num |
+---------+-----------+
|   10001 |     20005 |
|   10001 |     20009 |
|   10002 |     NULL  |
|   10003 |     20006 |
|   10004 |     20007 |
|   10005 |     20008 |
+---------+-----------+

外部联结使用关键字OUTER JOIN来指定联结的类型,并且在使用OUTER JOIN语法时,前面必须配上LEFT或RIGHT关键字指定包括其所有行的表(RIGHT指出的时OUTER JOIN右边的表,LEFT指出的是OUTER JOIN左边的表)。

上面的例子使用LEFT OUTER JOIN从FROM子句的左边表(customers表)中选择所有行。
你可以试试使用RIGHT OUTER JOIN,它是选择右边的表(orders)中的所有行,然后因为都是有跟左边的表相关联的,所以没有一个是NULL值的。

MySQL不支持简化字符*=和=*的使用(即上面的左外联结和右外联结),这两种操作符在其他DBMS中是很流行的。

究竟使用哪一种外部联结?看情况,它们之间唯一的差别是所关联的表的顺序不同。也就是,左外部联结可通过颠倒FROM子句后的表或WHERE子句前中的表的顺序转换为右外部联结。因此,两种类型的外部联结可互换使用,而究竟属于哪一种纯粹是根据方便而定。

12.2.4 使用带聚集函数的联结

聚集函数也可以与联结一起使用。

如下:比如要检索所有客户及每个客户所下的订单数:

SELECT c.cust_name, c.cust_id, COUNT(o.order_num) AS num_ord
FROM customers AS c INNER JOIN orders AS o
ON c.cust_id = o.cust_id
GROUP BY c.cust_id;

输出:

+----------------+---------+---------+
| cust_name      | cust_id | num_ord |
+----------------+---------+---------+
| Coyote Inc.    |   10001 |       2 |
| Wascals        |   10003 |       1 |
| Yosemite Place |   10004 |       1 |
| E Fudd         |   10005 |       1 |
+----------------+---------+---------+

而外部联结也一样可以使用,拿上面的例子:要检索所有客户及每个客户所下的订单数,包括那些没有订单的。

SELECT c.cust_name, c.cust_id, COUNT(o.order_num) AS num_ord
FROM customers AS c LEFT OUTER JOIN orders AS o
ON c.cust_id = o.cust_id
GROUP BY c.cust_id;
+----------------+---------+---------+
| cust_name      | cust_id | num_ord |
+----------------+---------+---------+
| Coyote Inc.    |   10001 |       2 |
| Mouse House    |   10002 |       0 |
| Wascals        |   10003 |       1 |
| Yosemite Place |   10004 |       1 |
| E Fudd         |   10005 |       1 |
+----------------+---------+---------+

总结

现在来总结一下联结的使用条件:

  • 注意所使用的联结类型。一般我们使用内部联结,但使用外部联结也是有效的。
  • 保证使用正确的联结条件,否则将返回不正确的数据。
  • 应该总是提供联结条件,否则会得出笛卡尔积。
  • 在一个联结中可以包含多个表,甚至对于每个联结可以采用不同的联结类型。虽然这样做是合法的,一般也很有用,但应该在一起测试它们前,分别测试每个联结。这将使故障排除更为简单。

你可能感兴趣的:(数据库)