概述#

如果你的履歷上有資料庫相關經驗,面試中可能會被問到這類問題。本章涵蓋關鍵概念及常見的查詢撰寫技巧。

SQL 有多種語法變體,本書範例以 Microsoft SQL Server 為基準進行測試。面試中遇到小幅語法差異是正常的。


SQL 語法與變體(SQL Syntax and Variations)#

Explicit JoinImplicit Join 兩者等效,本書統一使用 Explicit Join:

-- Explicit Join
SELECT CourseName, TeacherName
FROM Courses INNER JOIN Teachers
ON Courses.TeacherID = Teachers.TeacherID

-- Implicit Join(等效)
SELECT CourseName, TeacherName
FROM Courses, Teachers
WHERE Courses.TeacherID = Teachers.TeacherID

正規化 vs. 反正規化(Denormalized vs. Normalized Databases)#

  • Normalized(正規化):設計以最小化冗餘資料為目標。例如 Courses 表透過 TeacherID 外鍵連結 Teachers 表,每位老師的資訊只儲存一次。缺點是許多查詢需要昂貴的 JOIN 操作。
  • Denormalized(反正規化):透過儲存冗餘資料來加速讀取。例如直接在 Courses 表中儲存 TeacherName,避免 JOIN。反正規化常用於建立高擴展性的系統。

在大型、高流量系統中,JOIN 操作代價很高,通常需要採用反正規化或 NoSQL 來提升效能。


SQL 語句(SQL Statements)#

資料庫結構範例#

Courses:       CourseID*, CourseName, TeacherID
Teachers:      TeacherID*, TeacherName
Students:      StudentID*, StudentName
StudentCourses: CourseID*, StudentID*

Query 1:學生選課數量#

取得所有學生及其選修課程數量:

-- 錯誤版本(有三個問題)
SELECT Students.StudentName, count(*)
FROM Students INNER JOIN StudentCourses
ON Students.StudentID = StudentCourses.StudentID
GROUP BY Students.StudentID

問題分析:

  1. 未選課的學生會被排除(應使用 LEFT JOIN
  2. count(*) 會把未選課學生算成 1(應改為 count(StudentCourses.CourseID)
  3. GROUP BY StudentID 後,StudentNameSELECT 中必須使用 aggregate function

解法一:用子查詢包裝

SELECT StudentName, Students.StudentID, Cnt
FROM (
    SELECT Students.StudentID, count(StudentCourses.CourseID) as [Cnt]
    FROM Students LEFT JOIN StudentCourses
    ON Students.StudentID = StudentCourses.StudentID
    GROUP BY Students.StudentID
) T INNER JOIN Students on T.studentID = Students.StudentID

解法二:將 StudentName 加入 GROUP BY

SELECT StudentName, Students.StudentID, count(StudentCourses.CourseID) as [Cnt]
FROM Students LEFT JOIN StudentCourses
ON Students.StudentID = StudentCourses.StudentID
GROUP BY Students.StudentID, Students.StudentName

解法三:對 StudentName 使用 aggregate function

SELECT max(StudentName) as [StudentName], Students.StudentID,
       count(StudentCourses.CourseID) as [Count]
FROM Students LEFT JOIN StudentCourses
ON Students.StudentID = StudentCourses.StudentID
GROUP BY Students.StudentID

Query 2:每位老師教導的學生數#

取得所有老師及其學生數量,若同一位老師在兩門課教同一位學生,需重複計算。結果按學生數降序排列:

-- 步驟一:取得每位老師對應的 course-student 數量
SELECT TeacherID, count(StudentCourses.CourseID) AS [Number]
FROM Courses INNER JOIN StudentCourses
ON Courses.CourseID = StudentCourses.CourseID
GROUP BY Courses.TeacherID

-- 完整查詢(包含沒有開課的老師,以 0 表示)
SELECT TeacherName, isnull(StudentSize.Number, 0)
FROM Teachers LEFT JOIN
    (SELECT TeacherID, count(StudentCourses.CourseID) AS [Number]
     FROM Courses INNER JOIN StudentCourses
     ON Courses.CourseID = StudentCourses.CourseID
     GROUP BY Courses.TeacherID) StudentSize
ON Teachers.TeacherID = StudentSize.TeacherID
ORDER BY StudentSize.Number DESC

使用 isnull() 將 NULL 值轉換為 0,確保沒有學生的老師也能正確顯示。


小型資料庫設計(Small Database Design)#

設計資料庫時,方法類似於物件導向設計。

Step 1:釐清模糊之處(Handle Ambiguity)#

資料庫設計題通常有歧義,必須在動手前確認需求。

例如被要求設計公寓租賃仲介的系統時,需要確認:

  • 仲介有幾個據點?
  • 一個人可以租多間公寓嗎?
  • 非常罕見的情況要處理到什麼程度?

Step 2:定義核心物件(Define the Core Objects)#

找出系統的核心物件,每個物件通常對應一張資料表。例如公寓租賃仲介的核心物件: PropertyBuildingApartmentTenantManager

Step 3:分析關係(Analyze Relationships)#

確認各資料表之間的關係:一對多或多對多?

BuildingsApartments 是一對多關係:

ApartmentsBuildings
ApartmentIDintBuildingIDint
ApartmentAddressvarchar(100)BuildingNamevarchar(100)
BuildingIDintBuildingAddressvarchar(500)

若允許一個租客租多間公寓(多對多關係),需要一張中介表:

TenantApartmentsApartmentsTenants
TenantIDintApartmentIDintTenantIDint
ApartmentIDintApartmentAddressvarchar(500)TenantNamevarchar(100)
BuildingIDintTenantAddressvarchar(500)

Step 4:補充細節(Investigate Actions)#

走過系統中常見的操作,確認需要哪些額外的資料欄位。例如:租約期限、搬出紀錄、租金付款等,每一種操作都可能需要新的資料表或欄位。


大型資料庫設計(Large Database Design)#

在大型、可擴展的資料庫中,JOIN 操作通常很慢。因此必須對資料進行反正規化,將常用資料複製到多張資料表中,以空間換取查詢速度。

反正規化會引入資料一致性的挑戰——同一筆資料存在多處,更新時需要同步所有副本。這是大型系統設計中必須權衡的取捨。


面試題目列表#

以下題目 1-3 使用下列資料庫結構(每間公寓可有多位租客,每位租客可租多間公寓,每間公寓屬於一棟樓,每棟樓屬於一個社區):

ApartmentsBuildingsRequests
AptIDintBuildingIDintRequestIDint
UnitNumbervarchar(10)ComplexIDintStatusvarchar(100)
BuildingIDintBuildingNamevarchar(100)AptIDint
Addressvarchar(500)Descriptionvarchar(500)
ComplexesAptTenantsTenants
ComplexIDintTenantIDintTenantIDint
ComplexNamevarchar(100)AptIDintTenantNamevarchar(100)
題號題目
14.1Multiple Apartments:查詢租了不只一間公寓的所有租客
14.2Open Requests:查詢所有樓棟及其 Open 狀態的請求數量
14.3Close All Requests:關閉 #11 棟樓所有公寓的所有請求
14.4Joins:解釋各種 JOIN 類型及其適用情境
14.5Denormalization:什麼是反正規化?說明其優缺點
14.6Entity-Relationship Diagram:為公司、人員、職業人士畫出 ER 圖
14.7Design Grade Database:設計學生成績資料庫,並查詢前 10% 的學生