《MySQL 基礎篇》四:查詢操作

ACatSmiling發表於2024-09-22

Author: ACatSmiling

Since: 2024-09-19

多表查詢

多表查詢,也稱為關聯查詢,指兩個或更多個表一起完成查詢操作。

前提條件:這些一起查詢的表之間是有關係的(一對一、一對多),它們之間一定是有關聯欄位,這個關聯欄位可能建立了外來鍵,也可能沒有建立外來鍵。比如:員工表和部門表,這兩個表依靠 "部門編號" 進行關聯。

笛卡爾積

笛卡爾乘積是一個數學運算。假設有兩個集合 X 和 Y,那麼 X 和 Y 的笛卡爾積就是 X 和 Y 的所有可能組合,也就是第一個物件來自於 X,第二個物件來自於 Y 的所有可能。組合的個數即為兩個集合中元素個數的乘積數。

image-20230404224214892

SQL-92 中,笛卡爾積也稱為交叉連線 ,英文是 CROSS JOIN 。在 SQL-99 中也是使用 CROSS JOIN 表示交叉連線,它的作用就是可以把任意表進行連線,即使這兩張表不相關。在 MySQL 中如下情況會出現笛卡爾積:

# 查詢員工姓名和所在部門名稱
mysql> SELECT last_name, department_name FROM employees, departments;
+-------------+----------------------+
| last_name   | department_name      |
+-------------+----------------------+
| King        | Payroll              |
| King        | Recruiting           |
| King        | Retail Sales         |
| King        | Government Sales     |
| King        | IT Helpdesk          |
| King        | NOC                  |
| King        | IT Support           |
| King        | Operations           |
| King        | Contracting          |
| King        | Construction         |
| King        | Manufacturing        |
| King        | Benefits             |
| King        | Shareholder Services |
| King        | Control And Credit   |
| King        | Corporate Tax        |
| King        | Treasury             |
| King        | Accounting           |
| King        | Finance              |
| King        | Executive            |
| King        | Sales                |
| King        | Public Relations     |
| King        | IT                   |
| King        | Shipping             |
| King        | Human Resources      |
| King        | Purchasing           |
| King        | Marketing            |
| King        | Administration       |
| Kochhar     | Payroll              |
| ......                             |
+-------------+----------------------+
2889 rows in set (0.00 sec)

mysql> SELECT last_name, department_name FROM employees CROSS JOIN departments;
+-------------+----------------------+
| last_name   | department_name      |
+-------------+----------------------+
| King        | Payroll              |
| King        | Recruiting           |
| King        | Retail Sales         |
| King        | Government Sales     |
| King        | IT Helpdesk          |
| King        | NOC                  |
| King        | IT Support           |
| King        | Operations           |
| King        | Contracting          |
| King        | Construction         |
| King        | Manufacturing        |
| King        | Benefits             |
| King        | Shareholder Services |
| King        | Control And Credit   |
| King        | Corporate Tax        |
| King        | Treasury             |
| King        | Accounting           |
| King        | Finance              |
| King        | Executive            |
| King        | Sales                |
| King        | Public Relations     |
| King        | IT                   |
| King        | Shipping             |
| King        | Human Resources      |
| King        | Purchasing           |
| King        | Marketing            |
| King        | Administration       |
| Kochhar     | Payroll              |
| ......                             |
+-------------+----------------------+
2889 rows in set (0.00 sec)

mysql> SELECT last_name, department_name FROM employees INNER JOIN departments;
+-------------+----------------------+
| last_name   | department_name      |
+-------------+----------------------+
| King        | Payroll              |
| King        | Recruiting           |
| King        | Retail Sales         |
| King        | Government Sales     |
| King        | IT Helpdesk          |
| King        | NOC                  |
| King        | IT Support           |
| King        | Operations           |
| King        | Contracting          |
| King        | Construction         |
| King        | Manufacturing        |
| King        | Benefits             |
| King        | Shareholder Services |
| King        | Control And Credit   |
| King        | Corporate Tax        |
| King        | Treasury             |
| King        | Accounting           |
| King        | Finance              |
| King        | Executive            |
| King        | Sales                |
| King        | Public Relations     |
| King        | IT                   |
| King        | Shipping             |
| King        | Human Resources      |
| King        | Purchasing           |
| King        | Marketing            |
| King        | Administration       |
| Kochhar     | Payroll              |
| ......                             |
+-------------+----------------------+
2889 rows in set (0.00 sec)

mysql> SELECT last_name, department_name FROM employees JOIN departments;
+-------------+----------------------+
| last_name   | department_name      |
+-------------+----------------------+
| King        | Payroll              |
| King        | Recruiting           |
| King        | Retail Sales         |
| King        | Government Sales     |
| King        | IT Helpdesk          |
| King        | NOC                  |
| King        | IT Support           |
| King        | Operations           |
| King        | Contracting          |
| King        | Construction         |
| King        | Manufacturing        |
| King        | Benefits             |
| King        | Shareholder Services |
| King        | Control And Credit   |
| King        | Corporate Tax        |
| King        | Treasury             |
| King        | Accounting           |
| King        | Finance              |
| King        | Executive            |
| King        | Sales                |
| King        | Public Relations     |
| King        | IT                   |
| King        | Shipping             |
| King        | Human Resources      |
| King        | Purchasing           |
| King        | Marketing            |
| King        | Administration       |
| Kochhar     | Payroll              |
| ......                             |
+-------------+----------------------+
2889 rows in set (0.00 sec)

上述查詢結果,稱為笛卡爾積的錯誤,原因:

mysql> SELECT COUNT(employee_id) FROM employees;
+--------------------+
| COUNT(employee_id) |
+--------------------+
|                107 |
+--------------------+
1 row in set (0.00 sec)

mysql> SELECT COUNT(department_id) FROM departments;
+----------------------+
| COUNT(department_id) |
+----------------------+
|                   27 |
+----------------------+
1 row in set (0.00 sec)

mysql> SELECT 107 * 27 FROM dual;
+----------+
| 107 * 27 |
+----------+
|     2889 |
+----------+
1 row in set (0.00 sec)

笛卡爾積的錯誤會在下面條件下產生:

  • 省略多個表的連線條件(或關聯條件)。
  • 連線條件(或關聯條件)無效。
  • 所有表中的所有行互相連線。

為了避免笛卡爾積, 可以在 WHERE 加入有效的連線條件。加入連線條件後,查詢語法:

SELECT table1.column, table2.column
FROM table1, table2
WHERE table1.column1 = table2.column2; # 連線條件

連線 n 個表,至少需要 n - 1 個連線條件。

查詢員工的姓名及其部門名稱,正確的寫法:

mysql> SELECT last_name, department_name FROM employees, departments WHERE employees.department_id = departments.department_id;
+-------------+------------------+
| last_name   | department_name  |
+-------------+------------------+
| Whalen      | Administration   |
| Hartstein   | Marketing        |
| Fay         | Marketing        |
| Raphaely    | Purchasing       |
+-------------+------------------+
106 rows in set (0.00 sec)

在不同表中有相同欄位時,需要在欄位前加上所屬表名的字首。多表查詢時,建議對每一個查詢欄位,都新增上所屬表名的字首,這樣一是避免相同欄位不明確,二是可以對查詢進行最佳化,以提高查詢效率。

需要注意的是,如果使用了表的別名,在查詢欄位中、過濾條件中就只能使用別名進行代替,不能使用原有的表名,否則就會報錯,這涉及到 SQL 語句不同分段的順序執行問題。

阿里開發規範 :
【 強制 】對於資料庫中表記錄的查詢和變更,只要涉及多個表,都需要在列名前加表的別名(或表名)進行限定。

說明 :對多表進行查詢記錄、更新記錄、刪除記錄時,如果對操作列沒有限定表的別名(或表名),並且操作列在多個表中存在時,就會拋異常。

正例 :SELECT t1.name FROM table_first AS t1, table_second AS t2 WHERE t1.id = t2.id;

反例 :在某業務中,由於多表關聯查詢語句沒有加表的別名(或表名)的限制,正常執行兩年後,最近在某個表中增加一個同名欄位,在預釋出環境做資料庫變更後,線上查詢語句出現出 1052 異常:Column 'name' in field list is ambiguous。

多表查詢的分類

分類 1:等值連線 vs 非等值連線

等值連線

image-20230404230245804

mysql> SELECT employees.employee_id, employees.last_name, employees.department_id, departments.department_id, departments.location_id FROM employees, departments WHERE employees.department_id = departments.department_id;
+-------------+-------------+---------------+---------------+-------------+
| employee_id | last_name   | department_id | department_id | location_id |
+-------------+-------------+---------------+---------------+-------------+
|         103 | Hunold      |            60 |            60 |        1400 |
|         104 | Ernst       |            60 |            60 |        1400 |
|         105 | Austin      |            60 |            60 |        1400 |
|         106 | Pataballa   |            60 |            60 |        1400 |
|         107 | Lorentz     |            60 |            60 |        1400 |
+-------------+-------------+---------------+---------------+-------------+
106 rows in set (0.00 sec)
非等值連線

