CategoryResourceRepost/极客时间专栏/geek/MySQL 必知必会/实践篇/06 | 外键和连接:如何做关联查询?.md
louzefeng bf99793fd0 del
2024-07-09 18:38:56 +00:00

21 KiB
Raw Blame History

你好,我是朱晓峰。今天我来和你聊一聊关联查询的问题。

在实际的数据库应用开发过程中我们经常需要把2个或2个以上的表进行关联以获取需要的数据。这是因为为了提高存取效率我们会把不同业务模块的信息分别存放在不同的表里面。但是从业务层面上看我们需要完整全面的信息为经营决策提供数据支撑。

就拿咱们的超市项目来说,数据库里面的销售流水表一般只保存销售必需的信息(比如商品编号、数量、价格、金额和会员卡号等)。但是,在呈现给超市经营者的统计报表里面,只包括这些信息是不够的,比如商品编号、会员卡号,这些数字经营者就看不懂。因此,必须要从商品信息表提取出商品信息,从会员表中提取出会员的相关信息,这样才能形成一个完整的报表。这种把分散在多个不同的表里的数据查询出来的操作,就是多表查询

不过,这种查询可不简单,我们需要建立起多个表之间的关联,然后才能去查询,同时还需要规避关联表查询中的常见错误。具体怎么做呢?我来借助实际的项目给你讲一讲。

在我们项目的进货模块有这样2个数据表分别是进货单头表importhead和进货单明细表importdetails我们每天都要对这两个表进行增删改查的操作。

进货单头表记录的是整个进货单的总体信息:

进货单明细表记录了每次进货的商品明细信息。一条进货单头数据记录,对应多条进货商品的明细数据,也就是所谓的一对多的关系。具体信息如下表所示:

现在我们需要查询一次进货的所有相关数据包括进货单的总体信息和进货商品的明细这样一来我们就需要把2个表关联起来那么该怎么操作呢

在MySQL中为了把2个表关联起来会用到2个重要的功能外键FOREIGN KEY和连接JOIN。外键需要在创建表的阶段就定义连接可以通过相同意义的字段把2个表连接起来用在查询阶段。

接下来,我就先和你聊聊外键。

如何创建外键?

我先来解释一下什么是外键。

假设我们有2个表分别是表A和表B它们通过一个公共字段“id”发生关联关系我们把这个关联关系叫做R。如果“id”在表A中是主键那么表A就是这个关系R中的主表。相应的表B就是这个关系中的从表表B中的“id”就是表B用来引用表A中数据的叫外键。所以外键就是从表中用来引用主表中数据的那个公共字段

为了方便你理解,我画了一张图来展示:

如图所示在关联关系R中公众字段字段A是表A的主键所以表A是主表表B是从表。表B中的公共字段字段A是外键。

在MySQL中外键是通过外键约束来定义的。外键约束就是约束的一种它必须在从表中定义包括指明哪个是外键字段以及外键字段所引用的主表中的主键字段是什么。MySQL系统会根据外键约束的定义监控对主表中数据的删除操作。如果发现要删除的主表记录正在被从表中某条记录的外键字段所引用MySQL就会提示错误从而确保了关联数据不会缺失。

外键约束可以在创建表的时候定义,也可以通过修改表来定义。我们先来看看外键约束定义的语法结构:

[CONSTRAINT <外键约束名称>] FOREIGN KEY 字段名
REFERENCES <主表名> 字段名

你可以在创建表的时候定义外键约束:

CREATE TABLE 从表名
(
  字段名 类型,
  ...
-- 定义外键约束,指出外键字段和参照的主表字段
CONSTRAINT 外键约束名
FOREIGN KEY (字段名) REFERENCES 主表名 (字段名)
)

当然,你也可以通过修改表来定义外键约束:

ALTER TABLE 从表名 ADD CONSTRAINT 约束名 FOREIGN KEY 字段名 REFERENCES 主表名 (字段名);

一般情况下,表与表的关联都是提前设计好了的,因此,会在创建表的时候就把外键约束定义好。不过,如果需要修改表的设计(比如添加新的字段,增加新的关联关系),但没有预先定义外键约束,那么,就要用修改表的方式来补充定义。

下面,我就来讲一讲怎么创建外键约束。

先创建主表demo.importhead

CREATE TABLE demo.importhead (
    listnumber INT PRIMARY KEY,
    supplierid INT,
    stocknumber INT,
    importtype INT,
    importquantity DECIMAL(10 , 3 ),
    importvalue DECIMAL(10 , 2 ),
    recorder INT,
    recordingdate DATETIME
);

然后创建从表demo.importdetails并且给它定义外键约束

