引言
雖然為了效能和一致性,將資料分離到離散的表中通常很有用,但您常常需要查閱多個表中的資料以滿足某些請求。*連線*表是一種透過基於共同欄位值匹配每個記錄來組合來自不同表資料的方式。
有幾種不同型別的連線(join),它們提供了多種組合表記錄的方式。在本文中,我們將介紹 MySQL 如何實現連線,並討論每種連線在何種場景下最有用。
什麼是連線(Join)?
簡而言之,連線(join)是一種顯示來自多個表資料的方式。它們透過基於某些列中的匹配值,將來自不同源的記錄拼接在一起。每個結果行由第一個表中的記錄與第二個表中的行組合而成,基於每個表中的一個或多個列具有相同的值。
連線的基本語法如下所示
SELECT*FROM<first_table><join_type> <second_table><join_condition>;
在連線中,每個結果行都是透過包含第一個表的所有列,然後是第二個表的所有列來構建的。查詢的 SELECT 部分可用於指定您希望顯示的精確列。
如果用於比較的列中的值不唯一,則可能會從原始表中構建出多行。例如,假設您有一個來自第一個表的列,其中有兩個記錄的值為“red”。與之匹配的是第二個表中的一個列,該列有三行具有該值。連線將為該值生成六個不同的行,代表可以實現的不同組合。
連線的型別和連線條件決定了如何構建顯示出的每一行。這會影響到每個表中符合和不符合連線條件的行會發生什麼。
為了方便,許多連線將一個表的主鍵與第二個表上的關聯外部索引鍵進行匹配。儘管主鍵和外部索引鍵僅由資料庫系統用於維護一致性保證,但它們之間的關係通常使它們成為連線條件的良好候選。
不同型別的連線(Join)
有各種型別的連線可用,每種連線都可能產生不同的結果。瞭解每種連線的構建方式將幫助您確定哪種連線適合不同的場景。
內連線和交叉連線
預設的連線型別稱為內連線(inner join)。在 MySQL 中,這可以透過使用 INNER JOIN、僅 JOIN 或 CROSS JOIN 來指定。對於其他資料庫系統,INNER JOIN 和 CROSS JOIN 通常是兩個獨立的概念,但 MySQL 在相同的結構中實現了它們。
這是一個典型的例子,展示了內連線的語法
SELECT*FROMtable_1[INNER] JOIN table_2ON table_1.id = table_2.table_1_id;
內連線是最嚴格的連線型別,因為它只顯示透過組合每個表中的行而建立的行。任何在構成表中沒有匹配對應項的行都將從結果中移除。例如,如果第一個表中比較列的值為“blue”,而第二個表中沒有具有該值的記錄,則該行將被從輸出中抑制。
如果您將結果表示為組成表的維恩圖,內連線允許您表示兩個圓的重疊區域。只存在於其中一個表中的值都不會顯示。
如上所述,MySQL 也使用這種格式來生成交叉連線。在 MySQL 中,您可以使用不帶任何匹配條件的內連線來生成交叉連線。交叉連線不使用任何比較來確定每個表中的行是否相互匹配。相反,結果是透過簡單地將第一個表中的每一行新增到第二個表的每一行來構建的。
這會生成兩個或多個表中行的笛卡爾積。實際上,這種連線方式是無條件地組合每個表中的行。因此,如果每個表有三行,則結果表將有九行,包含來自兩個表的所有列。
例如,如果您有一個名為 t1 的表與一個名為 t2 的表組合,每個表都有行 r1、r2 和 r3,則結果將是九行,組合方式如下所示
t1.r1 + t2.r1t1.r1 + t2.r2t1.r1 + t2.r3t1.r2 + t2.r1t1.r2 + t2.r2t1.r2 + t2.r3t1.r3 + t2.r1t1.r3 + t2.r2t1.r3 + t2.r3
左連線
左連線(left join)是一種顯示內連線中所有記錄,外加第一個表中所有不匹配行的連線。在 MySQL 中,這可以指定為 LEFT OUTER JOIN 或僅 LEFT JOIN。
左連線的基本語法遵循以下模式
SELECT*FROMtable_1LEFT JOIN table_2ON table_1.id = table_2.table_1_id;
左連線的構建方式是:首先執行內連線以從兩個表中的所有匹配記錄構建行。然後,第一個表中不匹配的記錄也會被包含進來。由於連線中的每一行都包含兩個表的列,因此不匹配的列會使用 NULL 作為第二個表中所有列的值。
如果您將結果表示為組成表的維恩圖,左連線允許您表示整個左圓。左圓中由兩個圓交集表示的部分將包含由右表補充的額外資料。
右連線
右連線(right join)是一種顯示內連線中所有記錄,外加第二個表中所有不匹配行的連線。在 MySQL 中,這可以指定為 RIGHT OUTER JOIN 或僅 RIGHT JOIN。
右連線的基本語法遵循以下模式
SELECT*FROMtable_1RIGHT JOIN table_2ON table_1.id = table_2.table_1_id;
右連線的構建方式是:首先執行內連線以從兩個表中的所有匹配記錄構建行。然後,第二個表中不匹配的記錄也會被包含進來。由於連線中的每一行都包含兩個表的列,因此不匹配的列會使用 NULL 作為第一個表中所有列的值。
如果您將結果表示為組成表的維恩圖,右連線允許您表示整個右圓。右圓中由兩個圓交集表示的部分將包含由左表補充的額外資料。
出於可移植性原因,MySQL 建議您儘可能使用左連線而不是右連線。
全連線
全連線(full join)是一種顯示內連線中所有記錄,外加兩個組成表中所有不匹配行的連線。MySQL 不原生實現全連線,但我們可以透過一些技巧來模擬其行為。
為了複製全外連線的結果,我們將對兩個表共享的所有結果以及左表所有不匹配的行執行左連線。然後我們將使用 UNION ALL 集合運算子將這些結果與右表的“反連線”組合起來。“反連線”是一種專門查詢表之間不共有結果的連線操作。
全連線的基本語法遵循以下模式
( SELECT*FROMtable_1LEFT JOIN table_2ON table_1.id = table_2.table_1_id)UNION ALL( SELECT*FROMtable_1RIGHT JOIN table_2ON table_1.id = table_2.table_1_idWHERE table_1.id IS NULL);
由於連線中的每一行都包含兩個表的列,因此不匹配的列會使用 NULL 作為不匹配的另一個表中所有列的值。
如果您將結果表示為組成表的維恩圖,全連線允許您完全表示兩個組成圓。兩個圓的交集將包含由每個組成表提供的值。圓中重疊區域之外的部分將包含它們所屬表中的值,並使用 NULL 填充在另一個表中找到的列。
自連線
自連線是任何將一個表的行與自身組合的連線。這可能不會立即看出其用途,但它實際上有許多常見的應用。
通常,表描述的實體之間可能相互扮演多種角色。例如,如果您有一個 people 表,每行都可能包含一個 mother 列,該列引用表中的其他 people。自連線將允許您透過將表的第二個例項連線到第一個例項(其中這些值匹配)來將這些不同的行拼接在一起。
由於自連線會兩次引用同一張表,因此需要表別名來消除引用歧義。例如,在上面的例子中,您可以使用別名 people AS children 和 people AS mothers 來連線 people 表的兩個例項。這樣,在定義連線條件時,您可以指定所引用的表例項。
這是另一個例子,這次表示員工和經理之間的關係
SELECT*FROMpeople AS employeeJOIN people AS managerON employee.manager_id = manager.id;
連線條件
組合表時,連線條件決定了行將如何匹配在一起以形成複合結果。基本前提是定義每個表中必須匹配的列,以便在該行上發生連線。
ON 子句
定義表連線條件最標準的方式是使用 ON 子句。 ON 子句使用等號來指定每個表中將進行比較的精確列,以確定何時可以發生連線。MySQL 使用提供的列將每個表中的行拼接在一起。
ON 子句是可用連線條件中最冗長的,但也是最靈活的。它允許無論組合的每個表的列名有多標準化,都能進行精確指定。
ON 子句的基本語法如下所示
SELECT*FROMtable1JOINtable2ONtable1.id = table2.ident;
這裡,當 table1 中的 id 列與 table2 中的 ident 列匹配時,table1 和 table2 的行將被連線。由於使用了內連線,結果將只顯示已連線的行。由於查詢使用了萬用字元 * 字元,因此將顯示來自兩個表的所有列。
這意味著 table1 中的 id 列和 table2 中的 ident 列都將顯示,儘管它們因滿足連線條件而具有完全相同的值。您可以透過在 SELECT 列列表中指定要顯示的精確列來避免這種重複。
USING 子句
USING 子句是指定 ON 子句條件的一種簡寫形式,當要比較的列在兩個表中具有相同的名稱時可以使用。 USING 子句接受一個用括號括起來的列表,其中包含應進行比較的共享列名。
USING 子句的通用語法使用以下格式
SELECT*FROMtable1JOINtable2USING(id, state);
當兩個表共有的兩列(id 和 state)各自具有匹配值時,此連線將 table1 與 table2 組合起來。
同樣的連線可以使用 ON 更冗長地表達,如下所示
SELECT*FROMtable1JOINtable2ONtable1.id = table2.id AND table1.state = table2.state;
雖然上述兩種連線都會構建出相同資料的結果行,但它們的顯示方式會略有不同。 ON 子句包含兩個表的所有列,而 USING 子句會抑制重複列。因此,結果中不會有兩列獨立的 id 和兩列獨立的 state(每個表各一列),而是隻有共享列的各一個例項,然後是 table1 和 table2 提供的所有其他列。
NATURAL 子句
NATURAL 子句是另一種簡寫形式,可以進一步減少 USING 子句的冗長。 NATURAL 連線不指定任何要匹配的列。相反,MySQL 將根據每個資料庫中具有匹配列的所有列自動連線表。
NATURAL 連線子句的通用語法如下所示
SELECT*FROMtable1NATURAL JOINtable2;
假設 table1 和 table2 都包含名為 id、state 和 company 的列,則上述查詢等效於使用 ON 子句的此查詢
SELECT*FROMtable1JOINtable2ONtable1.id = table2.id AND table1.state = table2.state AND table1.company = table2.company;
以及使用 USING 子句的此查詢
SELECT*FROMtable1JOINtable2USING(id, state, company);
與 USING 子句類似,NATURAL 子句會抑制重複列,因此結果中每個連線列將只出現一個例項。
雖然 NATURAL 子句可以減少查詢的冗長,但在使用時必須小心。因為用於連線表的列是自動計算的,如果組成表中的列發生變化,由於新的連線條件,結果可能會大相徑庭。
連線條件與 WHERE 子句
連線條件與使用 WHERE 子句過濾資料行所用的比較有很多共同之處。這兩種結構都定義了必須評估為真才能考慮該行的表示式。因此,在 WHERE 結構中包含額外比較與在連線子句本身中定義它們之間的區別並不總是那麼直觀。
為了理解結果差異,我們必須檢視 MySQL 處理查詢不同部分的順序。在這種情況下,首先處理連線條件中的謂詞,以在記憶體中構建虛擬連線表。在此階段之後,評估 WHERE 子句中的表示式以過濾結果行。
例如,假設我們有兩個表:customers 和 orders,需要將它們連線起來。我們希望透過匹配 customers.id 列與 orders.customer_id 列來連線這兩個表。此外,我們對 orders 表中 product_id 為 12345 的行感興趣。
鑑於上述要求,我們有兩個關心條件。然而,我們表達這些條件的方式將決定我們得到的結果。
首先,讓我們將兩者都用作 LEFT JOIN 的連線條件
SELECTcustomers.id AS customers_id,customers.name,orders.id AS orders_id,orders.product_idFROMcustomersLEFT JOINordersONcustomers.id = orders.customers_id AND orders.product_id = 12345;
結果可能看起來像這樣
+--------------+----------+-----------+------------+customers_id | name | orders_id | product_id |+--------------+----------+-----------+------------+20 | Early Co | NULL | NULL |320 | Other Co | 680 | 12345 |4380 | Acme Co | 182 | 12345 |4380 | Acme Co | 480 | 12345 |8033 | Big Co | NULL | NULL |+--------------+----------+-----------+------------+5 rows in set (0.00 sec)
MySQL 透過執行以下操作得到了此結果
- 將
customers表中的所有行與orders表合併,其中customers.id與orders.customers_id匹配。orders.product_id匹配 12345
- 因為我們使用的是左連線,所以包含左表(
customers)中所有不匹配的行,並用NULL值填充右表(orders)中的列。 - 僅顯示
SELECT列規範中列出的列。
結果是所有連線的行都符合我們正在尋找的兩個條件。然而,左連線導致 MySQL 也包含來自第一個表中不滿足連線條件的任何行。這導致了“剩餘”行,這些行似乎不符合查詢的明顯意圖。
如果我們將第二個查詢 (orders.product_id = 12345) 移到 WHERE 子句中,而不是將其作為連線條件包含,我們會得到不同的結果
SELECTcustomers.id AS customers_id,customers.name,orders.id AS orders_id,orders.product_idFROMcustomersLEFT JOINordersONcustomers.id = orders.customers_idWHEREorders.product_id = 12345;
這次只顯示三行
+--------------+----------+-----------+------------+customers_id | name | orders_id | product_id |+--------------+----------+-----------+------------+4380 | Acme Co | 182 | 12345 |4380 | Acme Co | 480 | 12345 |320 | Other Co | 680 | 12345 |+--------------+----------+-----------+------------+3 rows in set (0.00 sec)
比較執行的順序是造成這些差異的原因。這次,MySQL 這樣處理查詢:
- 將
customers表中的所有行與orders表合併,其中customers.id與orders.customers_id匹配。 - 因為我們使用的是左連線,所以包含左表(
customers)中所有不匹配的行,並用NULL值填充右表(orders)中的列。 - 評估
WHERE子句,刪除orders.product_id列值不為 12345 的所有行。 - 僅顯示
SELECT列規範中列出的列。
這次,即使我們使用的是左連線,WHERE 子句也透過過濾掉所有沒有正確 product_id 的行來截斷結果。因為任何不匹配的行其 product_id 都將被設定為 NULL,所以這會移除所有由左連線填充的不匹配行。它還會移除任何由連線條件匹配但未透過第二輪檢查的行。
瞭解 MySQL 執行查詢的基本過程可以幫助您在處理資料時避免一些容易犯但難以除錯的錯誤。
總結
在本文中,我們討論了什麼是連線以及 MySQL 如何將它們作為組合多個表記錄的方式來實現。我們介紹了可用的不同型別的連線,以及 ON 和 WHERE 子句等不同條件如何影響資料庫構建結果的方式。
隨著您對連線越來越熟悉,您將能夠將它們作為工具包的常規部分,從各種來源拉取資料並將資訊片段拼接在一起,以建立更全面的檢視。連線有助於將因組織原則和效能考量而可能分離的資料彙集起來。學習如何有效使用連線可以幫助您彙集資料,無論其在系統中如何組織。