image-20230408220652672

mysql> SELECT e.last_name, e.salary, j.grade_level FROM employees e, job_grades j WHERE e.salary BETWEEN j.lowest_sal AND j.highest_sal;
+-------------+----------+-------------+
| last_name   | salary   | grade_level |
+-------------+----------+-------------+
| King        | 24000.00 | E           |
| Kochhar     | 17000.00 | E           |
| De Haan     | 17000.00 | E           |
| Hunold      |  9000.00 | C           |
| Ernst       |  6000.00 | C           |
| Austin      |  4800.00 | B           |
| Pataballa   |  4800.00 | B           |
| Lorentz     |  4200.00 | B           |
| Greenberg   | 12000.00 | D           |
| Gietz       |  8300.00 | C           |
+-------------+----------+-------------+
107 rows in set (0.00 sec)

分類 2:自連線 vs 非自連線

image-20230408221044800

table1 和 table2 本質上是同一張表,只是用取別名的方式虛擬成兩張表以代表不同的意義,然後兩個表再進行內連線、外連線等查詢。

mysql> SELECT CONCAT(worker.last_name, ' works for ', manager.last_name) FROM employees worker, employees manager WHERE worker.manager_id = manager.employee_id;
+------------------------------------------------------------+
| CONCAT(worker.last_name, ' works for ', manager.last_name) |
+------------------------------------------------------------+
| Kochhar works for King                                     |
| De Haan works for King                                     |
| Hunold works for De Haan                                   |
| Ernst works for Hunold                                     |
| Austin works for Hunold                                    |
| Pataballa works for Hunold                                 |
| Lorentz works for Hunold                                   |
| Gietz works for Higgins                                    |
+------------------------------------------------------------+
106 rows in set (0.00 sec)

分類 3:內連線 vs 外連線

image-20230408221549457

內連線:合併具有同一列的兩個以上的表的行,結果集中不包含一個表與另一個表不匹配的行

外連線:兩個表在連線過程中除了返回滿足連線條件的行以外,還返回左(或右)表中不滿足條件的行 ,這種連線稱為左(或右) 外連線。沒有匹配的行時,結果表中相應的列為空(NULL)。

  • 如果是左外連線,則連線條件中左邊的表也稱為主表,右邊的表稱為從表

  • 如果是右外連線,則連線條件中右邊的表也稱為主表 ,左邊的表稱為從表

SQL-99 語法實現多表查詢

基本語法

使用JOIN...ON子句建立連線的語法結構:

SELECT table1.column, table2.column,table3.column
FROM table1
JOIN table2 ON table1 和 table2 的連線條件
JOIN table3 ON table2 和 table3 的連線條件;

語法說明:

  • 可以使用 ON 子句指定額外的連線條件。
  • 這個連線條件是與其它條件分開的。
  • ON 子句使語句具有更高的易讀性。
  • 關鍵字 JOIN、INNER JOIN、CROSS JOIN 的含義是一樣的,都表示內連線。

它的巢狀邏輯類似我們使用的 FOR 迴圈:

for t1 in table1:
	for t2 in table2:
		if condition1:
			for t3 in table3:
				if condition2:
					output t1 + t2 + t3

SQL-99 採用的這種巢狀結構非常清爽、層次性更強、可讀性更強,即使再多的表進行連線也都清晰可見。如果採用 SQL-92,可讀性就會大打折扣。

內連線(INNER JOIN)的實現

語法:

SELECT 欄位列表
FROM A表 INNER JOIN B表
ON 關聯條件
WHERE 等其他子句;
mysql> SELECT e.employee_id, e.last_name, e.department_id, d.department_id, d.location_id FROM employees e JOIN departments d ON (e.department_id = d.department_id);
+-------------+-------------+---------------+---------------+-------------+
| employee_id | last_name   | department_id | department_id | location_id |
+-------------+-------------+---------------+---------------+-------------+
|         103 | Hunold      |            60 |            60 |        1400 |
|         104 | Ernst       |            60 |            60 |        1400 |
|         105 | Austin      |            60 |            60 |        1400 |
|         106 | Pataballa   |            60 |            60 |        1400 |
|         107 | Lorentz     |            60 |            60 |        1400 |
|         204 | Baer        |            70 |            70 |        2700 |
+-------------+-------------+---------------+---------------+-------------+
106 rows in set (0.00 sec)

mysql> SELECT employee_id, city, department_name FROM employees e JOIN departments d ON d.department_id = e.department_id JOIN locations l ON d.location_id = l.location_id;
+-------------+---------------------+------------------+
| employee_id | city                | department_name  |
+-------------+---------------------+------------------+
|         200 | Seattle             | Administration   |
|         201 | Toronto             | Marketing        |
|         202 | Toronto             | Marketing        |
|         114 | Seattle             | Purchasing       |
|         115 | Seattle             | Purchasing       |
|         116 | Seattle             | Purchasing       |
|         117 | Seattle             | Purchasing       |
|         118 | Seattle             | Purchasing       |
|         119 | Seattle             | Purchasing       |
|         203 | London              | Human Resources  |
|         120 | South San Francisco | Shipping         |
|         206 | Seattle             | Accounting       |
+-------------+---------------------+------------------+
106 rows in set (0.00 sec)

外連線(OUTER JOIN)的實現

左外連線(LEFT OUTER JOIN)

語法:

# 實現查詢結果是 A
SELECT 欄位列表
FROM A表 LEFT JOIN B表
ON 關聯條件
WHERE 等其他子句;
mysql> SELECT e.last_name, e.department_id, d.department_name FROM employees e LEFT OUTER JOIN departments d ON (e.department_id = d.department_id);
+-------------+---------------+------------------+
| last_name   | department_id | department_name  |
+-------------+---------------+------------------+
| King        |            90 | Executive        |
| Kochhar     |            90 | Executive        |
| De Haan     |            90 | Executive        |
| Hunold      |            60 | IT               |
| Ernst       |            60 | IT               |
| Livingston  |            80 | Sales            |
| Grant       |          NULL | NULL             |
| Johnson     |            80 | Sales            |
| Gietz       |           110 | Accounting       |
+-------------+---------------+------------------+
107 rows in set (0.00 sec)

內連線查詢結果是 106 條記錄,左外連線是 107 條記錄,有一個員工不存在部門資訊,內連線的時候,不會顯示這個員工的資訊。

右外連線(RIGHT OUTER JOIN)

語法:

# 實現查詢結果是 B
SELECT 欄位列表
FROM A表 RIGHT JOIN B表
ON 關聯條件
WHERE 等其他子句;
mysql> SELECT e.last_name, e.department_id, d.department_name FROM employees e RIGHT OUTER JOIN departments d ON (e.department_id = d.department_id);
+-------------+---------------+----------------------+
| last_name   | department_id | department_name      |
+-------------+---------------+----------------------+
| Whalen      |            10 | Administration       |
| Hartstein   |            20 | Marketing            |
| Fay         |            20 | Marketing            |
| Raphaely    |            30 | Purchasing           |
| Khoo        |            30 | Purchasing           |
| Baida       |            30 | Purchasing           |
| Tobias      |            30 | Purchasing           |
| Himuro      |            30 | Purchasing           |
| NULL        |          NULL | IT Support           |
| NULL        |          NULL | NOC                  |
| NULL        |          NULL | IT Helpdesk          |
| NULL        |          NULL | Government Sales     |
| NULL        |          NULL | Retail Sales         |
| NULL        |          NULL | Recruiting           |
| NULL        |          NULL | Payroll              |
+-------------+---------------+----------------------+
122 rows in set (0.00 sec)

右連線查詢時,有些部門沒有員工,也會顯示出部門的資訊。

需要注意的是,LEFT JOIN 和 RIGHT JOIN 只存在於 SQL-99 及以後的標準中,在 SQL-92 中不存在,只能用(+)表示。

SQL-92 中採用(+)代表從表所在的位置。即左或右外連線中,(+)表示哪個是從表。Oracle 對 SQL-92 支援較好,而 MySQL 則不支援 SQL-92 的外連線。

# 左外連線
SELECT last_name,department_name
FROM employees ,departments
WHERE employees.department_id = departments.department_id(+);

# 右外連線
SELECT last_name,department_name
FROM employees ,departments
WHERE employees.department_id(+) = departments.department_id;
滿外連線(FULL OUTER JOIN)

滿外連線的結果 = 左右表匹配的資料 + 左表沒有匹配到的資料 + 右表沒有匹配到的資料。

  • SQL-99 是支援滿外連線的。使用 FULL JOIN 或 FULL OUTER JOIN 來實現。
  • 需要注意的是:MySQL 不支援 FULL JOIN,但是可以用 LEFT JOIN UNION RIGHT JOIN 代替。

在 SQL-92 中,只有左外連線和右外連線,沒有滿(或全)外連線。

合併查詢結果

利用 UNION 關鍵字可以合併查詢結果,可以給出多條 SELECT 語句,並將它們的結果組合成單個結果集。合併時,兩個表對應的列數和資料型別必須相同,並且相互對應。各個 SELECT 語句之間使用 UNION 或 UNION ALL 關鍵字分隔。