CREATE TABLE demo.importdetails
(
	listnumber INT,
	itemnumber INT,
	quantity DECIMAL(10,3),
	importprice DECIMAL(10,2),
	importvalue DECIMAL(10,2),
	-- 定义外键约束,指出外键字段和参照的主表字段
	CONSTRAINT fk_importdetails_importhead
	FOREIGN KEY (listnumber) REFERENCES importhead (listnumber)
);

运行这个SQL语句我们就在创建表的同时定义了一个名字叫“fk_importdetails_importhead”的外键约束。同时我们声明这个外键约束的字段“listnumber”引用的是表importhead里面的字段“listnumber”。

外键约束创建好之后我们可以通过Workbench来查看外键约束是不是创建成功了

当然我们也可以通过SQL语句来查看这里我们要用到MySQL自带的、用于存储系统信息的数据库information_schema。我们可以查看外键约束的相关信息

mysql> SELECT
    ->     constraint_name, -- 表示外键约束名称
    ->     table_name, -- 表示外键约束所属数据表的名称
    ->     column_name, -- 表示外键约束的字段名称
    ->     referenced_table_name, -- 表示外键约束所参照的数据表名称
    ->     referenced_column_name -- 表示外键约束所参照的字段名称
    -> FROM
    ->     information_schema.KEY_COLUMN_USAGE
    -> WHERE
    ->     constraint_name = 'fk_importdetails_importhead';
+-----------------------------+---------------+-------------+-----------------------+------------------------+
| CONSTRAINT_NAME             | TABLE_NAME    | COLUMN_NAME | REFERENCED_TABLE_NAME | REFERENCED_COLUMN_NAME |
+-----------------------------+---------------+-------------+-----------------------+------------------------+
| fk_importdetails_importhead | importdetails | listnumber  | importhead            | listnumber             |
+-----------------------------+---------------+-------------+-----------------------+------------------------+
1 row in set (0.05 sec)

通过查询我们可以看到外键约束所在的表是“importdetails”外键字段是“listnumber”参照的主表是“importhead”参照的主表字段是“listnumber”。这样通过定义外键约束我们已经建立起了2个表之间的关联关系。

关联关系建立起来之后,如何才能获取我们需要的数据呢?这时,我们就需要用到连接查询了。

连接

在MySQL中有2种类型的连接分别是内连接INNER JOIN和外连接OUTER JOIN

  • 内连接表示查询结果只返回符合连接条件的记录,这种连接方式比较常用;
  • 外连接则不同,表示查询结果返回某一个表中的所有记录,以及另一个表中满足连接条件的记录。

下面我先来讲一下内连接。

在MySQL里面关键字JOIN、INNER JOIN、CROSS JOIN的含义是一样的都表示内连接。我们可以通过JOIN把两个表关联起来来查询两个表中的数据。

我借助一个小例子,来帮助你理解。

咱们的项目中有会员销售的需求,所以,我们的流水表中的数据记录,既包括非会员的普通销售,又包括会员销售。它们的区别是,会员销售的数据记录包括会员编号,而在非会员销售的数据记录中,会员编号为空。

来看一下项目中的销售表demo.trans)。实际的销售表比较复杂为了方便你理解我把表进行了简化并且假设业务字段cardno是会员信息表的主键。简化以后的结构如下所示

再看下简化后的会员信息表demo.membermaster

这两个表之间存在关联关系表demo.trans中的字段“cardno”是这个关联关系中的外键。

我们可以通过内连接,查询所有会员销售的流水记录:

mysql> SELECT
    ->     a.transactionno,
    ->     a.itemnumber,
    ->     a.quantity,
    ->     a.price,
    ->     a.transdate,
    ->     b.membername
    -> FROM
    ->     demo.trans AS a
    ->         JOIN
    ->     demo.membermaster AS b ON (a.cardno = b.cardno);
+---------------+------------+----------+-------+---------------------+------------+
| transactionno | itemnumber | quantity | price | transdate           | membername |
+---------------+------------+----------+-------+---------------------+------------+
|             1 |          1 |    1.000 | 89.00 | 2020-12-01 00:00:00 | 张三       |
+---------------+------------+----------+-------+---------------------+------------+
1 row in set (0.00 sec)

可以看到我们通过公共字段“cardno”把两个表关联到了一起查询出了会员消费的数据。

在这里关键字JOIN与关键字ON配对使用意思是查询满足关联条件“demo.trans表中cardno的值与demo.membermaster表中的cardno值相等”的两个表中的所有记录。

知道了内连接,我们再来学习下外连接。跟内连接只返回符合连接条件的记录不同的是,外连接还可以返回表中的所有记录,它包括两类,分别是左连接和右连接。

  • 左连接一般简写成LEFT JOIN返回左边表中的所有记录以及右表中符合连接条件的记录。
  • 右连接一般简写成RIGHT JOIN返回右边表中的所有记录以及左表中符合连接条件的记录。

