一、联结
SQL最强大的功能之一就是能在数据检索查询的执行中联结(join)表。联结是利用SQL的SELECT能执行的最重要的操作。在能够有效地使用联结前,必须了解关系表以及关系型数据库设计的一些基础知识。
通过主键和外键来建立表之间的联系,维护表间引用的完整性。
外键相当于一个指针,指向另一个表的主键。
1、关系表
假如有一个包含产品目录的数据库表,其中每种类别的物品占一行。对于每种物品要存储的信息包括产品描述和价格,以及生产该产品的供应商信息。现在,假如有同一供应商生产的多种物品,那么在何处存储供应商信息(如,供应商名、地址、联系方法等)呢?将这些数据与产品信息分开存储的理由如下:
- 因为同一供应商生产的每个产品的供应商信息都是相同的,对每个产品重复此信息浪费时间和存储空间
- 如果供应商信息发生改变(供应商搬家或电话号码变动),只需改动一次即可。
- 如果有重复数据(即每种产品都存储供应商信息),很难保证每次输入该数据的方式都相同。不一致的数据在报表中很难利用。
关键是,相同数据出现多次决不是一件好事,此因素是关系数据库设计的基础。关系表的设计就是要保证把信息分解成多个表,一类数据一个表。各表通过某些常用的值(即关系设计中的关系(relational))互相关联。
在这个例子中,可建立两个表,一个 存储供应商信息,另一个存储产品信息。vendors表包含所有供应商的信息,每个供应商占一行,每个供应商具有唯一标识。此标识称为主键(primary key),可以是供应商ID或任何其他唯一值。products表只存储产品信息,它除了存储供应商ID(vendors表中的外键)外不存储其他供应商信息。vendors表的主键又叫做products表的外键,它将vendors表和products表关联,利用供应商ID能从vendors表中找出供应商的详细信息。
外键(foreign key):外键为某个表中的一列,它包含另一个表的主键值,定义了两个表之间的关系。
这样做(关系表、设外键)的好处如下供应商信息不重复,从而不浪费时间和空间。如果供应商信息变动,可以只更新vendors表中的单个记录,相关表中的数据不用改动。由于数据无重复,显然数据是一致的,这使得处理数据更简单。
关系数据可以有效地存储和方便地处理。因此,关系数据库的可伸缩性远比非关系数据库要好。
可伸缩性(scale):能够适应不断增加的工作量而不失败。设计良好的数据库或应用程序称之为可伸缩性好
2、为什么要使用联结
分解数据为多个表能更有效地存储,更方便的处理,并且具有很大地可伸缩性。然而紧接着就有问题出现了,如果数据存储在多个表中,怎样用单条SELECT语句检索出数据,这时候就需要使用联结了。
联结是一种机制,用来在一条SELECT语句中关联表,因此称为联结。
使用特殊的语法,可以联结多个表返回一组输出,联结在运行时关联表中正确的行。
维护引用完整性:要理解联结不是物理实体,它在实际的数据库表中并不存在,联结由MySQL根据需要建立,它存在于查询的执行当中。在使用关系表时,仅在关系列中插入合法的数据非常重要。如果在products表中处入拥有非法供应商ID(即没有在vendors表中出现)的供应商生产的产品,则这些产品是不可访问的,因为它们没有关联到某个供应商。为防止这种情况的发生,可指示MySQL只允许在products表的供应商ID列中出现合法值。这就是维护引用完整性,它是通过在表的定义中指定主键和外键来实现的。
二、创建联结
联结的创建需要规定要联结的所有表以及它们如何关联即可。
1、笛卡儿积
笛卡尔积(cartesian product): 由没有联结条件的表关系返回的结果称为笛卡儿积,检索出的行的数目将是一个表中的行数乘以第二个表中的行数。
笛卡尔积在SQL中的实现方式既是交叉连接(Cross Join)。所有连接方式都会先生成临时笛卡尔积表,笛卡尔积是关系代数里的一个概念,表示两个表中的每一行数据任意组合。
在一条SELECT语句中联结几个表时,相应的关系是在运行构造的。在数据库表的定义中是不存在能指示MySQL如何对表进行联结的东西,在联结两个表时,实际上做的是将第一个表中的每一行和第二个表中的每一行配对。WHERE子句作为过滤条件,它只包含那些匹配给定的条件。没有WHERE子句则将会全部匹配。
MariaDB [course]> SELECT vend_name,prod_name,prod_price
-> FROM vendors,products
-> ORDER BY vend_name,prod_name;
+----------------+----------------+------------+
| vend_name | prod_name | prod_price |
+----------------+----------------+------------+
| ACME | .5 ton anvil | 5.99 |
| ACME | 1 ton anvil | 9.99 |
| ACME | 2 ton anvil | 14.99 |
| ACME | Bird seed | 10.00 |
| ACME | Carrots | 2.50 |
| ACME | Detonator | 13.00 |
| ACME | Fuses | 3.42 |
| ACME | JetPack 1000 | 35.00 |
| ACME | JetPack 2000 | 55.00 |
| ACME | Oil can | 8.99 |
| 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 |
| Anvils R Us | Bird seed | 10.00 |
| Anvils R Us | Carrots | 2.50 |
| Anvils R Us | Detonator | 13.00 |
| Anvils R Us | Fuses | 3.42 |
| Anvils R Us | JetPack 1000 | 35.00 |
| Anvils R Us | JetPack 2000 | 55.00 |
| Anvils R Us | Oil can | 8.99 |
| Anvils R Us | Safe | 50.00 |
| Anvils R Us | Sling | 4.49 |
| Anvils R Us | TNT (1 stick) | 2.50 |
| Anvils R Us | TNT (5 sticks) | 10.00 |
| Furball Inc. | .5 ton anvil | 5.99 |
| Furball Inc. | 1 ton anvil | 9.99 |
| Furball Inc. | 2 ton anvil | 14.99 |
| Furball Inc. | Bird seed | 10.00 |
| Furball Inc. | Carrots | 2.50 |
| Furball Inc. | Detonator | 13.00 |
| Furball Inc. | Fuses | 3.42 |
| Furball Inc. | JetPack 1000 | 35.00 |
| Furball Inc. | JetPack 2000 | 55.00 |
| Furball Inc. | Oil can | 8.99 |
| Furball Inc. | Safe | 50.00 |
| Furball Inc. | Sling | 4.49 |
| Furball Inc. | TNT (1 stick) | 2.50 |
| Furball Inc. | TNT (5 sticks) | 10.00 |
| Jet Set | .5 ton anvil | 5.99 |
| Jet Set | 1 ton anvil | 9.99 |
| Jet Set | 2 ton anvil | 14.99 |
| Jet Set | Bird seed | 10.00 |
| Jet Set | Carrots | 2.50 |
| Jet Set | Detonator | 13.00 |
| Jet Set | Fuses | 3.42 |
| Jet Set | JetPack 1000 | 35.00 |
| Jet Set | JetPack 2000 | 55.00 |
| Jet Set | Oil can | 8.99 |
| Jet Set | Safe | 50.00 |
| Jet Set | Sling | 4.49 |
| Jet Set | TNT (1 stick) | 2.50 |
| Jet Set | TNT (5 sticks) | 10.00 |
| Jouets Et Ours | .5 ton anvil | 5.99 |
| Jouets Et Ours | 1 ton anvil | 9.99 |
| Jouets Et Ours | 2 ton anvil | 14.99 |
| Jouets Et Ours | Bird seed | 10.00 |
| Jouets Et Ours | Carrots | 2.50 |
| Jouets Et Ours | Detonator | 13.00 |
| Jouets Et Ours | Fuses | 3.42 |
| Jouets Et Ours | JetPack 1000 | 35.00 |
| Jouets Et Ours | JetPack 2000 | 55.00 |
| Jouets Et Ours | Oil can | 8.99 |
| Jouets Et Ours | Safe | 50.00 |
| Jouets Et Ours | Sling | 4.49 |
| Jouets Et Ours | TNT (1 stick) | 2.50 |
| Jouets Et Ours | TNT (5 sticks) | 10.00 |
| LT Supplies | .5 ton anvil | 5.99 |
| LT Supplies | 1 ton anvil | 9.99 |
| LT Supplies | 2 ton anvil | 14.99 |
| LT Supplies | Bird seed | 10.00 |
| LT Supplies | Carrots | 2.50 |
| LT Supplies | Detonator | 13.00 |
| LT Supplies | Fuses | 3.42 |
| LT Supplies | JetPack 1000 | 35.00 |
| LT Supplies | JetPack 2000 | 55.00 |
| LT Supplies | Oil can | 8.99 |
| LT Supplies | Safe | 50.00 |
| LT Supplies | Sling | 4.49 |
| LT Supplies | TNT (1 stick) | 2.50 |
| LT Supplies | TNT (5 sticks) | 10.00 |
+----------------+----------------+------------+
84 rows in set (0.00 sec)
上例就是将两个表的笛卡儿积进行了输出,但是并不是我们需要的结果 。
不要忘了WHERE子句 :应该保证所有了联结都有WHERE子句,否则MySQL将返回错误的结果。
2、内联结
内联结也叫等值联结(equaljoin)它是基于两个表之间的相等测试。
MariaDB [course]> 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 |
+-------------+----------------+------------+
这里需要这种完全限定列名,可以看到要匹配的两个列以vendors.vend_id
和products.vend_id
指定。
完全限定列名:在引用的列出现二义性时,必须使用完全限定列名(数据表名.列名)。
MariaDB [course]> 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;
+-------------+----------------+------------+
| 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.00 sec)
该语句的两个表之间的关系是FROM子句
的组成部分,以INNER JOIN
指定。在使用这种语法时,联结条件用特定的ON子句
而不是WHERE子句给出,传递给ON的实际条件与传递给WHERE的相同。
3、联结多个表
SQL对一条SELECT语句中可以联结的表的数目没有限制,创建的基本规则也相同。
MariaDB [course]> SELECT vend_name,prod_name,prod_price,quantity
FROM products INNER JOIN orderitems ON products.prod_id = orderitems.prod_id
INNER JOIN vendors ON products.vend_id = vendors.vend_id
AND order_num=20005;
+-------------+----------------+------------+----------+
| vend_name | prod_name | prod_price | quantity |
+-------------+----------------+------------+----------+
| Anvils R Us | .5 ton anvil | 5.99 | 10 |
| Anvils R Us | 1 ton anvil | 9.99 | 3 |
| ACME | TNT (5 sticks) | 10.00 | 5 |
| ACME | Bird seed | 10.00 | 1 |
+-------------+----------------+------------+----------+
4 rows in set (0.01 sec)
该例子是显示编号为20005的订单中的物品,这里的FROM子句列出了3个表,而WHERE子句定义了 这两个联结条件,而第三个联结条件是指定订单20005中的物品。
MySQL在运行时关联指定的每个表以处理联结,这种处理可能是非常消耗资源的。联结的表越多,性能下降的越厉害。
再看一个例子,返回订购产品TNT2的 客户列表:
MariaDB [course]> SELECT cust_name,cust_contact
-> FROM customers INNER JOIN orders ON customers.cust_id = orders.cust_id
-> INNER JOIN orderitems ON orderitems.order_num = orders.order_num
-> AND prod_id='TNT2';
+----------------+--------------+
| cust_name | cust_contact |
+----------------+--------------+
| Coyote Inc. | Y Lee |
| Yosemite Place | Y Sam |
+----------------+--------------+
2 rows in set (0.00 sec)
三、高级联结
1、自联结
自联结:就是自己联结自己的操作。
现在有一个需求,你发现某物品存在问题, 想要知道同供应商的其他物品是否也存在物品。所以,需要先找到生产ID为DTNTR的物品的生产商,然后再找出该供应商生产的其他产品。
先使用子查询:
MariaDB [course]> SELECT prod_id,prod_name
-> FROM products
-> WHERE vend_id = (SELECT vend_id
-> FROM products
-> WHERE prod_id = 'DTNTR');
+---------+----------------+
| prod_id | prod_name |
+---------+----------------+
| DTNTR | Detonator |
| FB | Bird seed |
| FC | Carrots |
| SAFE | Safe |
| SLING | Sling |
| TNT1 | TNT (1 stick) |
| TNT2 | TNT (5 sticks) |
+---------+----------------+
7 rows in set (0.00 sec)
现在使用自联结查询:
MariaDB [course]> SELECT p1.prod_id,p1.prod_name
-> FROM products AS p1,products AS p2
-> WHERE p1.vend_id = p2.vend_id
-> AND p2.prod_id = 'DTNTR';
+---------+----------------+
| prod_id | prod_name |
+---------+----------------+
| DTNTR | Detonator |
| FB | Bird seed |
| FC | Carrots |
| SAFE | Safe |
| SLING | Sling |
| TNT1 | TNT (1 stick) |
| TNT2 | TNT (5 sticks) |
+---------+----------------+
7 rows in set (0.00 sec)
此查询中需要的两个表其实是相同的表,因此为了避免二义性使用了别名。如果不这样做,将返回错误。
用自联结而不是子查询:自联结通常作为外部语句用来的替代从相同表中检索数据时使用的子查询语句,有时候处理联结远比处理子查询的速度快很多。
2、自然联结
无论何时对表进行联结,应该至少有一列不止出现在一个表中(联结的列),标准联结返回所有数据,相同的列甚至多次出现。自然联结排除多次出现,使每一列只返回一次。
一般是通过对表使用通配符(SELECT *),对其他表的列使用明确的子集来完成的。
MariaDB [course]> 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='FB';
在该例中,通配符只对第一个表使用,所有其他列明确列出,所以没有重复的列被检索出来。
3、外部联结
许多联结将一个表中的行与另一个表中的行相关联,但有时候会需要包含没有相关联的行。
外联结:联结包含了那些在相关表中没有关联的行
先看一个内联结,它展示所有的顾客及其订单:
MariaDB [course]> SELECT customers.cust_id,orders.order_num
-> FROM orders INNER JOIN customers
-> ON customers.cust_id = orders.cust_id;
+---------+-----------+
| cust_id | order_num |
+---------+-----------+
| 10001 | 20005 |
| 10001 | 20009 |
| 10003 | 20006 |
| 10004 | 20007 |
| 10005 | 20008 |
+---------+-----------+
5 rows in set (0.00 sec)
看一个例子,对每个顾客下进行计数,包括哪些至今尚未下订单的顾客。
MariaDB [course]> 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 |
+---------+-----------+
6 rows in set (0.00 sec)
该例中customers表相当于集合图中的B,orders表相当于A。
上述例子用右外联结表示:
MariaDB [course]> SELECT customers.cust_id,orders.order_num
-> FROM orders RIGHT OUTER JOIN customers
-> ON customers.cust_id = orders.cust_id;
+---------+-----------+
| cust_id | order_num |
+---------+-----------+
| 10001 | 20005 |
| 10001 | 20009 |
| 10002 | NULL |
| 10003 | 20006 |
| 10004 | 20007 |
| 10005 | 20008 |
+---------+-----------+
6 rows in set (0.00 sec)
左外联结和右外联结之间的差别是,所关联的表的顺序不同。换句话说,左外联结可通过颠倒FROM子句中表的顺序转换为右外部联结。因此,两种类型的联结可以互换使用。
4、使用聚集函数的联结
聚集函数总是用来汇总数据的
检索所有顾客以及每个顾客所下的订单数
MariaDB [course]> SELECT customers.cust_name,
-> 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;
+----------------+---------+---------+
| cust_name | cust_id | num_ord |
+----------------+---------+---------+
| Coyote Inc. | 10001 | 2 |
| Wascals | 10003 | 1 |
| Yosemite Place | 10004 | 1 |
| E Fudd | 10005 | 1 |
+----------------+---------+---------+
聚集函数也可以方便地与其他联结一起使用:
MariaDB [course]> SELECT customers.cust_name,
-> 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;
+----------------+---------+---------+
| 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 |
+----------------+---------+---------+
5 rows in set (0.01 sec)
使用左外联结来包含所有客户,即使那些美欧订单客户。