資料庫查詢的核心目的是從大量資料中精確擷取所需的資訊。WHERE 子句作為 SQL 查詢的關鍵組成部分,提供了豐富多樣的篩選機制,讓我們能夠根據各種條件精確地過濾資料。從簡單的相等比較到複雜的模式比對,從數值範圍檢查到集合成員判斷,這些篩選運算子共同構成了資料查詢的強大工具箱。
掌握各種篩選條件的特性和適用場景,不僅能幫助我們撰寫出功能正確的查詢,更能讓我們寫出效能優異的 SQL 語句。不同的篩選運算子在執行效率上可能存在顯著差異,某些寫法可以善用索引加速查詢,而另一些寫法則可能導致全表掃描。本文將深入探討各種篩選條件的語法、功能和效能特性,並透過實際案例說明如何選擇最適合的篩選策略。
集合運算子 IN 與 NOT IN
當我們需要檢查某個欄位的值是否屬於一組預定義的值時,IN 運算子提供了簡潔而直觀的語法。相較於使用多個 OR 條件,IN 運算子不僅程式碼更簡潔,在許多資料庫系統中執行效率也更高。
考慮一個葡萄酒資料庫的查詢場景。假設我們想要找出特定幾種葡萄品種的所有酒款:
-- 使用 IN 運算子查詢特定葡萄品種的酒款
-- 比使用多個 OR 條件更簡潔且效率更高
SELECT
wine_id,
wine_name,
grape_variety,
vintage_year,
price
FROM wine
WHERE grape_variety IN ('Chardonnay', 'Riesling', 'Sauvignon Blanc')
ORDER BY grape_variety, vintage_year DESC;
這個查詢會傳回所有 grape_variety 為 Chardonnay、Riesling 或 Sauvignon Blanc 的酒款記錄。IN 運算子的等價寫法是使用多個 OR 條件:
-- 使用 OR 條件的等價寫法
-- 當條件數量增加時,可讀性會大幅降低
SELECT
wine_id,
wine_name,
grape_variety,
vintage_year,
price
FROM wine
WHERE grape_variety = 'Chardonnay'
OR grape_variety = 'Riesling'
OR grape_variety = 'Sauvignon Blanc'
ORDER BY grape_variety, vintage_year DESC;
NOT IN 運算子則用於篩選不屬於指定集合的記錄。例如,如果我們想要找出除了 Chardonnay 和 Riesling 以外的所有葡萄酒類型:
-- 使用 NOT IN 排除特定的葡萄品種
-- 查詢所有不是 Chardonnay 和 Riesling 的酒款
SELECT
wine_type_id,
wine_type_name,
description,
typical_serving_temp
FROM wine_type
WHERE wine_type_name NOT IN ('Chardonnay', 'Riesling')
ORDER BY wine_type_name;
在使用 NOT IN 時需要特別注意 NULL 值的處理。如果 NOT IN 的列表中包含 NULL,或者被比較的欄位含有 NULL 值,可能會產生非預期的結果。這是因為任何值與 NULL 的比較結果都是 UNKNOWN,而非 TRUE 或 FALSE:
-- NOT IN 與 NULL 值的陷阱
-- 假設某些酒款的 grape_variety 為 NULL
SELECT wine_name
FROM wine
WHERE grape_variety NOT IN ('Chardonnay', NULL); -- 這個查詢不會傳回任何結果!
-- 正確的處理方式:明確排除 NULL 值
SELECT wine_name
FROM wine
WHERE grape_variety NOT IN ('Chardonnay', 'Riesling')
AND grape_variety IS NOT NULL;
IN 運算子還可以與子查詢結合使用,這是一種非常常見的應用模式:
-- 使用子查詢的 IN 運算子
-- 找出有獲獎記錄的酒莊所生產的所有酒款
SELECT
w.wine_name,
w.vintage_year,
winery.winery_name
FROM wine w
INNER JOIN winery ON w.winery_id = winery.winery_id
WHERE w.winery_id IN (
SELECT DISTINCT winery_id
FROM wine_award
WHERE award_year >= 2020
)
ORDER BY winery.winery_name, w.vintage_year;
範圍查詢 BETWEEN 與 NOT BETWEEN
BETWEEN 運算子提供了檢查數值或日期是否落在特定範圍內的簡潔語法。它是一個包含邊界的範圍檢查,等同於使用 >= 和 <= 的組合條件。
-- 使用 BETWEEN 查詢特定年份範圍出生的客戶
-- 查詢 1981 至 1996 年出生的千禧世代
SELECT
customer_id,
customer_name,
birth_year,
email,
registration_date
FROM customer
WHERE birth_year BETWEEN 1981 AND 1996
ORDER BY birth_year, customer_name;
這個查詢等同於:
-- BETWEEN 的等價寫法
SELECT
customer_id,
customer_name,
birth_year,
email,
registration_date
FROM customer
WHERE birth_year >= 1981 AND birth_year <= 1996
ORDER BY birth_year, customer_name;
BETWEEN 運算子在處理日期範圍查詢時特別實用:
-- 查詢特定日期範圍內的訂單
-- 包含起始日和結束日
SELECT
order_id,
customer_id,
order_date,
total_amount,
status
FROM orders
WHERE order_date BETWEEN '2024-01-01' AND '2024-03-31'
ORDER BY order_date DESC;
需要注意的是,當 BETWEEN 用於日期時間欄位時,邊界值的處理可能因資料庫系統和欄位類型而異。如果欄位包含時間部分,使用 BETWEEN 時要特別小心:
-- 日期時間欄位的 BETWEEN 查詢
-- 可能會遺漏 2024-03-31 當天有時間部分的記錄
SELECT order_id, order_datetime
FROM orders
WHERE order_datetime BETWEEN '2024-01-01' AND '2024-03-31';
-- 更精確的寫法:使用 < 避免邊界問題
SELECT order_id, order_datetime
FROM orders
WHERE order_datetime >= '2024-01-01'
AND order_datetime < '2024-04-01';
NOT BETWEEN 運算子用於篩選不在指定範圍內的記錄:
-- 使用 NOT BETWEEN 查詢非千禧世代的客戶
-- 包含 1981 年之前和 1996 年之後出生的客戶
SELECT
customer_id,
customer_name,
birth_year,
CASE
WHEN birth_year < 1965 THEN 'Baby Boomer'
WHEN birth_year BETWEEN 1965 AND 1980 THEN 'Generation X'
WHEN birth_year > 1996 THEN 'Generation Z'
ELSE 'Unknown'
END AS generation
FROM customer
WHERE birth_year NOT BETWEEN 1981 AND 1996
ORDER BY birth_year;
BETWEEN 也可以用於字串的範圍比較,這在某些場景下很有用:
-- 查詢姓氏以 A 到 M 開頭的客戶
-- 字串比較基於字元的排序順序
SELECT customer_id, last_name, first_name
FROM customer
WHERE last_name BETWEEN 'A' AND 'M'
ORDER BY last_name;
模式比對 LIKE 與 NOT LIKE
LIKE 運算子是 SQL 中進行模式比對的主要工具。它支援兩種萬用字元:百分號(%)代表任意數量的字元(包括零個字元),底線(_)代表恰好一個任意字元。這兩種萬用字元的組合使得 LIKE 運算子能夠處理各種複雜的字串比對需求。
百分號萬用字元的應用
百分號萬用字元可以匹配任意長度的字元序列,包括空字串。根據百分號放置的位置不同,可以實現前綴匹配、後綴匹配或包含匹配:
-- 前綴匹配:查詢姓氏以 M 開頭的億萬富翁
-- 百分號放在模式末尾
SELECT
billionaire_id,
first_name,
last_name,
net_worth_billions,
industry
FROM billionaire
WHERE last_name LIKE 'M%'
ORDER BY net_worth_billions DESC;
這個查詢會傳回所有 last_name 以 M 開頭的記錄,包括 Musk、Melnichenko、Ma 等姓氏。
-- 後綴匹配:查詢姓名以 son 結尾的人
-- 百分號放在模式開頭
SELECT
billionaire_id,
first_name,
last_name
FROM billionaire
WHERE last_name LIKE '%son'
ORDER BY last_name;
-- 包含匹配:查詢姓氏中包含字母 e 的億萬富翁
-- 百分號放在模式兩端
SELECT
billionaire_id,
first_name,
last_name,
net_worth_billions
FROM billionaire
WHERE last_name LIKE '%e%'
ORDER BY last_name;
這個查詢會傳回所有姓氏中包含 e 的記錄,例如 Bezos、Gates、Melnichenko 等。
底線萬用字元的應用
底線萬用字元精確匹配一個字元,適用於已知字元數量但不確定具體字元的情況:
-- 查詢恰好三個字元且以 at 結尾的術語
-- 底線匹配第一個位置的任意字元
SELECT term_id, term, definition
FROM three_letter_term
WHERE term LIKE '_at'
ORDER BY term;
這個查詢會傳回如 cat、hat、mat、bat、rat 等符合模式的術語。
-- 查詢第二個字元為 a 的四字母單字
-- 結合底線和百分號萬用字元
SELECT word_id, word
FROM vocabulary
WHERE word LIKE '_a__'
ORDER BY word;
複合模式的應用
萬用字元可以組合使用來建立更複雜的比對模式:
-- 查詢電子郵件地址符合特定模式的客戶
-- 例如:第三個字元為 a,且域名為 gmail.com
SELECT
customer_id,
customer_name,
email
FROM customer
WHERE email LIKE '__a%@gmail.com'
ORDER BY customer_name;
NOT LIKE 的應用
NOT LIKE 運算子用於排除符合特定模式的記錄:
-- 查詢姓氏不以 M 開頭的億萬富翁
SELECT
billionaire_id,
first_name,
last_name,
net_worth_billions
FROM billionaire
WHERE last_name NOT LIKE 'M%'
ORDER BY net_worth_billions DESC;
-- 查詢不以 at 結尾的三字母術語
SELECT term_id, term, definition
FROM three_letter_term
WHERE term NOT LIKE '_at'
ORDER BY term;
實際應用案例
讓我們看一個更複雜的實際應用案例。假設我們有一個音樂家資料表,需要查詢來自納什維爾地區的歌手。納什維爾的電話號碼區碼為 615 或 629:
-- 查詢納什維爾地區的歌手
-- 使用 LIKE 同時匹配音樂家類型和電話區碼
SELECT
musician_id,
musician_name,
phone,
musician_type,
specialty
FROM musician
WHERE musician_type LIKE '%Singer%' -- 類型包含 Singer
AND (phone LIKE '615-%' -- 電話以 615 開頭
OR phone LIKE '629-%') -- 或以 629 開頭
ORDER BY musician_name;
這個查詢展示了如何結合多個 LIKE 條件來實現複雜的篩選邏輯。注意使用括號來正確分組 OR 條件,否則可能產生非預期的結果。
效能考量
LIKE 運算子的效能特性需要特別關注。當百分號放在模式開頭時(如 ‘%text’),資料庫無法使用索引,必須進行全表掃描:
-- 效能較差:模式以 % 開頭,無法使用索引
SELECT * FROM customer WHERE email LIKE '%@gmail.com';
-- 效能較佳:模式以字元開頭,可以使用索引
SELECT * FROM customer WHERE email LIKE 'john%';
如果經常需要進行後綴或包含搜尋,可以考慮以下最佳化策略:
-- 建立反轉欄位以支援後綴搜尋
ALTER TABLE customer ADD COLUMN email_reversed VARCHAR(255);
UPDATE customer SET email_reversed = REVERSE(email);
CREATE INDEX idx_email_reversed ON customer(email_reversed);
-- 使用反轉欄位進行後綴搜尋
SELECT * FROM customer
WHERE email_reversed LIKE REVERSE('%@gmail.com');
-- 等同於 WHERE email_reversed LIKE 'moc.liamg@%'
@startuml
!define PLANTUML_FORMAT svg
!theme _none_
skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100
rectangle "LIKE 運算子效能分析" as title
rectangle "可使用索引" as good {
rectangle "LIKE 'prefix%'"
rectangle "模式以固定字元開頭"
rectangle "效能較佳"
}
rectangle "無法使用索引" as bad {
rectangle "LIKE '%suffix'"
rectangle "LIKE '%contain%'"
rectangle "需要全表掃描"
}
good -[hidden]right-> bad
note bottom of good
適用場景:
前綴搜尋
首字母篩選
end note
note bottom of bad
最佳化建議:
使用全文索引
建立反轉欄位
考慮搜尋引擎
end note
@endumlEXISTS 存在性檢查
EXISTS 運算子用於檢查子查詢是否傳回至少一筆記錄。與 IN 運算子不同,EXISTS 只關心子查詢是否有結果,而不關心具體的值。這個特性使得 EXISTS 在某些場景下比 IN 更有效率。
-- 使用 EXISTS 檢查是否存在千禧世代客戶
-- EXISTS 只檢查是否有記錄,不關心具體內容
SELECT '客戶資料庫中至少有一位千禧世代' AS message
WHERE EXISTS (
SELECT 1
FROM customer
WHERE birth_year BETWEEN 1981 AND 1996
);
如果 customer 表中存在至少一位出生年份在 1981 到 1996 之間的客戶,查詢會傳回該訊息;否則不會傳回任何結果。
EXISTS 的一個重要特性是它不關心子查詢傳回的具體內容,只關心是否有記錄。因此,在子查詢中使用 SELECT 1 或 SELECT * 效果相同:
-- 以下兩個查詢完全等效
-- EXISTS 只檢查是否有記錄存在
-- 寫法一:SELECT *
SELECT '有千禧世代客戶' AS result
WHERE EXISTS (
SELECT *
FROM customer
WHERE birth_year BETWEEN 1981 AND 1996
);
-- 寫法二:SELECT 1(建議使用)
SELECT '有千禧世代客戶' AS result
WHERE EXISTS (
SELECT 1
FROM customer
WHERE birth_year BETWEEN 1981 AND 1996
);
EXISTS 最常見的應用是與相關子查詢(Correlated Subquery)結合使用,用於檢查關聯條件:
-- 查詢有下過訂單的客戶
-- 使用相關子查詢的 EXISTS
SELECT
c.customer_id,
c.customer_name,
c.email
FROM customer c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.customer_id -- 相關條件
)
ORDER BY c.customer_name;
NOT EXISTS 則用於查詢不存在關聯記錄的情況:
-- 查詢從未下過訂單的客戶
-- 使用 NOT EXISTS
SELECT
c.customer_id,
c.customer_name,
c.email,
c.registration_date
FROM customer c
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.customer_id
)
ORDER BY c.registration_date;
EXISTS 與 IN 的比較
EXISTS 和 IN 在許多情況下可以達成相同的效果,但它們的效能特性有所不同:
-- 使用 IN 的寫法
SELECT c.customer_id, c.customer_name
FROM customer c
WHERE c.customer_id IN (
SELECT o.customer_id
FROM orders o
WHERE o.order_date >= '2024-01-01'
);
-- 使用 EXISTS 的等效寫法
SELECT c.customer_id, c.customer_name
FROM customer c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.customer_id
AND o.order_date >= '2024-01-01'
);
一般而言,當子查詢的結果集較大時,EXISTS 的效能通常較佳;當子查詢的結果集較小時,IN 的效能可能較佳。現代資料庫最佳化器通常能夠自動選擇最佳的執行計劃,但了解這些差異有助於在效能調校時做出正確的決策。
@startuml
!define DISABLE_LINK
!define PLANTUML_FORMAT svg
!theme _none_
skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100
rectangle "EXISTS 運算子工作原理" as title
package "執行流程" {
rectangle "1. 對外層查詢的每一列" as step1
rectangle "2. 執行子查詢" as step2
rectangle "3. 檢查是否有結果" as step3
rectangle "4. 有結果則保留該列" as step4
step1 -down-> step2
step2 -down-> step3
step3 -down-> step4
}
package "特性" {
rectangle "找到第一筆即停止" as f1
rectangle "不關心實際傳回值" as f2
rectangle "適合相關子查詢" as f3
rectangle "NULL 值處理安全" as f4
f1 -down[hidden]-> f2
f2 -down[hidden]-> f3
f3 -down[hidden]-> f4
}
title -down-> "執行流程"
title -down-> "特性"
@enduml布林值欄位的檢查
布林值是一種只有兩個可能值的資料類型:TRUE(真)或 FALSE(假)。在不同的資料庫系統中,布林值的內部表示方式可能有所不同。MySQL 使用 TINYINT(1) 來儲存布林值,其中 1 代表 TRUE,0 代表 FALSE。
基本的布林值檢查
檢查布林值欄位最直接的方式是使用 IS TRUE 和 IS FALSE:
-- 建立包含布林值欄位的員工資料表
CREATE TABLE employee (
employee_id INT PRIMARY KEY,
employee_name VARCHAR(100),
department VARCHAR(50),
is_manager BOOLEAN, -- 是否為主管
is_active BOOLEAN DEFAULT TRUE, -- 是否在職
has_parking BOOLEAN -- 是否有停車位
);
-- 查詢所有在職的主管
SELECT
employee_id,
employee_name,
department
FROM employee
WHERE is_manager IS TRUE
AND is_active IS TRUE
ORDER BY department, employee_name;
多種等效的檢查方式
SQL 提供了多種檢查布林值的等效語法。了解這些不同的寫法有助於閱讀他人的程式碼,以及根據情況選擇最適合的方式:
-- 檢查 TRUE 值的各種等效寫法
-- 所有以下條件都檢查 is_active 為 TRUE
-- 寫法 1:使用 IS TRUE(最明確)
SELECT * FROM employee WHERE is_active IS TRUE;
-- 寫法 2:直接使用欄位名稱(最簡潔)
SELECT * FROM employee WHERE is_active;
-- 寫法 3:使用等於運算子
SELECT * FROM employee WHERE is_active = TRUE;
-- 寫法 4:使用不等於 FALSE
SELECT * FROM employee WHERE is_active != FALSE;
-- 寫法 5:使用數值 1
SELECT * FROM employee WHERE is_active = 1;
-- 寫法 6:使用不等於 0
SELECT * FROM employee WHERE is_active != 0;
-- 檢查 FALSE 值的各種等效寫法
-- 寫法 1:使用 IS FALSE(最明確)
SELECT * FROM employee WHERE is_manager IS FALSE;
-- 寫法 2:使用 NOT 運算子(較常用)
SELECT * FROM employee WHERE NOT is_manager;
-- 寫法 3:使用等於運算子
SELECT * FROM employee WHERE is_manager = FALSE;
-- 寫法 4:使用不等於 TRUE
SELECT * FROM employee WHERE is_manager != TRUE;
-- 寫法 5:使用數值 0
SELECT * FROM employee WHERE is_manager = 0;
-- 寫法 6:使用不等於 1
SELECT * FROM employee WHERE is_manager != 1;
NULL 值的處理
布林值欄位可能包含 NULL 值,這需要特別處理。IS TRUE 和 IS FALSE 會明確排除 NULL 值,而其他寫法的行為可能有所不同:
-- 假設 has_parking 欄位可能為 NULL
-- 查詢有停車位的員工(排除 NULL)
SELECT employee_name
FROM employee
WHERE has_parking IS TRUE;
-- 查詢沒有停車位或未知的員工
SELECT employee_name
FROM employee
WHERE has_parking IS NOT TRUE; -- 包含 FALSE 和 NULL
-- 查詢明確沒有停車位的員工(排除 NULL)
SELECT employee_name
FROM employee
WHERE has_parking IS FALSE;
-- 查詢停車位狀態未知的員工
SELECT employee_name
FROM employee
WHERE has_parking IS NULL;
實際應用案例
讓我們看一個結合多個布林條件的實際查詢:
-- 查詢符合特定條件的員工
-- 條件:在職、非主管、有停車位
SELECT
e.employee_id,
e.employee_name,
e.department,
CASE WHEN e.is_manager THEN '是' ELSE '否' END AS is_manager_display,
CASE WHEN e.has_parking THEN '有' ELSE '無' END AS parking_status
FROM employee e
WHERE e.is_active IS TRUE
AND e.is_manager IS FALSE
AND e.has_parking IS TRUE
ORDER BY e.department, e.employee_name;
在效能方面,直接使用欄位名稱(如 WHERE is_active)通常與使用 IS TRUE 的效能相同,因為資料庫最佳化器會將它們視為等效條件。然而,為了程式碼的可讀性和明確性,建議在需要強調布林檢查時使用 IS TRUE 和 IS FALSE 語法。
綜合應用與效能最佳化
在實際應用中,我們經常需要結合多種篩選條件來完成複雜的查詢需求。以下是一些綜合應用的範例和效能最佳化建議。
複合條件的組合
-- 綜合應用範例:篩選符合多重條件的客戶
-- 結合 IN、BETWEEN、LIKE 和布林值檢查
SELECT
c.customer_id,
c.customer_name,
c.email,
c.birth_year,
c.membership_level,
c.is_active
FROM customer c
WHERE c.membership_level IN ('Gold', 'Platinum') -- 會員等級篩選
AND c.birth_year BETWEEN 1980 AND 1995 -- 年齡範圍篩選
AND c.email LIKE '%@gmail.com' -- 電子郵件模式篩選
AND c.is_active IS TRUE -- 活躍狀態篩選
AND NOT EXISTS ( -- 排除有未付款訂單的客戶
SELECT 1
FROM orders o
WHERE o.customer_id = c.customer_id
AND o.payment_status = 'Pending'
)
ORDER BY c.customer_name;
效能最佳化策略
選擇篩選條件時,除了功能正確性外,效能也是重要的考量因素:
-- 建立適當的索引以支援篩選條件
CREATE INDEX idx_customer_membership ON customer(membership_level);
CREATE INDEX idx_customer_birth_year ON customer(birth_year);
CREATE INDEX idx_customer_active ON customer(is_active);
CREATE INDEX idx_orders_customer_status ON orders(customer_id, payment_status);
-- 複合索引可以支援多個條件的查詢
CREATE INDEX idx_customer_active_level ON customer(is_active, membership_level);
-- 條件順序的最佳化
-- 將選擇性高(過濾效果好)的條件放在前面
SELECT *
FROM customer
WHERE is_active IS TRUE -- 先過濾大量不活躍用戶
AND membership_level = 'Platinum' -- 再過濾少數高級會員
AND email LIKE 'a%'; -- 最後做模式比對
避免常見的效能陷阱
-- 陷阱 1:在索引欄位上使用函數
-- 不佳:無法使用索引
SELECT * FROM customer WHERE YEAR(registration_date) = 2024;
-- 較佳:使用範圍查詢
SELECT * FROM customer
WHERE registration_date >= '2024-01-01'
AND registration_date < '2025-01-01';
-- 陷阱 2:使用 OR 連接不同欄位
-- 可能無法有效使用索引
SELECT * FROM customer
WHERE email LIKE 'john%' OR phone LIKE '02-%';
-- 較佳:使用 UNION
SELECT * FROM customer WHERE email LIKE 'john%'
UNION
SELECT * FROM customer WHERE phone LIKE '02-%';
-- 陷阱 3:NOT IN 與 NULL 值
-- 可能產生非預期結果
SELECT * FROM customer WHERE customer_id NOT IN (SELECT customer_id FROM banned_list);
-- 較佳:使用 NOT EXISTS 或明確處理 NULL
SELECT * FROM customer c
WHERE NOT EXISTS (
SELECT 1 FROM banned_list b WHERE b.customer_id = c.customer_id
);
掌握 SQL 篩選查詢的各種運算子和技巧,是成為高效資料庫開發者的基礎。從基本的比較運算到複雜的存在性檢查,每種篩選方式都有其特定的應用場景和效能特性。IN 和 NOT IN 適合集合成員判斷,BETWEEN 適合範圍查詢,LIKE 適合模式比對,EXISTS 適合存在性檢查,而布林值檢查則提供了多種等效語法供選擇。
在實務應用中,選擇適當的篩選方式不僅要考慮功能正確性,還要考慮查詢效能。了解各種運算子對索引的影響,避免常見的效能陷阱,並善用資料庫提供的最佳化工具,是撰寫高效 SQL 查詢的關鍵。透過不斷的實踐和經驗累積,我們可以逐漸培養出在各種情境下選擇最佳篩選策略的能力。