返回文章列表

SQL資料庫查詢技巧與最佳實踐

深入探討 SQL 資料庫查詢的實用技巧與最佳實踐,包含 LIMIT 與 OFFSET 分頁查詢、UNION 與 UNION ALL 集合運算、多表 JOIN 關聯查詢,以及臨時表的建立與應用,並提供效能最佳化策略

資料庫 SQL

在資料庫應用開發中,撰寫高效且正確的 SQL 查詢是每位開發者必須掌握的核心技能。隨著資料量的增長和業務邏輯的複雜化,我們需要運用各種查詢技巧來解決實際問題。從控制結果集大小的 LIMIT 子句,到合併多個查詢結果的 UNION 運算,從關聯多個表格的 JOIN 操作,到暫存中間結果的臨時表應用,這些技術共同構成了資料庫查詢的完整工具箱。

本文將從實務角度出發,深入探討各種查詢技巧的語法、特性和應用場景。我們不僅會介紹這些技術的基本用法,還會討論它們的效能特性和最佳化策略。透過具體的案例和程式碼範例,讀者可以了解如何在實際專案中選擇和組合這些技巧,以達成高效且可維護的資料庫查詢。

LIMIT 與 OFFSET 分頁查詢

在許多應用場景中,我們並不需要一次取得所有的查詢結果。例如,在網頁應用中顯示搜尋結果時,通常會採用分頁方式,每頁只顯示固定數量的記錄。LIMIT 子句正是為這類需求而設計,它能夠限制查詢傳回的記錄數量。

基本的 LIMIT 用法

LIMIT 子句通常放在查詢語句的最後,指定要傳回的最大記錄數:

-- 查詢葡萄酒比賽的前三名
-- 使用 LIMIT 限制只傳回三筆記錄
SELECT
    wine_id,
    wine_name,
    winery_name,
    score,
    ranking
FROM wine_competition_result
ORDER BY ranking ASC    -- 先按排名排序
LIMIT 3;                -- 只取前三名

這個查詢會傳回排名前三的葡萄酒記錄。ORDER BY 子句確保結果按照排名順序排列,LIMIT 3 則限制只傳回前三筆。

-- 查詢最新的五筆訂單
SELECT
    order_id,
    customer_id,
    order_date,
    total_amount,
    status
FROM orders
ORDER BY order_date DESC  -- 按日期降冪排序
LIMIT 5;                  -- 只取最新五筆

LIMIT 與 OFFSET 的分頁應用

當我們需要實作分頁功能時,除了 LIMIT 之外,還需要 OFFSET 來指定要跳過的記錄數。OFFSET 告訴資料庫從第幾筆記錄開始傳回:

-- 分頁查詢:取得第二頁的資料(每頁 10 筆)
-- OFFSET 10 表示跳過前 10 筆記錄
SELECT
    product_id,
    product_name,
    price,
    category
FROM products
ORDER BY product_id
LIMIT 10 OFFSET 10;   -- 跳過前 10 筆,傳回接下來的 10 筆

在實際應用中,分頁的計算公式如下:

-- 通用分頁公式
-- page_number: 頁碼(從 1 開始)
-- page_size: 每頁記錄數
-- OFFSET = (page_number - 1) * page_size

-- 第 1 頁:LIMIT 10 OFFSET 0
-- 第 2 頁:LIMIT 10 OFFSET 10
-- 第 3 頁:LIMIT 10 OFFSET 20

-- 查詢第 5 頁的產品(每頁 20 筆)
SELECT product_id, product_name, price
FROM products
ORDER BY product_id
LIMIT 20 OFFSET 80;   -- (5-1) * 20 = 80

分頁查詢的效能考量

雖然 LIMIT 和 OFFSET 提供了簡單的分頁機制,但在處理大量資料時可能會遇到效能問題。當 OFFSET 值很大時,資料庫仍然需要掃描並跳過這些記錄,這會導致查詢效率降低:

-- 效能較差的大 OFFSET 查詢
-- 資料庫需要掃描前 100000 筆記錄後才開始傳回結果
SELECT product_id, product_name, price
FROM products
ORDER BY product_id
LIMIT 20 OFFSET 100000;

-- 更有效率的做法:使用 keyset 分頁
-- 假設上一頁最後一筆的 product_id 是 100020
SELECT product_id, product_name, price
FROM products
WHERE product_id > 100020    -- 從上一頁最後一筆之後開始
ORDER BY product_id
LIMIT 20;