語法格式:

SELECT column,... FROM table1
UNION [ALL]
SELECT column,... FROM table2;

UNION 運算子

UNION運算子返回兩個查詢的結果集的並集,並去除重複記錄。

image-20230409212240067

UNION ALL 運算子

UNION ALL運算子返回兩個查詢的結果集的並集,對於兩個結果集的重複部分,不去重。

image-20230409215219727

執行 UNION ALL 語句時所需要的資源比 UNION 語句少。如果明確知道合併資料後的結果資料不存在重複資料,或者不需要去除重複的資料,則儘量使用 UNION ALL 語句,以提高資料查詢的效率。

舉例,查詢郵箱包含 a 或者部門編號大於 90 的員工資訊:

mysql> SELECT * FROM employees WHERE email LIKE '%a%' OR department_id > 90;
+-------------+-------------+------------+----------+--------------------+------------+------------+----------+----------------+------------+---------------+
| employee_id | first_name  | last_name  | email    | phone_number       | hire_date  | job_id     | salary   | commission_pct | manager_id | department_id |
+-------------+-------------+------------+----------+--------------------+------------+------------+----------+----------------+------------+---------------+
|         101 | Neena       | Kochhar    | NKOCHHAR | 515.123.4568       | 1989-09-21 | AD_VP      | 17000.00 |           NULL |        100 |            90 |
|         102 | Lex         | De Haan    | LDEHAAN  | 515.123.4569       | 1993-01-13 | AD_VP      | 17000.00 |           NULL |        100 |            90 |
|         206 | William     | Gietz      | WGIETZ   | 515.123.8181       | 1994-06-07 | AC_ACCOUNT |  8300.00 |           NULL |        205 |           110 |
+-------------+-------------+------------+----------+--------------------+------------+------------+----------+----------------+------------+---------------+
67 rows in set (0.00 sec)

使用 UNION 寫法:

mysql> SELECT * FROM employees WHERE email LIKE '%a%' UNION SELECT * FROM employees WHERE department_id > 90;
+-------------+-------------+------------+----------+--------------------+------------+------------+----------+----------------+------------+---------------+
| employee_id | first_name  | last_name  | email    | phone_number       | hire_date  | job_id     | salary   | commission_pct | manager_id | department_id |
+-------------+-------------+------------+----------+--------------------+------------+------------+----------+----------------+------------+---------------+
|         101 | Neena       | Kochhar    | NKOCHHAR | 515.123.4568       | 1989-09-21 | AD_VP      | 17000.00 |           NULL |        100 |            90 |
|         102 | Lex         | De Haan    | LDEHAAN  | 515.123.4569       | 1993-01-13 | AD_VP      | 17000.00 |           NULL |        100 |            90 |
|         206 | William     | Gietz      | WGIETZ   | 515.123.8181       | 1994-06-07 | AC_ACCOUNT |  8300.00 |           NULL |        205 |           110 |
+-------------+-------------+------------+----------+--------------------+------------+------------+----------+----------------+------------+---------------+
67 rows in set (0.00 sec)

7 種 SQL JOINS 的實現

image-20230411141528205

中圖:內連線 A ∩ B。

mysql> SELECT employee_id, last_name, department_name FROM employees e JOIN departments d ON e.department_id = d.department_id;
+-------------+-------------+------------------+
| employee_id | last_name   | department_name  |
+-------------+-------------+------------------+
|         200 | Whalen      | Administration   |
|         201 | Hartstein   | Marketing        |
|         202 | Fay         | Marketing        |
|         205 | Higgins     | Accounting       |
|         206 | Gietz       | Accounting       |
+-------------+-------------+------------------+
106 rows in set (0.00 sec)

左上圖:左外連線。

mysql> SELECT employee_id, last_name, department_name FROM employees e LEFT JOIN departments d ON e.department_id = d.department_id;
+-------------+-------------+------------------+
| employee_id | last_name   | department_name  |
+-------------+-------------+------------------+
|         100 | King        | Executive        |
|         101 | Kochhar     | Executive        |
|         102 | De Haan     | Executive        |
|         178 | Grant       | NULL             |
|         205 | Higgins     | Accounting       |
|         206 | Gietz       | Accounting       |
+-------------+-------------+------------------+
107 rows in set (0.00 sec)

右上圖:右外連線。

mysql> SELECT employee_id, last_name, department_name FROM employees e RIGHT JOIN departments d ON e.department_id = d.`department_id`;
+-------------+-------------+----------------------+
| employee_id | last_name   | department_name      |
+-------------+-------------+----------------------+
|         200 | Whalen      | Administration       |
|         201 | Hartstein   | Marketing            |
|         202 | Fay         | Marketing            |
|        NULL | NULL        | IT Helpdesk          |
|        NULL | NULL        | Government Sales     |
|        NULL | NULL        | Retail Sales         |
|        NULL | NULL        | Recruiting           |
|        NULL | NULL        | Payroll              |
+-------------+-------------+----------------------+
122 rows in set (0.00 sec)

左中圖:A - A ∩ B。

mysql> SELECT employee_id, last_name, department_name FROM employees e LEFT JOIN departments d ON e.department_id = d.department_id WHERE d.department_id IS NULL;
+-------------+-----------+-----------------+
| employee_id | last_name | department_name |
+-------------+-----------+-----------------+
|         178 | Grant     | NULL            |
+-------------+-----------+-----------------+
1 row in set (0.00 sec)

右中圖:B - A ∩ B。

mysql> SELECT employee_id, last_name, department_name FROM employees e RIGHT JOIN departments d ON e.department_id = d.department_id WHERE e.department_id IS NULL;
+-------------+-----------+----------------------+
| employee_id | last_name | department_name      |
+-------------+-----------+----------------------+
|        NULL | NULL      | Treasury             |
|        NULL | NULL      | Corporate Tax        |
|        NULL | NULL      | Control And Credit   |
|        NULL | NULL      | Shareholder Services |
|        NULL | NULL      | Benefits             |
|        NULL | NULL      | Manufacturing        |
|        NULL | NULL      | Construction         |
|        NULL | NULL      | Contracting          |
|        NULL | NULL      | Operations           |
|        NULL | NULL      | IT Support           |
|        NULL | NULL      | NOC                  |
|        NULL | NULL      | IT Helpdesk          |
|        NULL | NULL      | Government Sales     |
|        NULL | NULL      | Retail Sales         |
|        NULL | NULL      | Recruiting           |
|        NULL | NULL      | Payroll              |
+-------------+-----------+----------------------+
16 rows in set (0.00 sec)

左下圖:滿外連線,左中圖 + 右上圖,A∪B。

# 沒有去重操作,效率高
mysql> SELECT employee_id, last_name, department_name FROM employees e LEFT JOIN departments d ON e.department_id = d.department_id WHERE d.department_id IS NULL UNION ALL SELECT employee_id, last_name, department_name FROM employees e RIGHT JOIN departments d ON e.department_id = d.department_id;
+-------------+-------------+----------------------+
| employee_id | last_name   | department_name      |
+-------------+-------------+----------------------+
|         178 | Grant       | NULL                 |
|         200 | Whalen      | Administration       |
|         201 | Hartstein   | Marketing            |
|         202 | Fay         | Marketing            |
|         114 | Raphaely    | Purchasing           |
|        NULL | NULL        | Construction         |
|        NULL | NULL        | Contracting          |
|        NULL | NULL        | Operations           |
|        NULL | NULL        | IT Support           |
|        NULL | NULL        | NOC                  |
|        NULL | NULL        | IT Helpdesk          |
|        NULL | NULL        | Government Sales     |
|        NULL | NULL        | Retail Sales         |
|        NULL | NULL        | Recruiting           |
|        NULL | NULL        | Payroll              |
+-------------+-------------+----------------------+
123 rows in set (0.00 sec)

右下圖:左中圖 + 右中圖,A ∪B - A ∩ B,或者 (A - A ∩ B) ∪ (B - A ∩ B)。

mysql> SELECT employee_id, last_name, department_name FROM employees e LEFT JOIN departments d ON e.department_id = d.department_id WHERE d.department_id IS NULL UNION ALL SELECT employee_id, last_name, department_name FROM employees e RIGHT JOIN departments d ON e.department_id = d.department_id WHERE e.department_id IS NULL;
+-------------+-----------+----------------------+
| employee_id | last_name | department_name      |
+-------------+-----------+----------------------+
|         178 | Grant     | NULL                 |
|        NULL | NULL      | Treasury             |
|        NULL | NULL      | Corporate Tax        |
|        NULL | NULL      | Control And Credit   |
|        NULL | NULL      | Shareholder Services |
|        NULL | NULL      | Benefits             |
|        NULL | NULL      | Manufacturing        |
|        NULL | NULL      | Construction         |
|        NULL | NULL      | Contracting          |
|        NULL | NULL      | Operations           |
|        NULL | NULL      | IT Support           |
|        NULL | NULL      | NOC                  |
|        NULL | NULL      | IT Helpdesk          |
|        NULL | NULL      | Government Sales     |
|        NULL | NULL      | Retail Sales         |
|        NULL | NULL      | Recruiting           |
|        NULL | NULL      | Payroll              |
+-------------+-----------+----------------------+
17 rows in set (0.00 sec)

