SQL 最強大的功能之一,莫過於在數據檢索過程中實現表聯結。關係型資料庫的設計初衷,就是將資訊分解存儲於多張表中,每張表專注於一類數據,再透過外鍵建立關聯。外鍵是某張表中的一列,其值引用另一張表的主鍵,從而定義兩表間的邏輯關係。良好的資料庫設計還需具備可伸縮性,即能在不斷增長的工作負載下穩定運行。
聯結是一種機制,允許在單條 SELECT 語句中關聯多張表。透過特定語法,MySQL 能在運行時正確匹配各表中的行,返回合併後的結果集。
最傳統的寫法是在 FROM 子句列出多張表,並在 WHERE 子句指定連接條件:
SELECT vend_name, prod_name, prod_price
FROM vendors, products
WHERE vendors.vend_id = products.vend_id
ORDER BY vend_name, prod_name;
關鍵提醒:務必包含 WHERE 子句,否則會產生笛卡爾積(交叉聯結),返回海量無意義數據。當列名在多表中重複時,必須使用「表名.列名」的完全限定名消除歧義。
基於相等測試的聯結稱為等值聯結,即內部聯結。ANSI SQL-92 標準推薦使用顯式 INNER JOIN ... ON 語法:
SELECT vend_name, prod_name, prod_price
FROM vendors INNER JOIN products
ON vendors.vend_id = products.vend_id;
此寫法將聯結條件從 WHERE 移至 ON 子句,語義更清晰,也便於後續添加篩選條件。
實際業務常需聯結 3 張以上的表。例如查詢訂購產品 'TNT2' 的客戶:
SELECT cust_name, cust_contact
FROM customers, orders, orderitems
WHERE customers.cust_id = orders.cust_id
AND orderitems.order_num = orders.order_num
AND prod_id = 'TNT2';
這相當於第 14 章介紹的子查詢功能,但聯結通常效能更優。
為表指定簡短別名,不僅縮短 SQL,更是自聯結的前提:
SELECT cust_name, cust_contact
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 = 'TNT2';
別名可用於 SELECT 列表、ORDER BY 等所有子句。
同一張表以不同別名出現兩次,解決「同表關聯」需求。例如找出與 DTNTR 同供應商的其他產品:
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';
排除重複列,使每列僅返回一次。事實上,我們手寫的內部聯結若明確列出所需列而非 SELECT *,本質上就是自然聯結。
內部聯結只返回匹配成功的行;外部聯結還會保留無匹配行。以 LEFT OUTER JOIN 為例:
SELECT customers.cust_id, orders.order_num
FROM customers LEFT OUTER JOIN orders
ON customers.cust_id = orders.cust_id;
結果會包含所有客戶,無訂單者的 order_num 顯示 NULL。RIGHT OUTER JOIN 則保留右表所有行。必須指定 LEFT 或 RIGHT 關鍵字。
統計每位客戶的訂單數:
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;
內部聯結會自動排除無訂單客戶;若需保留零訂單客戶,改用 LEFT OUTER JOIN。
組合查詢(Union/Compound Query)將多條 SELECT 結果垂直合併。適用場景:
SELECT vend_id, prod_id, prod_price FROM products WHERE prod_price <= 5
UNION
SELECT vend_id, prod_id, prod_price FROM products WHERE vend_id IN (1001,1002);
UNION 預設去重;若需保留重複行,使用 UNION ALL。
SELECT 組成,以 UNION 分隔;ORDER BY 僅能出現在最後一條SELECT 之後,對整個合併結果生效。
MySQL 內建全文搜索引擎,需先在目標列建立 FULLTEXT 索引,再用 MATCH() ... AGAINST() 執行搜索。
SELECT note_text
FROM productnotes
WHERE MATCH(note_text) AGAINST('rabbit');
結果按相關性等級降序排列。等級由詞頻、唯一詞數、索引總詞數、包含該詞的文檔數等因子計算得出。可在 SELECT 列表顯式輸出等級:
SELECT note_text, MATCH(note_text) AGAINST('rabbit') AS rank
FROM productnotes;
啟用 WITH QUERY EXPANSION 後,MySQL 執行兩輪掃描:先按原條件搜索,再從命中行提取「有用詞」進行二次搜索,顯著擴大召回率。
SELECT note_text
FROM productnotes
WHERE MATCH(note_text) AGAINST('anvils' WITH QUERY EXPANSION);
無需 FULLTEXT 索引即可使用,支援豐富操作符:
| 操作符 | 含義 |
|---|---|
+ | 必須包含 |
- | 必須排除 |
> | 提高權重 |
< | 降低權重 |
() | 分組 |
~ | 取消排序值 |
* | 通配符(僅尾部) |
"" | 精確短語 |
-- 必須同時包含 rabbit 和 bait
SELECT note_text FROM productnotes
WHERE MATCH(note_text) AGAINST('+rabbit +bait' IN BOOLEAN MODE);
-- 排除含 rope* 的行
SELECT note_text FROM productnotes
WHERE MATCH(note_text) AGAINST('heavy -rope*' IN BOOLEAN MODE);
INSERT INTO customers
(cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact, cust_email)
VALUES ('Pep E. Lapew', '100 main Street', 'Los Angeles', 'CA', '90046', 'USA', NULL, NULL);
INSERT INTO customers
(cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country)
VALUES
('Pep E. Lapew', '100 main Street', 'Los Angeles', 'CA', '90046', 'USA'),
('M. Martian', '42 Galaxy Way', 'New York', 'NY', '11213', 'USA');
INSERT INTO customers
(cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country)
SELECT cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country
FROM custnew;
注意:列順序必須一一對應,而非靠列名匹配。
兩種模式:全表更新(無 WHERE)與條件更新(帶 WHERE)。三要素:目標表、列與新值、篩選條件。
UPDATE customers
SET cust_name = 'The Fudds', cust_email = 'elmer@fudd.com'
WHERE cust_id = 10005;
執行後會返回 Rows matched: 1 Changed: 1 Warnings: 0,便於確認影響範圍。
同樣分全表刪除與條件刪除。危險操作:無 WHERE 的 DELETE 會清空整表,生產環境務必加條件或先備份。