当我们需要查询全部流水信息的时候,就会用到外连接,代码如下:

SELECT 
    a.transactionno,
    a.itemnumber,
    a.quantity,
    a.price,
    a.transdate,
    b.membername
FROM demo.trans AS a
LEFT JOIN demo.membermaster AS b -- LEFT JOIN以demo.transaction为主
ON (a.cardno = b.cardno);

可以看到我用到了LEFT JOIN意思是以表demo.trans中的数据记录为主这个表中的数据记录要全部出现在结果集中同时给出符合连接条件a.cardno=b.cardno)的表demo.membermaster中的字段membername的值。

我们也可以使用RIGHT JOIN实现同样的效果代码如下

SELECT 
    a.transactionno,
    a.itemnumber,
    a.quantity,
    a.price,
    a.transdate,
    b.membername
FROM
    demo.membermaster AS b
        RIGHT JOIN
    demo.trans AS a ON (a.cardno = b.cardno); -- RIGHT JOIN顺序颠倒了还是以demo.trans为主

其实,这里就是把顺序颠倒了一下,意思是一样的。运行之后,我们都能得到下面的结果:

mysql> SELECT
    ->     a.transactionno,
    ->     a.itemnumber,
    ->     a.quantity,
    ->     a.price,
    ->     a.transdate,
    ->     b.membername
    -> FROM
    ->     demo.trans AS a
    ->         LEFT JOIN   -- 左连接
    ->     demo.membermaster AS b ON (a.cardno = b.cardno);
+---------------+------------+----------+-------+---------------------+------------+
| transactionno | itemnumber | quantity | price | transdate           | membername |
+---------------+------------+----------+-------+---------------------+------------+
|             1 |          1 |    1.000 | 89.00 | 2020-12-01 00:00:00 | 张三       |
|             2 |          2 |    1.000 | 12.00 | 2020-12-02 00:00:00 | NULL       |
+---------------+------------+----------+-------+---------------------+------------+
2 rows in set (0.00 sec)

mysql> SELECT
    ->     a.transactionno,
    ->     a.itemnumber,
    ->     a.quantity,
    ->     a.price,
    ->     a.transdate,
    ->     b.membername
    -> FROM
    ->     demo.membermaster AS b
    ->         RIGHT JOIN   -- 右连接
    ->     demo.trans AS a
    ->     ON (a.cardno = b.cardno);
+---------------+------------+----------+-------+---------------------+------------+
| transactionno | itemnumber | quantity | price | transdate           | membername |
+---------------+------------+----------+-------+---------------------+------------+
|             1 |          1 |    1.000 | 89.00 | 2020-12-01 00:00:00 | 张三       |
|             2 |          2 |    1.000 | 12.00 | 2020-12-02 00:00:00 | NULL       |
+---------------+------------+----------+-------+---------------------+------------+
2 rows in set (0.00 sec)

通过关联查询,销售流水数据里就补齐了会员的名称,我们也就获取到了需要的数据。

关联查询的误区

有了连接我们就可以进行2个表的关联查询了。你可能会有疑问关联查询必须在外键约束的基础上才可以吗

其实在MySQL中外键约束不是关联查询的必要条件。很多人往往在设计表的时候觉得只要连接查询就可以搞定一切了外键约束太麻烦没有必要。如果你这么想就进入了一个误区。

下面我就以超市进货的例子,来实际说明一下,为什么这种思路不对。

假设一次进货数据是这样的供货商编号是1进货仓库编号是1。我们进货的商品编号是1234进货数量是1进货价格是10进货金额是10。

我先插入单头数据:

INSERT INTO demo.importhead
(
listnumber,
supplierid,
stocknumber
)
VALUES
(
1234,
1,
1
);

运行成功后,查看一下表的内容:

mysql> SELECT *
    -> FROM demo.importhead;
+------------+------------+-------------+------------+----------+-------------+-------------+
| listnumber | supplierid | stocknumber | importtype | quantity | importprice | importvalue |
+------------+------------+-------------+------------+----------+-------------+-------------+
|       1234 |          1 |           1 |          1 |     NULL |        NULL |        NULL |
+------------+------------+-------------+------------+----------+-------------+-------------+
1 row in set (0.00 sec)

可以看到我们有了一个进货单头单号是1234供货商是1号供货商进货仓库是1号仓库。

接着,我们向进货单明细表中插入进货明细数据:

INSERT INTO demo.importdetails
(
listnumber,
itemnumber,
quantity,
importprice,
importvalue
)
VALUES
(
1234,
1,
1,
10,
10
);