左中圖語法格式:

# 實現 A - A ∩ B
SELECT 欄位列表
FROM A表 LEFT JOIN B表
ON 關聯條件
WHERE 從表關聯欄位 IS NULL AND 等其他子句;

右中圖語法格式:

# 實現 B - A ∩ B
SELECT 欄位列表
FROM A表 RIGHT JOIN B表
ON 關聯條件
WHERE 從表關聯欄位 IS NULL AND 等其他子句;

左下圖語法格式:

# 實現查詢結果是 A ∪ B,用左外的 A,union 右外的 B
SELECT 欄位列表
FROM A表 LEFT JOIN B表
ON 關聯條件
WHERE 等其他子句
UNION
SELECT 欄位列表
FROM A表 RIGHT JOIN B表
ON 關聯條件
WHERE 等其他子句;

SQL-99 語法新特性

自然連線

SQL-99 在 SQL-92 的基礎上提供了一些特殊語法,比如NATURAL JOIN用來表示自然連線。我們可以把自然連線理解為 SQL-92 中的等值連線,它會自動查詢兩張連線表中所有相同的欄位,然後進行等值連線。

在 SQL-92 標準中:

mysql> SELECT employee_id, last_name, department_name FROM employees e JOIN departments d ON e.department_id = d.department_id AND e.manager_id = d.manager_id;
+-------------+------------+-----------------+
| employee_id | last_name  | department_name |
+-------------+------------+-----------------+
|         202 | Fay        | Marketing       |
|         115 | Khoo       | Purchasing      |
|         116 | Baida      | Purchasing      |
|         117 | Tobias     | Purchasing      |
|         118 | Himuro     | Purchasing      |
|         119 | Colmenares | Purchasing      |
|         129 | Bissot     | Shipping        |
|         130 | Atkinson   | Shipping        |
|         131 | Marlow     | Shipping        |
|         132 | Olson      | Shipping        |
|         184 | Sarchand   | Shipping        |
|         185 | Bull       | Shipping        |
|         186 | Dellinger  | Shipping        |
|         187 | Cabrio     | Shipping        |
|         104 | Ernst      | IT              |
|         105 | Austin     | IT              |
|         106 | Pataballa  | IT              |
|         107 | Lorentz    | IT              |
|         150 | Tucker     | Sales           |
|         151 | Bernstein  | Sales           |
|         152 | Hall       | Sales           |
|         153 | Olsen      | Sales           |
|         154 | Cambrault  | Sales           |
|         155 | Tuvault    | Sales           |
|         101 | Kochhar    | Executive       |
|         102 | De Haan    | Executive       |
|         109 | Faviet     | Finance         |
|         110 | Chen       | Finance         |
|         111 | Sciarra    | Finance         |
|         112 | Urman      | Finance         |
|         113 | Popp       | Finance         |
|         206 | Gietz      | Accounting      |
+-------------+------------+-----------------+
32 rows in set (0.00 sec)

在 SQL-99 中可以寫成:

mysql> SELECT employee_id, last_name, department_name FROM employees e NATURAL JOIN departments d;
+-------------+------------+-----------------+
| employee_id | last_name  | department_name |
+-------------+------------+-----------------+
|         202 | Fay        | Marketing       |
|         115 | Khoo       | Purchasing      |
|         116 | Baida      | Purchasing      |
|         117 | Tobias     | Purchasing      |
|         118 | Himuro     | Purchasing      |
|         119 | Colmenares | Purchasing      |
|         129 | Bissot     | Shipping        |
|         130 | Atkinson   | Shipping        |
|         131 | Marlow     | Shipping        |
|         132 | Olson      | Shipping        |
|         184 | Sarchand   | Shipping        |
|         185 | Bull       | Shipping        |
|         186 | Dellinger  | Shipping        |
|         187 | Cabrio     | Shipping        |
|         104 | Ernst      | IT              |
|         105 | Austin     | IT              |
|         106 | Pataballa  | IT              |
|         107 | Lorentz    | IT              |
|         150 | Tucker     | Sales           |
|         151 | Bernstein  | Sales           |
|         152 | Hall       | Sales           |
|         153 | Olsen      | Sales           |
|         154 | Cambrault  | Sales           |
|         155 | Tuvault    | Sales           |
|         101 | Kochhar    | Executive       |
|         102 | De Haan    | Executive       |
|         109 | Faviet     | Finance         |
|         110 | Chen       | Finance         |
|         111 | Sciarra    | Finance         |
|         112 | Urman      | Finance         |
|         113 | Popp       | Finance         |
|         206 | Gietz      | Accounting      |
+-------------+------------+-----------------+
32 rows in set (0.00 sec)

USING 連線

當進行連線的時候,SQL-99 還支援使用USING指定資料表裡的同名欄位進行等值連線,但是隻能配合 JOIN 一起使用。比如:

mysql> SELECT employee_id, last_name, department_name FROM employees e JOIN departments d USING (department_id);
+-------------+-------------+------------------+
| employee_id | last_name   | department_name  |
+-------------+-------------+------------------+
|         200 | Whalen      | Administration   |
|         201 | Hartstein   | Marketing        |
|         202 | Fay         | Marketing        |
|         205 | Higgins     | Accounting       |
|         206 | Gietz       | Accounting       |
+-------------+-------------+------------------+
106 rows in set (0.00 sec)

與自然連線 NATURAL JOIN 不同的是,USING 指定了具體的相同的欄位名稱,需要在 USING 的括號 () 中填入要指定的同名欄位。同時使用 JOIN...USING 可以簡化 JOIN ON 的等值連線。它與下面的 SQL 查詢結果是相同的:

mysql> SELECT employee_id, last_name, department_name FROM employees e, departments d WHERE e.department_id = d.department_id;

小結

表連線的約束條件可以有三種方式:WHERE,ON,USING。

  • WHERE:適用於所有關聯查詢。
  • ON :只能和 JOIN 一起使用,只能寫關聯條件。雖然關聯條件可以併到 WHERE 中和其他條件一起寫,但分開寫可讀性更好。
  • USING:只能和 JOIN 一起使用,要求兩個關聯欄位在關聯表中名稱一致,而且只能表示關聯欄位值相等。
# 把關聯條件寫在 where 後面
mysql> SELECT last_name, department_name FROM employees, departments WHERE employees.department_id = departments.department_id;

# 把關聯條件寫在 on 後面,只能和 JOIN 一起使用
mysql> SELECT last_name, department_name FROM employees INNER JOIN departments ON employees.department_id = departments.department_id;

mysql> SELECT last_name, department_name FROM employees CROSS JOIN departments ON employees.department_id = departments.department_id;

mysql> SELECT last_name, department_name FROM employees JOIN departments ON employees.department_id = departments.department_id;

# 把關聯欄位寫在 using() 中,只能和 JOIN 一起使用,而且兩個表中的關聯欄位必須名稱相同,而且只能表示 =
# 查詢員工姓名與基本工資
mysql> SELECT last_name, job_title FROM employees INNER JOIN jobs USING(job_id);

# n 張表關聯,至少需要 n-1 個關聯條件
# 查詢員工姓名,基本工資,部門名稱
mysql> SELECT last_name, job_title, department_name FROM employees, departments, jobs WHERE employees.department_id = departments.department_id AND employees.job_id = jobs.job_id;

mysql> SELECT last_name, job_title, department_name FROM employees INNER JOIN departments INNER JOIN jobs ON employees.department_id = departments.department_id AND employees.job_id = jobs.job_id;

注意:要控制連線表的數量。多表連線就相當於巢狀 for 迴圈一樣,非常消耗資源,會讓 SQL 查詢效能下降得很嚴重,因此不要連線不必要的表。在許多 DBMS 中,也都會有最大連線表的限#制。

連表查詢的條件問題

正確寫法:

SELECT 
  ledger.credit_code AS '統一社會信用程式碼', info.company_name AS '企業名稱', ledger.hazard_ledger_id AS '主鍵',
  (CASE ledger.danger_level WHEN 1 THEN '一級' WHEN 2 THEN '二級' WHEN 3 THEN '三級' WHEN 4 THEN '四級' ELSE ledger.danger_level END) AS '隱患等級',
  (CASE ledger.manager_level WHEN 0 THEN '集團級' WHEN 1 THEN '公司級' WHEN 2 THEN '企業級' WHEN 3 THEN '車間級' WHEN 4 THEN '裝置級' ELSE ledger.manager_level END) AS '管理級別',
  ledger.danger_type AS '隱患型別分類編碼',
  (CASE 
        WHEN ledger.danger_type LIKE '1-%' THEN '健康' 
        WHEN ledger.danger_type LIKE '2-%' THEN '安全' 
        WHEN ledger.danger_type LIKE '3-%' THEN '環保' 
        WHEN ledger.danger_type LIKE '4-%' THEN '節能' 
        WHEN ledger.danger_type LIKE '5-%' THEN '低碳' 
        WHEN ledger.danger_type LIKE '6-%' THEN '安保' 
        ELSE ledger.danger_type 
   END) AS '隱患型別一級分類',
  -- ledger.danger_name AS '隱患名稱', ledger.danger_description AS '隱患描述', ledger.danger_src AS '隱患來源', ledger.danger_type AS '隱患型別(專業)', ledger.danger_type1 AS '隱患型別(危害因素)', ledger.danger_type2 AS '隱患型別(體系)',
  ledger.find_by_name AS '發現人', ledger.find_date AS '發現時間', ledger.registration_by_name AS '登記人', ledger.regist_time AS '登記時間', '自建系統' AS '系統型別', NOW() AS '匯出時間'
