1. 程式人生 > SQL入門教學 >實戰4:如何使用中間表

實戰4:如何使用中間表

1. 前言

外來鍵一節中,我們介紹了外來鍵的基本使用,並在末尾中給出了下面這句話:

外來鍵是體現資料表關係的核心功能點,但主流的外來鍵方式卻都是弱外來鍵

不知道你是否會有些許疑惑,弱外來鍵是什麼?強外來鍵又是什麼?它與本節的中間表又有什麼關係?

帶著這些疑惑,我們一起來開始本小節的學習。

2. 弱外來鍵與強外來鍵

2.1 強外來鍵的缺點

外來鍵一節中,我們介紹到外來鍵可以通過如下的方式來建立:

FOREIGN KEY (user_id) REFERENCES imooc_user(id)

通過宣告方式,資料庫會自主將兩張表做外來鍵關聯,我們把這樣的外來鍵稱為強外來鍵。強外來鍵最大的特點就是資料庫層面支援,資料庫會自動維護外來鍵關聯的表。

但是也正是因為這個特性,強外來鍵不夠靈活,舉個例子來說,當你刪除某張表的資料時,如果另一張表有此表的外來鍵,那麼刪除可能會被拒絕,當然你可以通過級聯來同時刪除另一張表中關聯的資料。如下,我們新建兩張存在外來鍵關聯的表:

DROP TABLE IF EXISTS imooc_user;
CREATE TABLE imooc_user
(
  id int PRIMARY KEY,
  username varchar(20),
  age int
);
DROP TABLE IF EXISTS imooc_user_score;
CREATE TABLE imooc_user_score
(
  id int
PRIMARY KEY, user_id int NOT NULL, score int, FOREIGN KEY (user_id) REFERENCES imooc_user(id) ); INSERT INTO imooc_user(id,username,age) VALUES (1,'pedro',23); INSERT INTO imooc_user_score(id,user_id,score) VALUES (1,1,9);

建立成功後,我們通過 Delete 來刪除使用者pedro

DELETE FROM imooc_user WHERE id = 1; 

資料庫提示我們刪除失敗,並給出瞭如下錯誤資訊:

(1451, 'Cannot delete or update a parent row: a foreign key constraint fails (`imooc`.`imooc_user_score`, CONSTRAINT `imooc_user_score_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `imooc_user` (`id`))')

資料庫告訴我們idimooc_user_score表的外來鍵,如果刪除會破壞資料的完整性,因此拒絕了我們的操作。

我們改造一下外來鍵約束,讓它支援級聯刪除:

ALTER TABLE imooc_user_score DROP FOREIGN KEY imooc_user_score_ibfk_1;
ALTER TABLE imooc_user_score ADD CONSTRAINT imooc_user_score_ibfk_1 FOREIGN KEY(user_id) REFERENCES imooc_user(id) ON DELETE CASCADE;

我們再次刪除pedro

DELETE FROM imooc_user WHERE id = 1; 

這次刪除成功,且imooc_user_score中的關聯資料也被刪除了。

強外來鍵雖然能夠保證資料的完整性(要麼都有,要麼都沒有),但是弊端很明顯,刪除了一些資料後,與之關聯的資料也都被刪除了,不利於資料的維護,也不利於更改和遷移;再者,強外來鍵會因為關聯來同步檢測和更新兩張表,無疑會拉低資料庫整體的效能。因此目前大家普遍採用弱外來鍵的方式。

2.2 什麼是弱外來鍵

join 一節中,我們提到外來鍵的最終落腳點是使用 Join 來連線資料,不過 SQL 連線並非只支援強外來鍵,它其實也支援弱外來鍵,甚至無外來鍵,只要連線的欄位能夠對應上,連線都是可行的。

那麼什麼是弱外來鍵了?答案其實很簡單,強外來鍵是資料庫層面上的外來鍵,而弱外來鍵是邏輯層面的上的外來鍵。如下,我們新建兩表:

CREATE TABLE imooc_user
(
  id int PRIMARY KEY,
  username varchar(20),
  age int
);
CREATE TABLE imooc_user_score
(
  id int PRIMARY KEY,
  user_id int NOT NULL,
  score int,
);

在新建 imooc_user_score 表的 SQL 語句中,我們並未宣告user_id是外來鍵,但是在邏輯層面上我們認為它就是外來鍵,在連線的時候知道其對應關係就行了。

3. 中間表