Keyset 分頁(也稱為游標分頁或 seek method)利用索引直接定位到起始位置,效能遠優於傳統的 OFFSET 分頁。但它需要在應用程式中追蹤上一頁的最後一筆記錄。

不同資料庫的語法差異

LIMIT 的語法在不同資料庫系統中略有差異:

-- MySQL 和 PostgreSQL 的語法
SELECT * FROM products LIMIT 10 OFFSET 20;
-- 或者
SELECT * FROM products LIMIT 20, 10;  -- MySQL 特有語法:LIMIT offset, count

-- SQL Server 的語法
SELECT TOP 10 * FROM products;
-- 或者(SQL Server 2012+)
SELECT * FROM products
ORDER BY product_id
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;

-- Oracle 的語法(12c+)
SELECT * FROM products
FETCH FIRST 10 ROWS ONLY;
-- 或者使用 ROWNUM(較舊版本)
SELECT * FROM (
    SELECT p.*, ROWNUM rn FROM products p WHERE ROWNUM <= 30
) WHERE rn > 20;

UNION 與 UNION ALL 集合運算

當我們需要合併多個查詢的結果時,可以使用 UNION 或 UNION ALL 運算子。這兩個運算子都能將多個 SELECT 語句的結果合併成一個結果集,但它們在處理重複記錄方面有所不同。

UNION 的基本用法

UNION 會自動移除合併結果中的重複記錄,只保留唯一的值:

-- 合併兩個表中的葡萄酒名稱
-- UNION 會自動移除重複的記錄
SELECT wine_type_name AS wine_name FROM wine_type
UNION
SELECT wine_name FROM wine_competition_result;

假設 wine_type 表包含 Chardonnay、Cabernet Sauvignon、Merlot,而 wine_competition_result 表包含 Riesling、Merlot、Pinot Grigio,UNION 的結果會是六筆唯一的記錄(Merlot 只出現一次)。

UNION ALL 保留所有記錄

如果需要保留所有記錄,包括重複的部分,應該使用 UNION ALL:

-- 使用 UNION ALL 保留所有記錄
-- 不會移除重複值
SELECT wine_type_name AS wine_name FROM wine_type
UNION ALL
SELECT wine_name FROM wine_competition_result;

使用同樣的範例資料,UNION ALL 的結果會是七筆記錄(Merlot 出現兩次)。

UNION 的使用規則

使用 UNION 時需要遵守以下規則:

-- 規則 1:欄位數量必須相同
-- 正確:兩個查詢都選取兩個欄位
SELECT product_id, product_name FROM products_2023
UNION
SELECT product_id, product_name FROM products_2024;

-- 錯誤:欄位數量不同
-- SELECT product_id, product_name, price FROM products_2023
-- UNION
-- SELECT product_id, product_name FROM products_2024;  -- 會產生錯誤

-- 規則 2:對應欄位的資料類型必須相容
-- 正確:整數與整數、字串與字串相容
SELECT order_id, order_date FROM online_orders
UNION
SELECT order_id, order_date FROM store_orders;

-- 規則 3:結果集的欄位名稱取自第一個查詢
-- 可以使用別名來指定自訂的欄位名稱
SELECT product_name AS item_name, price FROM electronics
UNION
SELECT product_name, price FROM clothing;

實際應用案例

讓我們看一些 UNION 的實際應用場景:

-- 案例 1:合併多個年度的銷售記錄
SELECT
    'Q1' AS quarter,
    product_id,
    SUM(quantity) AS total_quantity,
    SUM(amount) AS total_amount
FROM sales_q1_2024
GROUP BY product_id

UNION ALL

SELECT
    'Q2' AS quarter,
    product_id,
    SUM(quantity) AS total_quantity,
    SUM(amount) AS total_amount
FROM sales_q2_2024
GROUP BY product_id

ORDER BY product_id, quarter;
-- 案例 2:整合不同來源的客戶資料
SELECT
    customer_id,
    customer_name,
    email,
    'Website' AS source
FROM website_customers

UNION

SELECT
    customer_id,
    customer_name,
    email,
    'Mobile App' AS source
FROM mobile_app_customers

ORDER BY customer_name;
-- 案例 3:建立下拉選單的選項清單
SELECT
    '' AS code,
    '-- 請選擇國家 --' AS display_name,
    0 AS sort_order  -- 確保空選項在最前面

UNION ALL

SELECT
    country_code AS code,
    country_name AS display_name,
    1 AS sort_order