运行成功,查看一下表的内容:

mysql> SELECT *
    -> FROM demo.importdetails;
+------------+------------+----------+-------------+-------------+
| listnumber | itemnumber | quantity | importprice | importvalue |
+------------+------------+----------+-------------+-------------+
|       1234 |          1 |    1.000 |       10.00 |       10.00 |
+------------+------------+----------+-------------+-------------+
1 row in set (0.00 sec)

这样我们就有了1234号进货单的明细数据进货商品是1号商品进货数量是1个进货价格是10元进货金额是10元。

这个时候,如果我删除进货单头表的数据,就会出现只有明细、没有单头的数据缺失情况。我们来看看会发生什么:

DELETE FROM demo.importhead
WHERE listnumbere = 1234;

运行这条语句MySQL会提示错误因为数据删除违反了外键约束。看到了吗MySQL阻止了数据不一致的情况出现。

不知道你有没有注意我插入数据的顺序为什么我要先插入进货单头表的数据再插入进货单明细表的数据呢其实这是因为如果我先插入数据到从表也就是进货单明细表会导致MySQL找不到参照的主表信息会提示错误因为添加数据违反了外键约束。

你可能会不以为然,觉得按照信息系统的操作逻辑,生成一张进货单的时候,一定是先生成单头,再插入明细。同样,删除一张进货单的时候,一定是先删除明细,再删除单头。

要是你这么想可能就会“中招”了。原因很简单既然我们把进货数据拆成了2个表这就决定了无论是数据添加还是数据删除都不能通过一条SQL语句实现。实际工作中什么突发情况都是有可能发生的。你认为一定会完成的操作完全有可能只执行了一部分。

我们曾经就遇到过这么一个问题:用户月底盘点的时候,盘点单无法生成,系统提示“有未处理的进货单”。经过排查,发现是进货单数据发生了数据缺失,明细数据还在,对应的单头数据却被删除了。我们反复排查之后,才发现是缺少了防止数据缺失的机制。最后通过定义外键约束,解决了这个问题。

所以虽然你不用外键约束也可以进行关联查询但是有了它MySQL系统才会保护你的数据避免出现误删的情况从而提高系统整体的可靠性。

现在来回答另外一个问题为什么在MySQL里没有外键约束也可以进行关联查询呢原因是外键约束是有成本的需要消耗系统资源。对于大并发的SQL操作有可能会不适合。比如大型网站的中央数据库可能会因为外键约束的系统开销而变得非常慢。所以MySQL允许你不使用系统自带的外键约束在应用层面完成检查数据一致性的逻辑。也就是说即使你不用外键约束也要想办法通过应用层面的附加逻辑来实现外键约束的功能确保数据的一致性。

总结

这节课,我给你介绍了如何进行多表查询,我们重点学习了外键和连接。

外键约束可以帮助我们确定从表中的外键字段与主表中的主键字段之间的引用关系还可以确保从表中数据所引用的主表数据不会被删除从而保证了2个表中数据的一致性。

连接可以帮助我们对2个相关的表进行连接查询从2个表中获取需要的信息。左连接表示连接以左边的表为主结果集中要包括左边表中的所有记录右连接表示连接以右边的表为主结果集中要包括右边表中的所有记录。

我汇总了常用的SQL语句你一定要重点掌握。

-- 定义外键约束:
CREATE TABLE 从表名
(
字段 字段类型
....
CONSTRAINT 外键约束名称
FOREIGN KEY (字段名) REFERENCES 主表名 (字段名称)
);
ALTER TABLE 从表名 ADD CONSTRAINT 约束名 FOREIGN KEY 字段名 REFERENCES 主表名 (字段名);

-- 连接查询
SELECT 字段名
FROM 表名 AS a
JOIN 表名 AS b
ON (a.字段名称=b.字段名称);
 
SELECT 字段名
FROM 表名 AS a
LEFT JOIN 表名 AS b
ON (a.字段名称=b.字段名称);
 
SELECT 字段名
FROM 表名 AS a
RIGHT JOIN 表名 AS b
ON (a.字段名称=b.字段名称);

刚开始学习MySQL的同学很容易忽略在关联表中定义外键约束的重要性从而导致数据缺失影响系统的可靠性。我建议你尽量养成在关联表中定义外键约束的习惯。不过如果你的业务场景因为高并发等原因无法承担外键约束的成本也可以不定义外键约束但是一定要在应用层面实现外键约束的逻辑功能这样才能确保系统的正确可靠。

思考题

如果你的业务场景因高并发等原因,不能使用外键约束,在这种情况下,你怎么在应用层面确保数据的一致性呢?

欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,欢迎你把它分享给你的朋友或同事,我们下节课见。