本文 AI 產出,尚未審核

MySQL 子查詢(Subquery)── FROM 子查詢深入剖析

簡介

在日常的資料庫開發中,我們常常需要一次取得多張表格的彙整結果,或是對某筆資料先做過濾再與其他資料做聯結。FROM 子查詢(又稱「派生表」或「內嵌視圖」)正是解決這類需求的利器。它允許我們把一段完整的 SELECT 結果,當作臨時表格放進外層查詢的 FROM 子句中,從而實現更靈活且易於維護的查詢邏輯。

對於剛入門的開發者而言,FROM 子查詢可能看起來有些複雜;但只要掌握其核心概念與常見寫法,就能在報表、統計、資料清理等實務情境中大幅提升查詢效率與可讀性。


核心概念

1. 什麼是 FROM 子查詢?

FROM 子查詢是一段完整的 SELECT 語句,放在外層查詢的 FROM 子句裡,並為它指定一個 別名(alias),讓外層查詢可以像對待普通資料表一樣對待它。

SELECT *
FROM (SELECT col1, col2 FROM tableA WHERE condition) AS derived_tbl
WHERE derived_tbl.col1 > 100;
  • 臨時性:子查詢只在當前執行的查詢期間存在,執行完畢即釋放。
  • 別名必須:MySQL 必須為子查詢指定別名,否則會拋出錯誤。
  • 可堆疊:子查詢本身也可以再包含子查詢,形成多層嵌套。

2. 為什麼使用 FROM 子查詢?

需求 使用 FROM 子查詢的好處
彙總統計再過濾 先在子查詢中做 GROUP BY,再在外層過濾聚合結果,避免 HAVINGWHERE 混用的困惑。
資料前處理 只取出符合條件的欄位或做欄位計算後,再與其他表格 JOIN,提高可讀性。
避免重複計算 把一次性計算的結果存成子查詢,外層多次使用時不必重複執行相同的運算。
模擬視圖 若不想正式建立視圖,可在查詢中即時使用子查詢達到相同效果。

3. 基本語法結構

SELECT outer_col1, outer_col2, ...
FROM (
    SELECT inner_col1, inner_col2, ...
    FROM inner_table
    WHERE inner_condition
) AS sub_alias
WHERE outer_condition;
  • inner_table:子查詢內部操作的原始資料表。
  • sub_alias:子查詢必須的別名,可自行命名。
  • outer_condition:外層查詢的過濾條件,常用於對聚合結果進一步篩選。

程式碼範例

以下範例皆以 MySQL 8.0+ 為基礎,實務上可直接貼到 MySQL Workbench 或其他客戶端執行。

範例 1:先彙總再過濾(銷售額前 5 名的產品)

-- 先算出每個產品的總銷售額,再挑出前 5 名
SELECT p.product_id, p.total_sales
FROM (
    SELECT product_id, SUM(amount) AS total_sales
    FROM sales
    GROUP BY product_id
) AS p
ORDER BY p.total_sales DESC
LIMIT 5;

說明:子查詢 p 產生每個產品的總銷售額,外層查詢負責排序與限制筆數。

範例 2:結合子查詢與 JOIN(找出最近一次購買的客戶資訊)

SELECT c.customer_id, c.name, recent.last_order_date, recent.amount
FROM customers AS c
JOIN (
    SELECT customer_id, MAX(order_date) AS last_order_date, amount
    FROM orders
    GROUP BY customer_id
) AS recent
ON c.customer_id = recent.customer_id
WHERE recent.amount > 500;

說明:子查詢先算出每位客戶的最新訂單日期與金額,外層再把客戶資料 JOIN 進來,最後只保留金額大於 500 的紀錄。

範例 3:子查詢作為「過濾條件」的來源(找出在同一天內下過兩筆以上訂單的客戶)

SELECT *
FROM (
    SELECT customer_id, order_date, COUNT(*) AS cnt
    FROM orders
    GROUP BY customer_id, order_date
) AS daily_cnt
WHERE daily_cnt.cnt >= 2;

說明:子查詢先統計每位客戶每日的訂單筆數,外層僅挑選出筆數 ≥ 2 的紀錄。

範例 4:多層子查詢(計算每個部門的員工平均薪資,並挑出平均薪資高於公司整體平均值的部門)