聊完了外來鍵,我們來介紹本節的重點——中間表。先引入一個場景,有兩張資料表,分別是imooc_user(使用者表)和imooc_class(課程表),對於使用者來說,他(她)可以購買多門課程,而對於課程來說,它也可以被多個使用者購買。這樣就產生了一個難題,使用者與課程之間是典型的多對多關係,因此我們需要另一張表(imooc_user_class)來記錄使用者與課程之間的購買關係。

類似於imooc_user_class這樣的關係表,我們稱之為中間表。對於它們三者,我們可以這樣設計(省略諸多欄位資訊):

DROP TABLE IF EXISTS imooc_user;
CREATE TABLE imooc_user
(
  id int PRIMARY KEY,
  username varchar(20),
  age int
);
DROP TABLE IF EXISTS imooc_class;
CREATE TABLE imooc_class
(
  id int PRIMARY KEY,
  name varchar(50),
  description varchar(100)
);
DROP TABLE IF EXISTS imooc_user_class;
CREATE TABLE imooc_user_class
(
  id int PRIMARY KEY,
  user_id int NOT NULL,
  class_id int NOT NULL
);

imooc_user_class表的結構上看,它的主體其實就是一些外來鍵的組合。這也是中間表與外來鍵的關係。

它們之間的關係如下圖所示:
圖片描述

4. 實踐

接下來,我們以實戰的角度來看imooc_user(使用者表)和imooc_class(課程表)以及關係表imooc_user_class

首先,我們新增幾條使用者和課程記錄:

INSERT INTO imooc_user(id,username,age) VALUES (1,'pedro',23),(2,'tom',19),(3,'mary',22);
INSERT INTO imooc_class(id,name,description) VALUES 
(1,'SQL知多少', '一卷囊括天下SQL事'),
(2,'回首又見Java','你驀然回首時,我依然在燈火闌珊處'),
(3,'倚Python屠蟲記', '看我這把Python大刀斬盡你無數爬蟲');

4.1 使用弱外來鍵

接著,我們來模擬使用者購買課程。

某一天,pedro購買了SQL知多少回首又見Java這兩門課,有了中間表,我們無需改動主表,而是新增記錄至中間表即可:

INSERT INTO imooc_user_class VALUES(1,1,1), (2,1,2);

第二天,mary購買了SQL知多少倚Python屠蟲記兩門課:

INSERT INTO imooc_user_class VALUES(3,3,1), (4,3,3);

現在,管理員需要檢視資料。首先,他想知道誰都購買了課程,由於購買記錄都記載在了imooc_user_class表中,我們只需要查詢它即可(一個人可能購買多門課程,所以需要 Distinct 去重):

SELECT DISTINCT user_id FROM imooc_user_class;
+---------+
| user_id |
+---------+
| 1       |
| 3       |
+---------+

光有user_id可不行,我們需要知道使用者名稱,於是連線一下imooc_user即可:

SELECT DISTINCT user_id,username FROM imooc_user_class LEFT JOIN imooc_user ON imooc_user_class.user_id = imooc_user.id;
+---------+----------+
| user_id | username |
+---------+----------+
| 1       | pedro    |
| 3       | mary     |
+---------+----------+

我們發現,pedromary都購買了課程,這與上述一致。

管理員還想知道,哪些課程被購買了:

SELECT DISTINCT class_id,name FROM imooc_user_class LEFT JOIN imooc_class ON imooc_user_class.class_id = imooc_class.id;
+----------+----------------+
| class_id | name           |
+----------+----------------+
| 1        | SQL知多少       |
| 2        | 回首又見Java    |
| 3        | 倚Python屠蟲記  |
+----------+----------------+

不錯,三門課都被購買了。管理員更想知道SQL知多少這門課被誰購買了:

SELECT DISTINCT user_id,username FROM imooc_user_class LEFT JOIN imooc_user ON imooc_user_class.user_id = imooc_user.id WHERE imooc_user_class.class_id = 1;
+---------+----------+
| user_id | username |
+---------+----------+
| 1       | pedro    |
| 3       | mary     |
+---------+----------+

不錯,大家都買了這門課?。

4.2 弱外來鍵總結

可以看到,中間表的存在讓資料的查詢變得更為方便和有效了。當然你也可以選擇不要中間表,而在兩張主表中各自新增對方外來鍵的方式來達到同樣的效果,不過這樣的方式顯然不推薦。

5. 小結

  • 業務開發中的很大一部分業務都是通過中間表來實現的,請務必熟練掌握和理解它。
  • 連線操作是驅動中間表資料的核心操作,如果你還不夠熟悉連線,可以再次閱讀連線小節,並著手操練一番。