FROM countries
WHERE is_active = TRUE

ORDER BY sort_order, display_name;

UNION 與其他集合運算子

除了 UNION,SQL 還提供了其他集合運算子:

-- INTERSECT:取得兩個查詢的交集
-- 傳回同時出現在兩個結果集中的記錄
SELECT customer_id FROM premium_members
INTERSECT
SELECT customer_id FROM newsletter_subscribers;

-- EXCEPT(或 MINUS):取得差集
-- 傳回在第一個結果集但不在第二個結果集中的記錄
SELECT product_id FROM all_products
EXCEPT
SELECT product_id FROM discontinued_products;

需要注意的是,INTERSECT 和 EXCEPT 並非所有資料庫都支援。MySQL 在 8.0.31 版本之前不支援這兩個運算子,需要使用其他方式來達成相同的效果:

-- 在 MySQL 中模擬 INTERSECT
SELECT DISTINCT g.snack_name
FROM good_snack g
INNER JOIN bad_snack b ON g.snack_name = b.snack_name;

-- 在 MySQL 中模擬 EXCEPT
SELECT g.snack_name
FROM good_snack g
WHERE g.snack_name NOT IN (
    SELECT snack_name FROM bad_snack
);

多表 JOIN 關聯查詢

在正規化的資料庫設計中,資料通常分散儲存於多個相關聯的表格。JOIN 操作讓我們能夠根據表格之間的關聯條件,將這些分散的資料組合起來。本節將透過實際案例,說明如何運用 JOIN 來進行多表查詢。

基本的多表 JOIN

讓我們以一個旅遊資料庫為例,包含省份、首府城市和旅遊景點三個表格:

-- 資料表結構
-- province: province_id, province_name, official_language
-- capital_city: city_id, city_name, province_id, population
-- tourist_attraction: attraction_id, attraction_name, province_id, annual_visitors

-- 查詢省份、首府和旅遊景點的完整資訊
SELECT
    p.province_name,                  -- 省份名稱
    p.official_language,              -- 官方語言
    cc.city_name AS capital_name,     -- 首府名稱
    cc.population,                    -- 首府人口
    ta.attraction_name,               -- 景點名稱
    ta.annual_visitors                -- 年遊客數
FROM province p
INNER JOIN capital_city cc
    ON p.province_id = cc.province_id
INNER JOIN tourist_attraction ta
    ON p.province_id = ta.province_id
ORDER BY p.province_name, ta.annual_visitors DESC;

這個查詢透過 province_id 欄位,將三個表格關聯起來。每個省份可能有多個旅遊景點,所以結果中同一個省份可能會出現多次。

使用別名簡化查詢

當查詢涉及多個表格時,使用表格別名可以讓程式碼更簡潔易讀:

-- 使用有意義的別名
SELECT
    w.winery_name,
    w.founded_year,
    va.area_name,
    r.region_name,
    c.country_name,
    wt.wine_type_name,
    p.annual_production
FROM winery w
INNER JOIN viticultural_area va ON w.area_id = va.area_id
INNER JOIN region r ON va.region_id = r.region_id
INNER JOIN country c ON r.country_id = c.country_id
INNER JOIN portfolio p ON w.winery_id = p.winery_id
INNER JOIN wine_type wt ON p.wine_type_id = wt.wine_type_id
WHERE p.annual_production > 10000
ORDER BY c.country_name, r.region_name, w.winery_name;

結合 JOIN 與篩選條件

在實際應用中,我們經常需要在 JOIN 的基礎上加入各種篩選條件:

-- 查詢提供導覽且當季有生產 Merlot 的酒莊
SELECT
    w.winery_name,
    w.founded_year,
    va.area_name,
    wt.wine_type_name
FROM winery w
INNER JOIN viticultural_area va
    ON w.area_id = va.area_id
INNER JOIN portfolio p
    ON w.winery_id = p.winery_id
INNER JOIN wine_type wt
    ON p.wine_type_id = wt.wine_type_id
WHERE w.offers_tours = TRUE           -- 提供導覽
  AND p.in_season = TRUE              -- 當季生產
  AND wt.wine_type_name = 'Merlot'    -- 酒種為 Merlot
ORDER BY va.area_name, w.winery_name;

使用 LEFT JOIN 處理可選關聯

當某些關聯可能不存在時,應該使用 LEFT JOIN 來保留主表的所有記錄:

-- 查詢所有省份及其旅遊景點(包括沒有景點的省份)
SELECT
    p.province_name,
    p.official_language,
    COALESCE(ta.attraction_name, '尚無登錄景點') AS attraction_name,
    COALESCE(ta.annual_visitors, 0) AS annual_visitors
FROM province p
LEFT JOIN tourist_attraction ta
    ON p.province_id = ta.province_id
ORDER BY p.province_name, ta.annual_visitors DESC;
@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 "多表 JOIN 查詢結構" as title

rectangle "主表" as main {
    rectangle "province"
}

rectangle "關聯表" as related {
    rectangle "capital_city"
    rectangle "tourist_attraction"
}

rectangle "JOIN 條件" as conditions {
    rectangle "province_id = province_id"
}

main -[hidden]down-> related
related -[hidden]down-> conditions

note bottom of main
起始點
包含所有待查詢的主要記錄
end note

note bottom of related
透過外鍵關聯
可能是一對一或一對多
end note

@enduml

臨時表的建立與應用

臨時表是一種特殊的表格,它只在當前資料庫連線(session)期間存在,連線結束後會自動刪除。臨時表非常適合用來儲存中間結果,簡化複雜的查詢邏輯。

建立臨時表

建立臨時表有兩種主要方式:直接定義結構,或根據查詢結果建立。

-- 方式 1:直接定義臨時表結構
CREATE TEMPORARY TABLE temp_winery_summary (
    winery_id INT PRIMARY KEY,
    winery_name VARCHAR(200),
    area_name VARCHAR(100),
    wine_count INT,
    total_production INT
);

-- 之後可以插入資料
INSERT INTO temp_winery_summary
SELECT
    w.winery_id,
    w.winery_name,
    va.area_name,
    COUNT(p.wine_type_id) AS wine_count,
    SUM(p.annual_production) AS total_production
FROM winery w
INNER JOIN viticultural_area va ON w.area_id = va.area_id
INNER JOIN portfolio p ON w.winery_id = p.winery_id
GROUP BY w.winery_id, w.winery_name, va.area_name;
-- 方式 2:根據查詢結果直接建立臨時表
-- 結構會自動根據查詢結果推導
CREATE TEMPORARY TABLE temp_touring_merlot_wineries AS
SELECT
    w.winery_id,
    w.winery_name,
    va.area_name,
    va.area_id
FROM winery w
INNER JOIN viticultural_area va
    ON w.area_id = va.area_id
INNER JOIN portfolio p
    ON w.winery_id = p.winery_id
INNER JOIN wine_type wt
    ON p.wine_type_id = wt.wine_type_id
WHERE w.offers_tours = TRUE
  AND p.in_season = TRUE
  AND wt.wine_type_name = 'Merlot';

使用臨時表

建立臨時表後,可以像使用一般表格一樣查詢它:

-- 查詢臨時表的內容
SELECT * FROM temp_touring_merlot_wineries
ORDER BY area_name, winery_name;

-- 將臨時表與其他表格進行 JOIN
SELECT
    t.winery_name,
    t.area_name,
    r.region_name,
    c.country_name
FROM temp_touring_merlot_wineries t
INNER JOIN viticultural_area va ON t.area_id = va.area_id
INNER JOIN region r ON va.region_id = r.region_id
INNER JOIN country c ON r.country_id = c.country_id
ORDER BY c.country_name, r.region_name;

臨時表的應用場景

臨時表在以下場景中特別有用:

-- 場景 1:分步驟處理複雜查詢
-- 第一步:建立包含初步篩選結果的臨時表
CREATE TEMPORARY TABLE temp_active_customers AS
SELECT
    customer_id,
    customer_name,
    email,
    registration_date
FROM customers
WHERE is_active = TRUE
  AND registration_date >= DATE_SUB(CURRENT_DATE, INTERVAL 1 YEAR);

-- 第二步:基於臨時表進行進一步分析
SELECT
    c.customer_id,
    c.customer_name,
    COUNT(o.order_id) AS order_count,
    SUM(o.total_amount) AS total_spent
FROM temp_active_customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.customer_name
HAVING total_spent > 1000
ORDER BY total_spent DESC;
-- 場景 2:在迴圈或程序中重複使用中間結果
-- 建立一次,多次查詢
CREATE TEMPORARY TABLE temp_product_stats AS
SELECT
    category_id,
    COUNT(*) AS product_count,
    AVG(price) AS avg_price,
    MAX(price) AS max_price,
    MIN(price) AS min_price
FROM products
GROUP BY category_id;