FROM dp_phd_ledger ledger
LEFT JOIN company_info info ON ledger.credit_code = info.credit_code AND info.deleted = 0 # 注意此處
WHERE ledger.deleted = 0 AND ledger.danger_type IS NOT NULL AND ledger.find_date >= '2023-05-01 00:00:00' 
ORDER BY ledger.credit_code, DATE_FORMAT(ledger.find_date, '%Y-%m-%d')

錯誤寫法:

SELECT 
  ledger.credit_code AS '統一社會信用程式碼', info.company_name AS '企業名稱', ledger.hazard_ledger_id AS '主鍵',
  (CASE ledger.danger_level WHEN 1 THEN '一級' WHEN 2 THEN '二級' WHEN 3 THEN '三級' WHEN 4 THEN '四級' ELSE ledger.danger_level END) AS '隱患等級',
  (CASE ledger.manager_level WHEN 0 THEN '集團級' WHEN 1 THEN '公司級' WHEN 2 THEN '企業級' WHEN 3 THEN '車間級' WHEN 4 THEN '裝置級' ELSE ledger.manager_level END) AS '管理級別',
  ledger.danger_type AS '隱患型別分類編碼',
  (CASE 
        WHEN ledger.danger_type LIKE '1-%' THEN '健康' 
        WHEN ledger.danger_type LIKE '2-%' THEN '安全' 
        WHEN ledger.danger_type LIKE '3-%' THEN '環保' 
        WHEN ledger.danger_type LIKE '4-%' THEN '節能' 
        WHEN ledger.danger_type LIKE '5-%' THEN '低碳' 
        WHEN ledger.danger_type LIKE '6-%' THEN '安保' 
        ELSE ledger.danger_type 
   END) AS '隱患型別一級分類',
  -- ledger.danger_name AS '隱患名稱', ledger.danger_description AS '隱患描述', ledger.danger_src AS '隱患來源', ledger.danger_type AS '隱患型別(專業)', ledger.danger_type1 AS '隱患型別(危害因素)', ledger.danger_type2 AS '隱患型別(體系)',
  ledger.find_by_name AS '發現人', ledger.find_date AS '發現時間', ledger.registration_by_name AS '登記人', ledger.regist_time AS '登記時間', '自建系統' AS '系統型別', NOW() AS '匯出時間'
FROM dp_phd_ledger ledger
LEFT JOIN company_info info ON ledger.credit_code = info.credit_code
# 注意此處
WHERE info.deleted = 0 AND ledger.deleted = 0 AND ledger.danger_type IS NOT NULL AND ledger.find_date >= '2023-05-01 00:00:00' 
ORDER BY ledger.credit_code, DATE_FORMAT(ledger.find_date, '%Y-%m-%d')

對比正確的連表查詢條件寫法,錯誤的寫法中,將右表的 info.deleted = 0 條件放到了 WHERE 中,正確的寫法則是放在了 ON 中,原因:dp_phd_ledger 為左表,company_info 表為右表,LEFT JOIN 時,company_info 表可能沒有資料,此時,company_info 表的 deleted 欄位為 NULL,不滿足 deleted = 0 條件,資料會丟失。而將 info.deleted = 0 條件放到 ON 中,會先過濾 company_info 表的資料,此時不會丟失資料。

子查詢

子查詢指一個查詢語句巢狀在另一個查詢語句內部的查詢,這個特性從 MySQL 4.1 開始引入。

SQL 中子查詢的使用大大增強了 SELECT 查詢的能力,因為很多時候查詢需要從結果集中獲取資料,或者需要從同一個表中先計算得出一個資料結果,然後與這個資料結果(可能是某個標量,也可能是某個集合)進行比較。

需求分析與問題解決

實際問題

image-20230428005415876

現有解決方式:

# 方式一
mysql> SELECT salary FROM employees WHERE last_name = 'Abel';
+----------+
| salary   |
+----------+
| 11000.00 |
+----------+
1 row in set (0.00 sec)

mysql> SELECT last_name, salary FROM employees WHERE salary > 11000;
+-----------+----------+
| last_name | salary   |
+-----------+----------+
| King      | 24000.00 |
| Kochhar   | 17000.00 |
| De Haan   | 17000.00 |
| Greenberg | 12000.00 |
| Russell   | 14000.00 |
| Partners  | 13500.00 |
| Errazuriz | 12000.00 |
| Ozer      | 11500.00 |
| Hartstein | 13000.00 |
| Higgins   | 12000.00 |
+-----------+----------+
10 rows in set (0.00 sec)

# 方式二:自連線
mysql> SELECT e2.last_name, e2.salary FROM employees e1, employees e2 WHERE e1.last_name = 'Abel' AND e1.`salary` < e2.`salary`;
+-----------+----------+
| last_name | salary   |
+-----------+----------+
| King      | 24000.00 |
| Kochhar   | 17000.00 |
| De Haan   | 17000.00 |
| Greenberg | 12000.00 |
| Russell   | 14000.00 |
| Partners  | 13500.00 |
| Errazuriz | 12000.00 |
| Ozer      | 11500.00 |
| Hartstein | 13000.00 |
| Higgins   | 12000.00 |
+-----------+----------+
10 rows in set (0.01 sec)

# 方式三:子查詢
mysql> SELECT last_name, salary FROM employees WHERE salary > (SELECT salary FROM employees WHERE last_name = 'Abel');
+-----------+----------+
| last_name | salary   |
+-----------+----------+
| King      | 24000.00 |
| Kochhar   | 17000.00 |
| De Haan   | 17000.00 |
| Greenberg | 12000.00 |
| Russell   | 14000.00 |
| Partners  | 13500.00 |
| Errazuriz | 12000.00 |
| Ozer      | 11500.00 |
| Hartstein | 13000.00 |
| Higgins   | 12000.00 |
+-----------+----------+
10 rows in set (0.00 sec)

子查詢的基本使用

子查詢的基本語法結構:

image-20230428005851943

  • 子查詢(內查詢)在主查詢之前一次執行完成。
  • 子查詢的結果被主查詢(外查詢)使用。

注意事項:

  • 子查詢要包含在括號內。
  • 將子查詢放在比較條件的右側。
  • 單行運算子對應單行子查詢,多行運算子對應多行子查詢。

子查詢的分類

分類方式一:按內查詢的結果返回一條還是多條記錄,將子查詢分為單行子查詢多行子查詢

  • 單行子查詢

    image-20230428010204830

  • 多行子查詢

    image-20230428010411394

分類方式二:按內查詢是否被執行多次,將子查詢劃分為相關(或關聯)子查詢不相關(或非關聯)子查詢

  • 子查詢從資料表中查詢了資料結果,如果這個資料結果只執行一次,然後這個資料結果作為主查詢的條件進行執行,那麼這樣的子查詢叫做不相關子查詢。
  • 同樣,如果子查詢需要執行多次,即採用迴圈的方式,先從外部查詢開始,每次都傳入子查詢進行查詢,然後再將結果反饋給外部,這種巢狀的執行方式就稱為相關子查詢。

單行子查詢

單行比較運算子

運算子 含義
= equal to
> greater than
>= greater than or equal to
< less than
<= less than or equal to
<> not equal to

程式碼示例

查詢工資大於 149 號員工工資的員工的資訊:

image-20230428124259050

mysql> SELECT last_name, salary FROM employees WHERE salary > (SELECT salary FROM employees WHERE employee_id = 149);
+-----------+----------+
| last_name | salary   |
+-----------+----------+
| King      | 24000.00 |
| Kochhar   | 17000.00 |
| De Haan   | 17000.00 |
| Greenberg | 12000.00 |
| Raphaely  | 11000.00 |
| Russell   | 14000.00 |
| Partners  | 13500.00 |
| Errazuriz | 12000.00 |
| Cambrault | 11000.00 |
| Ozer      | 11500.00 |
| Abel      | 11000.00 |
| Hartstein | 13000.00 |
| Higgins   | 12000.00 |
+-----------+----------+
13 rows in set (0.00 sec)

返回 job_id 與 141 號員工相同,salary 比 143 號員工多的員工姓名,job_id 和工資:

mysql> SELECT last_name, job_id, salary FROM employees WHERE job_id = (SELECT job_id FROM employees WHERE employee_id = 141) AND salary > (SELECT salary FROM employees WHERE employee_id = 143);
+-------------+----------+---------+
| last_name   | job_id   | salary  |
+-------------+----------+---------+
| Nayer       | ST_CLERK | 3200.00 |
| Mikkilineni | ST_CLERK | 2700.00 |
| Bissot      | ST_CLERK | 3300.00 |
| Atkinson    | ST_CLERK | 2800.00 |
| Mallin      | ST_CLERK | 3300.00 |
| Rogers      | ST_CLERK | 2900.00 |
| Ladwig      | ST_CLERK | 3600.00 |
| Stiles      | ST_CLERK | 3200.00 |
| Seo         | ST_CLERK | 2700.00 |
| Rajs        | ST_CLERK | 3500.00 |
| Davies      | ST_CLERK | 3100.00 |
+-------------+----------+---------+
11 rows in set (0.00 sec)

返回公司工資最少的員工的 last_name,job_id 和 salary:

mysql> SELECT last_name, job_id, salary FROM employees WHERE salary = (SELECT MIN(salary) FROM employees);
+-----------+----------+---------+
| last_name | job_id   | salary  |
+-----------+----------+---------+
| Olson     | ST_CLERK | 2100.00 |
+-----------+----------+---------+
1 row in set (0.00 sec)

查詢與 141 號或 174 號員工的 manager_id 和 department_id 相同的其他員工的 employee_id,manager_id,department_id:

# 方式一
mysql> SELECT employee_id, manager_id, department_id FROM employees WHERE manager_id IN (SELECT manager_id FROM employees WHERE employee_id IN (174,141)) AND department_id IN (SELECT department_id FROM employees WHERE employee_id IN (174,141)) AND employee_id NOT IN (174,141);
+-------------+------------+---------------+
| employee_id | manager_id | department_id |
+-------------+------------+---------------+
|         142 |        124 |            50 |
|         143 |        124 |            50 |
|         144 |        124 |            50 |
|         196 |        124 |            50 |
|         197 |        124 |            50 |
|         198 |        124 |            50 |
|         199 |        124 |            50 |
|         175 |        149 |            80 |
|         176 |        149 |            80 |
|         177 |        149 |            80 |
|         179 |        149 |            80 |
+-------------+------------+---------------+
11 rows in set (0.00 sec)

# 方式二
mysql> SELECT employee_id, manager_id, department_id FROM employees WHERE (manager_id, department_id) IN (SELECT manager_id, department_id FROM employees WHERE employee_id IN (141,174)) AND employee_id NOT IN (141,174);
+-------------+------------+---------------+
| employee_id | manager_id | department_id |
+-------------+------------+---------------+
|         142 |        124 |            50 |
|         143 |        124 |            50 |
|         144 |        124 |            50 |
|         196 |        124 |            50 |
|         197 |        124 |            50 |
|         198 |        124 |            50 |
|         199 |        124 |            50 |
|         175 |        149 |            80 |
|         176 |        149 |            80 |
|         177 |        149 |            80 |
|         179 |        149 |            80 |
+-------------+------------+---------------+
11 rows in set (0.00 sec)

HAVING 中的子查詢

執行過程:

  • 首先,執行子查詢;
  • 然後,向主查詢中的 HAVING 子句返回結果。

查詢最低工資大於 50 號部門最低工資的部門 id 和其最低工資:

mysql> SELECT department_id, MIN(salary) FROM employees GROUP BY department_id HAVING MIN(salary) > (SELECT MIN(salary) FROM employees WHERE department_id = 50); 
+---------------+-------------+
| department_id | MIN(salary) |
+---------------+-------------+
|          NULL |     7000.00 |
|            10 |     4400.00 |
|            20 |     6000.00 |
|            30 |     2500.00 |
|            40 |     6500.00 |
|            60 |     4200.00 |
|            70 |    10000.00 |
|            80 |     6100.00 |
|            90 |    17000.00 |
|           100 |     6900.00 |
|           110 |     8300.00 |
+---------------+-------------+
11 rows in set (0.00 sec)

CASE 中的子查詢

查詢員工的 employee_id,last_name 和 location。其中,若員工 department_id 與 location_id 為1800 的員工的 department_id 相同,則 location 為 Canada,其餘則為 USA。

mysql> SELECT employee_id, last_name, (CASE department_id WHEN (SELECT department_id FROM departments WHERE location_id = 1800) THEN 'Canada' ELSE 'USA' END) location FROM employees;
+-------------+-------------+----------+
| employee_id | last_name   | location |
+-------------+-------------+----------+
|         100 | King        | USA      |
|         101 | Kochhar     | USA      |
|         102 | De Haan     | USA      |
|         103 | Hunold      | USA      |
|         104 | Ernst       | USA      |
|         105 | Austin      | USA      |
|         106 | Pataballa   | USA      |
|         107 | Lorentz     | USA      |
|         108 | Greenberg   | USA      |
|         109 | Faviet      | USA      |
|         110 | Chen        | USA      |
|         111 | Sciarra     | USA      |
|         112 | Urman       | USA      |
|         113 | Popp        | USA      |
|         200 | Whalen      | USA      |
|         201 | Hartstein   | Canada   |
|         202 | Fay         | Canada   |
|         203 | Mavris      | USA      |
|         204 | Baer        | USA      |
|         205 | Higgins     | USA      |
|         206 | Gietz       | USA      |
+-------------+-------------+----------+
107 rows in set (0.00 sec)

空值問題

mysql> SELECT last_name, job_id FROM employees WHERE job_id = (SELECT job_id FROM employees WHERE last_name = 'Haas');
Empty set (0.00 sec)

mysql> SELECT job_id FROM employees WHERE last_name = 'Haas';
Empty set 

employees 中沒有叫 Haas 的員工,子查詢不返回任何行,主查詢也為空值。

非法使用子查詢

mysql> SELECT employee_id, last_name FROM employees WHERE salary = (SELECT MIN(salary) FROM employees GROUP BY department_id);
ERROR 1242 (21000): Subquery returns more than 1 row

多行子查詢,但使用了單行比較符。

多行子查詢

多行子查詢,也稱為集合比較子查詢,其特點如下:

  • 內查詢返回多行。
  • 使用多行比較運算子。

多行比較運算子

運算子 含義
IN 等於列表中的任意一個
ANY 需要和單行比較運算子一起使用,和子查詢返回的某一個值比較
ALL 需要和單行比較運算子一起使用,和子查詢返回的所有值比較
SOME 實際上是 ANY 的別名,作用相同,一般常使用 ANY

程式碼示例

返回其它 job_id 中比 job_id 為 IT_PROG 部門任一工資低的員工的員工號、姓名、job_id 以及 salary:

image-20230430003347643

mysql> SELECT employee_id, last_name, job_id, salary FROM employees WHERE salary < ANY (SELECT salary FROM employees WHERE job_id = 'IT_PROG') AND job_id <> 'IT_PROG';
+-------------+-------------+------------+---------+
| employee_id | last_name   | job_id     | salary  |
+-------------+-------------+------------+---------+
|         110 | Chen        | FI_ACCOUNT | 8200.00 |
|         111 | Sciarra     | FI_ACCOUNT | 7700.00 |
|         112 | Urman       | FI_ACCOUNT | 7800.00 |
|         113 | Popp        | FI_ACCOUNT | 6900.00 |
|         115 | Khoo        | PU_CLERK   | 3100.00 |		
|         116 | Baida       | PU_CLERK   | 2900.00 |
|         117 | Tobias      | PU_CLERK   | 2800.00 |
|         118 | Himuro      | PU_CLERK   | 2600.00 |
|         119 | Colmenares  | PU_CLERK   | 2500.00 |
|         120 | Weiss       | ST_MAN     | 8000.00 |
|         121 | Fripp       | ST_MAN     | 8200.00 |
|         122 | Kaufling    | ST_MAN     | 7900.00 |
|         123 | Vollman     | ST_MAN     | 6500.00 |
|         124 | Mourgos     | ST_MAN     | 5800.00 |
|         125 | Nayer       | ST_CLERK   | 3200.00 |
|         203 | Mavris      | HR_REP     | 6500.00 |
|         206 | Gietz       | AC_ACCOUNT | 8300.00 |
+-------------+-------------+------------+---------+
76 rows in set (0.00 sec)

mysql> SELECT salary FROM employees WHERE job_id = 'IT_PROG';
+---------+
| salary  |
+---------+
| 9000.00 |
| 6000.00 |
| 4800.00 |
| 4800.00 |
| 4200.00 |
+---------+
5 rows in set (0.00 sec)

返回其它 job_id 中比 job_id 為 IT_PROG 部門所有工資都低的員工的員工號、姓名、job_id 以及 salary:

image-20230430004652356