SELECT dept.dept_name, dept.avg_salary
FROM (
    SELECT d.dept_id, d.dept_name, AVG(e.salary) AS avg_salary
    FROM departments d
    JOIN employees e ON d.dept_id = e.dept_id
    GROUP BY d.dept_id, d.dept_name
) AS dept
WHERE dept.avg_salary > (
    SELECT AVG(salary) FROM employees
);

說明:內層子查詢算出各部門的平均薪資,外層再比對公司整體平均薪資,只保留高於整體平均的部門。

範例 5:使用子查詢模擬視圖(即時產生月份銷售彙總)

SELECT *
FROM (
    SELECT DATE_FORMAT(order_date, '%Y-%m') AS month, SUM(amount) AS month_sales
    FROM orders
    GROUP BY month
) AS monthly_sales
WHERE monthly_sales.month_sales > 10000
ORDER BY monthly_sales.month;

說明:子查詢把每個月份的銷售額彙總起來,外層再過濾出銷售額超過 10,000 的月份。


常見陷阱與最佳實踐

陷阱 說明 解決方式
忘記給子查詢別名 MySQL 會拋出 ERROR 1248: Every derived table must have its own alias 必須 為每個子查詢指定別名,例如 AS sub
過度嵌套 多層子查詢會讓執行計畫變得複雜,甚至導致效能下降。 盡量將可重用的邏輯抽成 視圖暫存表(Temporary Table)。
忽略索引 子查詢內的過濾條件若未使用索引,整體查詢會變慢。 在子查詢涉及的欄位上建立適當的 索引,或使用 EXPLAIN 確認執行計畫。
使用 SELECT * 會把所有欄位都帶入外層,浪費 I/O。 只挑選需要的欄位,降低資料傳輸量。
子查詢返回多筆而外層期待單筆 如在 WHERE col = (SELECT ...) 中返回多筆會錯誤。 使用 IN 或改寫為 JOIN

最佳實踐

  1. 明確命名別名:使用具意義的別名(如 dept_salesrecent_orders),提升可讀性。
  2. 適度使用聚合:若子查詢只做聚合,盡量在子查詢內完成 HAVING,減少外層過濾。
  3. 結合 EXPLAIN:在開發階段,用 EXPLAIN 檢視子查詢與外層查詢的執行計畫,確保使用索引。
  4. 考慮視圖:若同一子查詢會被多個報表共用,建立 永久視圖(VIEW)或 暫存表,減少重複編寫。
  5. 限制結果集:在子查詢中加入 LIMIT(若適用),可減少不必要的資料傳遞。

實際應用場景

場景 典型需求 FROM 子查詢的角色
月度業績報表 先彙總每位業務的月銷售,再挑選出達標者。 子查詢先算出 sales_by_month,外層過濾 target
客戶行為分析 找出在同一天內多次點擊的使用者。 子查詢統計每日點擊次數,外層過濾次數 ≥ 2。
財務審核 比較部門平均薪資與全公司平均薪資。 兩層子查詢:第一層算部門平均,第二層算公司總平均。
即時儀表板 顯示最近 7 天內的活躍使用者數量。 子查詢先篩選最近 7 天的紀錄,外層再做 COUNT(DISTINCT user_id)
資料清理 找出有重複資料的欄位並列出其主鍵。 子查詢找出重複欄位值,外層 JOIN 原表取得主鍵。

總結

FROM 子查詢是 MySQL 中一個強大且靈活的工具,讓我們可以先在子層完成資料的過濾、彙總或計算,再於外層進行更進一步的處理。掌握它的正確寫法(別名、適當的欄位選擇)與效能考量(索引、避免過度嵌套),即可在日常開發、報表產出與資料分析中事半功倍。

關鍵回顧

  • 必須為每個子查詢指定別名。
  • 子查詢適合做一次性的聚合或前置處理。
  • 使用 EXPLAIN 與適當索引確保效能。
  • 若子查詢被多次使用,考慮改為視圖或暫存表。

透過本篇的概念說明與實務範例,你現在應該能夠在自己的 MySQL 專案裡,熟練運用 FROM 子查詢 來解決複雜的資料需求,寫出更清晰、可維護且效能良好的 SQL。祝你寫查詢順利,資料庫永遠跑得快!