-- 查詢 1:找出產品數量最多的類別
SELECT * FROM temp_product_stats ORDER BY product_count DESC LIMIT 5;

-- 查詢 2:找出平均價格最高的類別
SELECT * FROM temp_product_stats ORDER BY avg_price DESC LIMIT 5;

-- 查詢 3:找出價格範圍最大的類別
SELECT
    category_id,
    (max_price - min_price) AS price_range
FROM temp_product_stats
ORDER BY price_range DESC LIMIT 5;

臨時表與 CTE 的比較

臨時表和 CTE(Common Table Expression)都可以用來儲存中間結果,但它們有不同的特性:

-- 使用 CTE
WITH active_customers AS (
    SELECT customer_id, customer_name
    FROM customers
    WHERE is_active = TRUE
),
customer_orders AS (
    SELECT
        ac.customer_id,
        ac.customer_name,
        COUNT(o.order_id) AS order_count
    FROM active_customers ac
    LEFT JOIN orders o ON ac.customer_id = o.customer_id
    GROUP BY ac.customer_id, ac.customer_name
)
SELECT * FROM customer_orders WHERE order_count > 5;

-- 使用臨時表達成相同效果
CREATE TEMPORARY TABLE temp_active_customers AS
SELECT customer_id, customer_name
FROM customers
WHERE is_active = TRUE;

CREATE TEMPORARY TABLE temp_customer_orders AS
SELECT
    ac.customer_id,
    ac.customer_name,
    COUNT(o.order_id) AS order_count
FROM temp_active_customers ac
LEFT JOIN orders o ON ac.customer_id = o.customer_id
GROUP BY ac.customer_id, ac.customer_name;

SELECT * FROM temp_customer_orders WHERE order_count > 5;

臨時表的優點是可以建立索引、可以在多個查詢中重複使用、可以更新資料。缺點是需要額外的磁碟空間、需要明確的建立和清理步驟。CTE 的優點是語法簡潔、適合一次性使用的中間結果。缺點是無法建立索引、無法跨查詢重複使用。

查詢效能最佳化策略

撰寫功能正確的查詢只是第一步,在實際應用中還需要考慮查詢效能。以下是一些實用的最佳化策略。

選擇適當的索引

索引是提升查詢效能最有效的手段。應該為經常用於 WHERE、JOIN 和 ORDER BY 的欄位建立索引:

-- 為常用的查詢條件建立索引
CREATE INDEX idx_orders_customer ON orders(customer_id);
CREATE INDEX idx_orders_date ON orders(order_date);
CREATE INDEX idx_products_category ON products(category_id);

-- 複合索引:適用於經常一起使用的欄位
CREATE INDEX idx_orders_customer_date ON orders(customer_id, order_date);

避免不必要的全表掃描

某些查詢寫法會導致資料庫無法使用索引,應該盡量避免:

-- 不佳:在索引欄位上使用函數
SELECT * FROM orders WHERE YEAR(order_date) = 2024;

-- 較佳:使用範圍查詢
SELECT * FROM orders
WHERE order_date >= '2024-01-01' AND order_date < '2025-01-01';

-- 不佳:使用 LIKE '%keyword'
SELECT * FROM products WHERE product_name LIKE '%chair%';

-- 較佳:如果可能,使用前綴匹配
SELECT * FROM products WHERE product_name LIKE 'office chair%';

限制結果集大小

即使在開發階段,也應該養成限制結果集的習慣:

-- 開發時使用 LIMIT 避免取回過多資料
SELECT * FROM large_table LIMIT 100;

-- 只選取需要的欄位,而非 SELECT *
SELECT order_id, order_date, total_amount
FROM orders
WHERE customer_id = 123;

使用 EXPLAIN 分析查詢

大多數資料庫都提供 EXPLAIN 命令來分析查詢的執行計劃:

-- 分析查詢的執行計劃
EXPLAIN SELECT
    c.customer_name,
    COUNT(o.order_id) AS order_count
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE c.registration_date >= '2024-01-01'
GROUP BY c.customer_id, c.customer_name
HAVING order_count > 5;

透過分析執行計劃,可以了解查詢是否使用了索引、預估的掃描行數、JOIN 的執行方式等資訊,從而針對性地進行最佳化。

@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 "查詢最佳化流程" as title

rectangle "步驟 1" as step1 {
    rectangle "撰寫功能正確的查詢"
}

rectangle "步驟 2" as step2 {
    rectangle "使用 EXPLAIN 分析執行計劃"
}

