☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
資料庫 中級

SQL 進階:JOIN、子查詢、CTE

學會了基本 CRUD 之後,我們要學習如何跨表查詢,這才是 SQL 真正強大的地方。

想像你有一本「學生名冊」和一本「成績單」,JOIN 就是把兩本冊子攤開來,用學號把資料對在一起看。


JOIN 的種類

INNER JOIN — 交集

只回傳兩邊都有對應資料的列。

-- 查詢有選課的學生及其課程名稱
SELECT s.Name, c.CourseName, e.Score  -- 選取學生姓名、課程名稱、分數
FROM Students s                        -- 從學生表開始
INNER JOIN Enrollments e               -- 內連接選課表
    ON s.Id = e.StudentId              -- 用學生 ID 對應
INNER JOIN Courses c                   -- 再連接課程表
    ON e.CourseId = c.Id;              -- 用課程 ID 對應

LEFT JOIN — 左邊全部保留

-- 查詢所有學生(包含沒選課的)
SELECT s.Name, c.CourseName            -- 選取姓名和課程
FROM Students s                        -- 左表:學生(全部保留)
LEFT JOIN Enrollments e                -- 左連接選課表
    ON s.Id = e.StudentId              -- 用學生 ID 對應
LEFT JOIN Courses c                    -- 再左連接課程表
    ON e.CourseId = c.Id;              -- 用課程 ID 對應
-- 沒選課的學生,CourseName 會顯示 NULL

RIGHT JOIN — 右邊全部保留

-- 查詢所有課程(包含沒人選的)
SELECT s.Name, c.CourseName            -- 選取姓名和課程
FROM Students s                        -- 左表:學生
RIGHT JOIN Enrollments e               -- 右連接選課表
    ON s.Id = e.StudentId              -- 用學生 ID 對應
RIGHT JOIN Courses c                   -- 再右連接課程表
    ON e.CourseId = c.Id;              -- 用課程 ID 對應
-- 沒人選的課程,Name 會顯示 NULL

FULL OUTER JOIN — 兩邊全部保留

-- 查詢所有學生和所有課程的配對
SELECT s.Name, c.CourseName            -- 選取姓名和課程
FROM Students s                        -- 左表:學生
FULL OUTER JOIN Enrollments e          -- 完全外連接選課表
    ON s.Id = e.StudentId              -- 用學生 ID 對應
FULL OUTER JOIN Courses c              -- 再完全外連接課程表
    ON e.CourseId = c.Id;              -- 用課程 ID 對應
-- 兩邊沒對應到的都會保留,缺少的部分顯示 NULL

子查詢(Subquery)

子查詢就是查詢裡面再包一個查詢,像是俄羅斯娃娃一樣。

-- 找出分數高於平均的學生
SELECT Name, Score                     -- 選取姓名和分數
FROM Students                          -- 從學生表查詢
WHERE Score > (                        -- 分數大於...
    SELECT AVG(Score) FROM Students    -- ...所有學生的平均分數
);

相關子查詢(Correlated Subquery)

每一列都會執行一次子查詢,效能較差但功能強大。

-- 找出每門課的最高分學生
SELECT s.Name, e.CourseId, e.Score     -- 選取姓名、課程 ID、分數
FROM Students s                        -- 從學生表
JOIN Enrollments e                     -- 連接選課表
    ON s.Id = e.StudentId              -- 用學生 ID 對應
WHERE e.Score = (                      -- 分數等於...
    SELECT MAX(e2.Score)               -- ...該課程的最高分
    FROM Enrollments e2                -- 從選課表
    WHERE e2.CourseId = e.CourseId     -- 條件:同一門課(這裡參考了外層查詢)
);

CTE(Common Table Expression)

CTE 用 WITH ... AS 語法,把複雜查詢拆成有名字的暫時結果集,讓 SQL 更好讀。

-- 用 CTE 計算每個學生的平均分數,再篩選及格的
WITH StudentAvg AS (                   -- 定義一個叫 StudentAvg 的暫時結果集
    SELECT                             -- 查詢內容
        StudentId,                     -- 學生 ID
        AVG(Score) AS AvgScore         -- 計算平均分數
    FROM Enrollments                   -- 從選課表
    GROUP BY StudentId                 -- 依學生分組
)
SELECT s.Name, sa.AvgScore             -- 從 CTE 和學生表選取資料
FROM StudentAvg sa                     -- 使用剛才定義的 CTE
JOIN Students s                        -- 連接學生表
    ON sa.StudentId = s.Id             -- 用學生 ID 對應
WHERE sa.AvgScore >= 60                -- 只要平均及格的
ORDER BY sa.AvgScore DESC;             -- 依平均分數降序排列