mysql> SELECT employee_id, last_name, job_id, salary FROM employees WHERE salary < ALL (SELECT salary FROM employees WHERE job_id = 'IT_PROG') AND job_id <> 'IT_PROG';
+-------------+-------------+----------+---------+
| employee_id | last_name   | job_id   | salary  |
+-------------+-------------+----------+---------+
|         115 | Khoo        | PU_CLERK | 3100.00 |
|         116 | Baida       | PU_CLERK | 2900.00 |
|         117 | Tobias      | PU_CLERK | 2800.00 |
|         118 | Himuro      | PU_CLERK | 2600.00 |
|         119 | Colmenares  | PU_CLERK | 2500.00 |
|         125 | Nayer       | ST_CLERK | 3200.00 |
|         126 | Mikkilineni | ST_CLERK | 2700.00 |
|         127 | Landry      | ST_CLERK | 2400.00 |
|         128 | Markle      | ST_CLERK | 2200.00 |
|         129 | Bissot      | ST_CLERK | 3300.00 |
|         130 | Atkinson    | ST_CLERK | 2800.00 |
|         131 | Marlow      | ST_CLERK | 2500.00 |
|         132 | Olson       | ST_CLERK | 2100.00 |
|         193 | Everett     | SH_CLERK | 3900.00 |
|         194 | McCain      | SH_CLERK | 3200.00 |
|         195 | Jones       | SH_CLERK | 2800.00 |
|         196 | Walsh       | SH_CLERK | 3100.00 |
|         197 | Feeney      | SH_CLERK | 3000.00 |
|         198 | OConnell    | SH_CLERK | 2600.00 |
|         199 | Grant       | SH_CLERK | 2600.00 |
+-------------+-------------+----------+---------+
44 rows in set (0.00 sec)

查詢平均工資最低的部門 id:

# 方式一
mysql> SELECT department_id FROM employees GROUP BY department_id HAVING AVG(salary) = (SELECT MIN(avg_sal) FROM (SELECT AVG(salary) avg_sal FROM employees GROUP BY department_id) t_dept_avg_sal);
+---------------+
| department_id |
+---------------+
|            50 |
+---------------+
1 row in set (0.00 sec)

# 方式二
mysql> SELECT department_id FROM employees GROUP BY department_id HAVING AVG(salary) = (SELECT MIN(avg_sal) FROM (SELECT AVG(salary) avg_sal FROM employees GROUP BY department_id) t_dept_avg_sal);
+---------------+
| department_id |
+---------------+
|            50 |
+---------------+
1 row in set (0.00 sec)

MySQL 中聚合函式不能巢狀使用。

空值問題

mysql> SELECT last_name FROM employees WHERE employee_id NOT IN (SELECT manager_id FROM employees);
Empty set (0.00 sec)

mysql> SELECT manager_id FROM employees;
+------------+
| manager_id |
+------------+
|       NULL |
|        100 |
|        100 |
|        100 |
|        149 |
|        149 |
|        201 |
|        205 |
+------------+
107 rows in set (0.00 sec)

mysql> SELECT last_name FROM employees WHERE employee_id NOT IN (SELECT manager_id FROM employees WHERE manager_id IS NOT NULL);
+-------------+
| last_name   |
+-------------+
| Ernst       |
| Austin      |
| Pataballa   |
| Lorentz     |
| Faviet      |
| Mavris      |
| Baer        |
| Gietz       |
+-------------+
89 rows in set (0.00 sec)

因為子查詢中,返回了一個 NULL 值,導致主查詢失敗。新增條件後,主查詢正常。

相關子查詢

相關子查詢執行流程

如果子查詢的執行依賴於外部查詢,通常情況下都是因為子查詢中的表用到了外部的表,並進行了條件關聯,因此每執行一次外部查詢,子查詢都要重新計算一次,這樣的子查詢就稱之為關聯子查詢

相關子查詢按照一行接一行的順序執行,主查詢的每一行都執行一次子查詢。

image-20230430094211250

image-20230430094315239

程式碼示例

查詢員工中工資大於本部門平均工資的員工的 last_name,salary 和其 department_id:

# 相關子查詢
mysql> SELECT e1.last_name, e1.salary, e1.department_id FROM employees e1 WHERE salary > (SELECT AVG(salary) FROM employees e2 WHERE e2.department_id = e1.department_id);
+-----------+----------+---------------+
| last_name | salary   | department_id |
+-----------+----------+---------------+
| King      | 24000.00 |            90 |
| Hunold    |  9000.00 |            60 |
| Ernst     |  6000.00 |            60 |
| Greenberg | 12000.00 |           100 |
| Faviet    |  9000.00 |           100 |
| Raphaely  | 11000.00 |            30 |
| Weiss     |  8000.00 |            50 |
| Fripp     |  8200.00 |            50 |
| Kaufling  |  7900.00 |            50 |
| Vollman   |  6500.00 |            50 |
| Mourgos   |  5800.00 |            50 |
| Ladwig    |  3600.00 |            50 |
| Rajs      |  3500.00 |            50 |
| Russell   | 14000.00 |            80 |
| Partners  | 13500.00 |            80 |
| Errazuriz | 12000.00 |            80 |
| Cambrault | 11000.00 |            80 |
| Zlotkey   | 10500.00 |            80 |
| Tucker    | 10000.00 |            80 |
| Bernstein |  9500.00 |            80 |
| Hall      |  9000.00 |            80 |
| King      | 10000.00 |            80 |
| Sully     |  9500.00 |            80 |
| McEwen    |  9000.00 |            80 |
| Vishney   | 10500.00 |            80 |
| Greene    |  9500.00 |            80 |
| Ozer      | 11500.00 |            80 |
| Bloom     | 10000.00 |            80 |
| Fox       |  9600.00 |            80 |
| Abel      | 11000.00 |            80 |
| Sarchand  |  4200.00 |            50 |
| Bull      |  4100.00 |            50 |
| Chung     |  3800.00 |            50 |
| Dilly     |  3600.00 |            50 |
| Bell      |  4000.00 |            50 |
| Everett   |  3900.00 |            50 |
| Hartstein | 13000.00 |            20 |
| Higgins   | 12000.00 |           110 |
+-----------+----------+---------------+
38 rows in set (0.00 sec)

# 方式二:在 FROM 中使用子查詢
mysql> SELECT e1.last_name, e1.salary, e1.department_id FROM employees e1, (SELECT department_id, AVG(salary) dept_avg_sal FROM employees GROUP
BY department_id) e2 WHERE e1.department_id = e2.department_id AND e2.dept_avg_sal < e1.salary;
+-----------+----------+---------------+
| last_name | salary   | department_id |
+-----------+----------+---------------+
| Hartstein | 13000.00 |            20 |
| Raphaely  | 11000.00 |            30 |
| Weiss     |  8000.00 |            50 |
| Fripp     |  8200.00 |            50 |
| Kaufling  |  7900.00 |            50 |
| Vollman   |  6500.00 |            50 |
| Mourgos   |  5800.00 |            50 |
| Ladwig    |  3600.00 |            50 |
| Rajs      |  3500.00 |            50 |
| Sarchand  |  4200.00 |            50 |
| Bull      |  4100.00 |            50 |
| Chung     |  3800.00 |            50 |
| Dilly     |  3600.00 |            50 |
| Bell      |  4000.00 |            50 |
| Everett   |  3900.00 |            50 |
| Hunold    |  9000.00 |            60 |
| Ernst     |  6000.00 |            60 |
| Russell   | 14000.00 |            80 |
| Partners  | 13500.00 |            80 |
| Errazuriz | 12000.00 |            80 |
| Cambrault | 11000.00 |            80 |
| Zlotkey   | 10500.00 |            80 |
| Tucker    | 10000.00 |            80 |
| Bernstein |  9500.00 |            80 |
| Hall      |  9000.00 |            80 |
| King      | 10000.00 |            80 |
| Sully     |  9500.00 |            80 |
| McEwen    |  9000.00 |            80 |
| Vishney   | 10500.00 |            80 |
| Greene    |  9500.00 |            80 |
| Ozer      | 11500.00 |            80 |
| Bloom     | 10000.00 |            80 |
| Fox       |  9600.00 |            80 |
| Abel      | 11000.00 |            80 |
| King      | 24000.00 |            90 |
| Greenberg | 12000.00 |           100 |
| Faviet    |  9000.00 |           100 |
| Higgins   | 12000.00 |           110 |
+-----------+----------+---------------+
38 rows in set (0.00 sec)

FROM 型的子查詢:子查詢是作為 FROM 的一部分,子查詢要用 () 引起來,並且要給這個子查詢取別名, 把它當成一張 "臨時的虛擬的表" 來使用。(主表與虛擬表關聯查詢)

查詢員工的 id,salary,按照 department_name 排序:

# 在 ORDER BY 中使用子查詢
mysql> SELECT e.employee_id, e.salary FROM employees e ORDER BY (SELECT department_name FROM departments d WHERE e.department_id = d.department_id);
+-------------+----------+
| employee_id | salary   |
+-------------+----------+
|         178 |  7000.00 |
|         205 | 12000.00 |
|         206 |  8300.00 |
|         200 |  4400.00 |
|         100 | 24000.00 |
|         101 | 17000.00 |
|         102 | 17000.00 |
|         108 | 12000.00 |
|         109 |  9000.00 |
|         110 |  8200.00 |
|         111 |  7700.00 |
|         196 |  3100.00 |
|         197 |  3000.00 |
|         198 |  2600.00 |
|         199 |  2600.00 |
+-------------+----------+
107 rows in set (0.00 sec)