rectangle "步驟 3" as step3 {
    rectangle "識別效能瓶頸"
}

rectangle "步驟 4" as step4 {
    rectangle "建立適當的索引"
}

rectangle "步驟 5" as step5 {
    rectangle "改寫查詢語句"
}

rectangle "步驟 6" as step6 {
    rectangle "驗證效能改善"
}

step1 -[hidden]down-> step2
step2 -[hidden]down-> step3
step3 -[hidden]down-> step4
step4 -[hidden]down-> step5
step5 -[hidden]down-> step6

@enduml

綜合應用案例

讓我們透過一個完整的案例,綜合運用本文介紹的各種技術。假設我們需要為一個電商平台建立一個銷售報表系統。

-- 案例:電商平台銷售分析報表

-- 步驟 1:建立臨時表儲存活躍客戶的基本資訊
CREATE TEMPORARY TABLE temp_active_customers AS
SELECT
    c.customer_id,
    c.customer_name,
    c.email,
    c.membership_level,
    c.registration_date
FROM customers c
WHERE c.is_active = TRUE
  AND c.registration_date >= DATE_SUB(CURRENT_DATE, INTERVAL 2 YEAR);

-- 步驟 2:計算每個客戶的訂單統計
CREATE TEMPORARY TABLE temp_customer_stats AS
SELECT
    ac.customer_id,
    ac.customer_name,
    ac.membership_level,
    COUNT(o.order_id) AS order_count,
    SUM(o.total_amount) AS total_spent,
    AVG(o.total_amount) AS avg_order_value,
    MAX(o.order_date) AS last_order_date
FROM temp_active_customers ac
LEFT JOIN orders o ON ac.customer_id = o.customer_id
WHERE o.status = 'Completed'
GROUP BY ac.customer_id, ac.customer_name, ac.membership_level;

-- 步驟 3:產生最終報表
-- 結合 UNION 顯示不同會員等級的統計
SELECT
    'Premium' AS membership_level,
    COUNT(*) AS customer_count,
    SUM(total_spent) AS total_revenue,
    AVG(order_count) AS avg_orders_per_customer,
    AVG(avg_order_value) AS avg_order_value
FROM temp_customer_stats
WHERE membership_level = 'Premium'

UNION ALL

SELECT
    'Standard' AS membership_level,
    COUNT(*) AS customer_count,
    SUM(total_spent) AS total_revenue,
    AVG(order_count) AS avg_orders_per_customer,
    AVG(avg_order_value) AS avg_order_value
FROM temp_customer_stats
WHERE membership_level = 'Standard'

UNION ALL

SELECT
    'Basic' AS membership_level,
    COUNT(*) AS customer_count,
    SUM(total_spent) AS total_revenue,
    AVG(order_count) AS avg_orders_per_customer,
    AVG(avg_order_value) AS avg_order_value
FROM temp_customer_stats
WHERE membership_level = 'Basic'

ORDER BY
    CASE membership_level
        WHEN 'Premium' THEN 1
        WHEN 'Standard' THEN 2
        WHEN 'Basic' THEN 3
    END;

-- 步驟 4:取得消費金額前 20 名的客戶明細
SELECT
    cs.customer_name,
    cs.membership_level,
    cs.order_count,
    cs.total_spent,
    cs.avg_order_value,
    cs.last_order_date,
    DATEDIFF(CURRENT_DATE, cs.last_order_date) AS days_since_last_order
FROM temp_customer_stats cs
WHERE cs.total_spent > 0
ORDER BY cs.total_spent DESC
LIMIT 20;

這個案例展示了如何結合臨時表、JOIN、UNION 和 LIMIT 來建立一個完整的報表查詢。透過分步驟處理,我們可以將複雜的邏輯拆解成易於理解和維護的部分。

掌握 SQL 查詢技巧是資料庫開發的基礎能力。LIMIT 和 OFFSET 讓我們能夠控制結果集的大小,實作分頁功能。UNION 和 UNION ALL 讓我們能夠合併多個查詢的結果,整合不同來源的資料。多表 JOIN 讓我們能夠從正規化的資料庫中提取完整的資訊。臨時表讓我們能夠儲存中間結果,簡化複雜的查詢邏輯。

在實際應用中,選擇適當的技術並考慮效能因素,是撰寫高品質 SQL 查詢的關鍵。透過不斷的實踐和經驗累積,我們可以逐漸培養出在各種情境下選擇最佳方案的能力,為應用程式提供高效且可靠的資料支援。