Window Functions(窗口函數)

窗口函數可以在不改變原始列數的情況下,計算排名、累計等聚合值。

-- 在每門課中計算學生排名
SELECT
    s.Name,                            -- 學生姓名
    c.CourseName,                      -- 課程名稱
    e.Score,                           -- 分數
    ROW_NUMBER() OVER (                -- 依每門課的分數排序,給予流水編號
        PARTITION BY e.CourseId        -- 依課程分組(每門課獨立排名)
        ORDER BY e.Score DESC          -- 依分數降序
    ) AS RowNum,
    RANK() OVER (                      -- RANK 遇到同分會跳號
        PARTITION BY e.CourseId        -- 同樣依課程分組
        ORDER BY e.Score DESC          -- 依分數降序
    ) AS ScoreRank,
    DENSE_RANK() OVER (                -- DENSE_RANK 遇到同分不跳號
        PARTITION BY e.CourseId        -- 依課程分組
        ORDER BY e.Score DESC          -- 依分數降序
    ) AS DenseRank
FROM Enrollments e                     -- 從選課表
JOIN Students s ON s.Id = e.StudentId  -- 連接學生表
JOIN Courses c ON c.Id = e.CourseId;   -- 連接課程表

ROW_NUMBER vs RANK vs DENSE_RANK:假設分數是 100, 95, 95, 90

  • ROW_NUMBER: 1, 2, 3, 4(不管同分,流水號)
  • RANK: 1, 2, 2, 4(同分同名次,跳號)
  • DENSE_RANK: 1, 2, 2, 3(同分同名次,不跳號)

🤔 我這樣寫為什麼會錯?

❌ 錯誤 1:忘記 ON 條件,產生 Cartesian Join

-- ❌ 忘記 ON 條件 → 兩張表的每一列都會配對!
SELECT s.Name, c.CourseName            -- 選取姓名和課程
FROM Students s, Courses c;            -- 這會產生 M × N 列的結果!
-- 如果學生有 100 人,課程有 50 門 → 回傳 5000 列 😱
-- ✅ 正確寫法:用 JOIN + ON 指定關聯條件
SELECT s.Name, c.CourseName            -- 選取姓名和課程
FROM Students s                        -- 從學生表
JOIN Enrollments e ON s.Id = e.StudentId -- 透過選課表連接
JOIN Courses c ON e.CourseId = c.Id;     -- 再連接課程表

❌ 錯誤 2:NULL 在 JOIN 中的陷阱

-- ❌ NULL 不等於任何值(包括另一個 NULL)
-- 如果 DepartmentId 為 NULL 的學生不會出現在 INNER JOIN 結果中
SELECT s.Name, d.DeptName              -- 選取姓名和部門名稱
FROM Students s                        -- 從學生表
INNER JOIN Departments d               -- 內連接部門表
    ON s.DepartmentId = d.Id;          -- NULL != 任何值 → 這些列被排除
-- ✅ 如果要保留沒有部門的學生,用 LEFT JOIN
SELECT s.Name, ISNULL(d.DeptName, N'未分配') AS DeptName -- 用 ISNULL 處理 NULL
FROM Students s                        -- 從學生表
LEFT JOIN Departments d                -- 左連接保留所有學生
    ON s.DepartmentId = d.Id;          -- NULL 的學生也會保留

❌ 錯誤 3:在 WHERE 子句中對 LEFT JOIN 的右表篩選

-- ❌ 這等於把 LEFT JOIN 變成 INNER JOIN!
SELECT s.Name, c.CourseName            -- 選取姓名和課程
FROM Students s                        -- 從學生表
LEFT JOIN Courses c                    -- 左連接課程表
    ON s.CourseId = c.Id               -- 連接條件
WHERE c.IsActive = 1;                  -- 這會過濾掉 NULL(等於 INNER JOIN)
-- ✅ 條件放在 ON 裡面,才能保留左表所有列
SELECT s.Name, c.CourseName            -- 選取姓名和課程
FROM Students s                        -- 從學生表
LEFT JOIN Courses c                    -- 左連接課程表
    ON s.CourseId = c.Id               -- 連接條件
    AND c.IsActive = 1;               -- 篩選條件放在 ON 裡

💡 重點整理

JOIN 類型 說明
INNER JOIN 只回傳兩邊都有的(交集)
LEFT JOIN 左邊全部保留,右邊沒有的填 NULL
RIGHT JOIN 右邊全部保留,左邊沒有的填 NULL
FULL OUTER JOIN 兩邊全部保留
CTE 把子查詢取名字,提高可讀性
Window Function 不減少列數的聚合計算

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