在SELECT中,除了GROUP BY 和 LIMIT之外,其他位置都可以宣告子查詢!

若 employees 表中 employee_id 與 job_history 表中 employee_id 相同的數目不小於 2,輸出這些相同 id 的員工的 employee_id,last_name 和其 job_id:

mysql> SELECT e.employee_id, e.last_name, e.job_id FROM employees e WHERE 2 <= (SELECT COUNT(*) FROM job_history WHERE employee_id = e.employee_id);
+-------------+-----------+---------+
| employee_id | last_name | job_id  |
+-------------+-----------+---------+
|         101 | Kochhar   | AD_VP   |
|         176 | Taylor    | SA_REP  |
|         200 | Whalen    | AD_ASST |
+-------------+-----------+---------+
3 rows in set (0.00 sec)

EXISTS 與 NOT EXISTS 關鍵字

關聯子查詢通常也會和 EXISTS 運算子一起來使用,用來檢查在子查詢中是否存在滿足條件的行。

  • 如果在子查詢中不存在滿足條件的行:

    • 條件返回 FALSE;
    • 繼續在子查詢中查詢。(不滿足繼續找)
  • 如果在子查詢中存在滿足條件的行:

    • 條件返回 TRUE;
    • 不在子查詢中繼續查詢。(滿足即返回)

NOT EXISTS 關鍵字表示如果不存在某種條件,則返回 TRUE,否則返回 FALSE。

查詢公司管理者的 employee_id,last_name,job_id,department_id 資訊:

# 方式一:自連線
mysql> SELECT DISTINCT e1.employee_id, e1.last_name, e1.job_id, e1.department_id FROM employees e1 JOIN employees e2 WHERE e1.employee_id
= e2.manager_id;
+-------------+-----------+---------+---------------+
| employee_id | last_name | job_id  | department_id |
+-------------+-----------+---------+---------------+
|         100 | King      | AD_PRES |            90 |
|         101 | Kochhar   | AD_VP   |            90 |
|         102 | De Haan   | AD_VP   |            90 |
|         103 | Hunold    | IT_PROG |            60 |
|         108 | Greenberg | FI_MGR  |           100 |
|         114 | Raphaely  | PU_MAN  |            30 |
|         120 | Weiss     | ST_MAN  |            50 |
|         121 | Fripp     | ST_MAN  |            50 |
|         122 | Kaufling  | ST_MAN  |            50 |
|         123 | Vollman   | ST_MAN  |            50 |
|         124 | Mourgos   | ST_MAN  |            50 |
|         145 | Russell   | SA_MAN  |            80 |
|         146 | Partners  | SA_MAN  |            80 |
|         147 | Errazuriz | SA_MAN  |            80 |
|         148 | Cambrault | SA_MAN  |            80 |
|         149 | Zlotkey   | SA_MAN  |            80 |
|         201 | Hartstein | MK_MAN  |            20 |
|         205 | Higgins   | AC_MGR  |           110 |
+-------------+-----------+---------+---------------+
18 rows in set (0.00 sec)

# 方式二:存在一條記錄,employees 表中的 manager_id 等於 employee_id,即該 employee 為管理者
mysql> SELECT employee_id, last_name, job_id, department_id FROM employees e1 WHERE EXISTS (SELECT * FROM employees e2 WHERE e2.manager_id = e1.employee_id);
+-------------+-----------+---------+---------------+
| employee_id | last_name | job_id  | department_id |
+-------------+-----------+---------+---------------+
|         100 | King      | AD_PRES |            90 |
|         101 | Kochhar   | AD_VP   |            90 |
|         102 | De Haan   | AD_VP   |            90 |
|         103 | Hunold    | IT_PROG |            60 |
|         108 | Greenberg | FI_MGR  |           100 |
|         114 | Raphaely  | PU_MAN  |            30 |
|         120 | Weiss     | ST_MAN  |            50 |
|         121 | Fripp     | ST_MAN  |            50 |
|         122 | Kaufling  | ST_MAN  |            50 |
|         123 | Vollman   | ST_MAN  |            50 |
|         124 | Mourgos   | ST_MAN  |            50 |
|         145 | Russell   | SA_MAN  |            80 |
|         146 | Partners  | SA_MAN  |            80 |
|         147 | Errazuriz | SA_MAN  |            80 |
|         148 | Cambrault | SA_MAN  |            80 |
|         149 | Zlotkey   | SA_MAN  |            80 |
|         201 | Hartstein | MK_MAN  |            20 |
|         205 | Higgins   | AC_MGR  |           110 |
+-------------+-----------+---------+---------------+
18 rows in set (0.00 sec)

# 方式三:相關子查詢
mysql> SELECT employee_id, last_name, job_id, department_id FROM employees WHERE employee_id IN (SELECT DISTINCT manager_id FROM employees);
+-------------+-----------+---------+---------------+
| employee_id | last_name | job_id  | department_id |
+-------------+-----------+---------+---------------+
|         100 | King      | AD_PRES |            90 |
|         101 | Kochhar   | AD_VP   |            90 |
|         102 | De Haan   | AD_VP   |            90 |
|         103 | Hunold    | IT_PROG |            60 |
|         108 | Greenberg | FI_MGR  |           100 |
|         114 | Raphaely  | PU_MAN  |            30 |
|         120 | Weiss     | ST_MAN  |            50 |
|         121 | Fripp     | ST_MAN  |            50 |
|         122 | Kaufling  | ST_MAN  |            50 |
|         123 | Vollman   | ST_MAN  |            50 |
|         124 | Mourgos   | ST_MAN  |            50 |
|         145 | Russell   | SA_MAN  |            80 |
|         146 | Partners  | SA_MAN  |            80 |
|         147 | Errazuriz | SA_MAN  |            80 |
|         148 | Cambrault | SA_MAN  |            80 |
|         149 | Zlotkey   | SA_MAN  |            80 |
|         201 | Hartstein | MK_MAN  |            20 |
|         205 | Higgins   | AC_MGR  |           110 |
+-------------+-----------+---------+---------------+
18 rows in set (0.00 sec)

查詢 departments 表中,不存在於 employees 表中的部門的 department_id 和 department_name:

mysql> SELECT department_id, department_name FROM departments d WHERE NOT EXISTS (SELECT * FROM employees e WHERE e.department_id = d.department_id);
+---------------+----------------------+
| department_id | department_name      |
+---------------+----------------------+
|           120 | Treasury             |
|           130 | Corporate Tax        |
|           140 | Control And Credit   |
|           150 | Shareholder Services |
|           160 | Benefits             |
|           170 | Manufacturing        |
|           180 | Construction         |
|           190 | Contracting          |
|           200 | Operations           |
|           210 | IT Support           |
|           220 | NOC                  |
|           230 | IT Helpdesk          |
|           240 | Government Sales     |
|           250 | Retail Sales         |
|           260 | Recruiting           |
|           270 | Payroll              |
+---------------+----------------------+
16 rows in set (0.00 sec)

相關更新

語法:

UPDATE table1 alias1
SET column = (SELECT expression FROM table2 alias2 WHERE alias1.column = alias2.column);

使用相關子查詢依據一個表中的資料更新另一個表的資料。

在 employees 中增加一個 department_name 欄位,資料為員工對應的部門名稱:

mysql> ALTER TABLE employees ADD(department_name VARCHAR(50));
Query OK, 0 rows affected (0.04 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> UPDATE employees e SET department_name = (SELECT department_name FROM departments d WHERE e.department_id = d.department_id);
Query OK, 106 rows affected (0.01 sec)

相關刪除

語法:

DELETE FROM table1 alias1
WHERE column operator (SELECT expression FROM table2 alias2 WHERE alias1.column = alias2.column);

使用相關子查詢依據一個表中的資料刪除另一個表的資料。

刪除表 employees 中,與 emp_history 表皆有的資料:

mysql> DELETE FROM employees e WHERE e.employee_id IN (SELECT eh.employee_id FROM emp_history eh WHERE eh.employee_id = e.employee_id);

子查詢與自連線的對比

查詢工資比 Abel 高的員工資訊:

# 方式一:子查詢
mysql> SELECT last_name, salary FROM employees WHERE salary > (SELECT salary FROM employees WHERE last_name = 'Abel');

# 方式二:自連線
mysql> SELECT e2.last_name, e2.salary FROM employees e1, employees e2 WHERE e1.last_name = 'Abel' AND e1.salary < e2.salary;

此情況中,即可以使用子查詢,也可以使用自連線。一般情況建議使用自連線,因為在許多 DBMS 的處理過程中,對於自連線的處理速度要比子查詢快得多。

可以這樣理解:子查詢實際上是透過未知表進行查詢後的條件判斷,而自連線是透過已知的自身資料表進行條件判斷,因此在大部分 DBMS 中都對自連線處理進行了最佳化。

原文連結

https://github.com/ACatSmiling/zero-to-zero/blob/main/RelationalDatabase/mysql.md

相關文章