資料庫是**批量運算**引擎,而非逐列處理器。將複雜演算法轉化為高效 SQL 的核心在於:**拒絕游標/迴圈,擁抱集合運算、關聯式代數、窗口函數**。本文透過三大經典案例——**分級統計、週期性排程、累積扣款**——展示從程序式演算法到集合式 SQL 的重構全過程。
student_Score (sno, cno, score, level),依分數區間寫入等級:
DECLARE cur CURSOR FOR SELECT sno, cno, score FROM student_Score;
OPEN cur;
FETCH NEXT FROM cur INTO @sno, @cno, @score;
WHILE @@FETCH_STATUS = 0
BEGIN
IF @score >= 90 SET @level = 'A';
ELSE IF @score >= 80 SET @level = 'B';
ELSE IF @score >= 60 SET @level = 'C';
ELSE SET @level = 'D';
UPDATE student_Score SET level = @level WHERE sno = @sno AND cno = @cno;
FETCH NEXT FROM cur INTO @sno, @cno, @score;
END
CLOSE cur; DEALLOCATE cur;
**缺點**:上萬次單獨 UPDATE、鎖定爭用、計畫快取汙染、執行時間隨資料線性增長。
UPDATE student_Score SET level = 'D'; -- 全部先設 D
UPDATE student_Score SET level = 'C' WHERE score BETWEEN 60 AND 79;
UPDATE student_Score SET level = 'B' WHERE score BETWEEN 80 AND 89;
UPDATE student_Score SET level = 'A' WHERE score BETWEEN 90 AND 100;
**缺點**:掃描表 4 次,高分列被重複更新 4 次 (D→C→B→A),產生大量寫入 I/O 與記錄檔。
-- 1. 建立分級查閱表 (小型、靜態、可索引)
CREATE TABLE ScoreLevel (
StartScore INT PRIMARY KEY,
EndScore INT,
Level CHAR(1)
);
INSERT INTO ScoreLevel VALUES (0,59,'D'), (60,79,'C'), (80,89,'B'), (90,100,'A');
-- 2. 單次 UPDATE 完成所有分級
UPDATE s
SET s.level = sl.Level
FROM student_Score s
JOIN ScoreLevel sl ON s.score BETWEEN sl.StartScore AND sl.EndScore;
**優勢**:單次掃描、單次寫入、查閱表可維護 (業務調整只改表不改碼)、優化器可選擇 Hash/Loop Join。
UPDATE student_Score
SET level = CASE
WHEN score >= 90 THEN 'A'
WHEN score >= 80 THEN 'B'
WHEN score >= 60 THEN 'C'
ELSE 'D'
END;
**適用**:規則固定、不需外部維護、單一陳述式完成。
觀察規律:
統一數學模型:第 N 天發放 ⇔ N % 7 = 0 或 (N - 3) % 7 = 0
-- 1. 計數器表 (單列)
CREATE TABLE Misc (Days INT NOT NULL DEFAULT 0);
INSERT INTO Misc VALUES (0);
-- 2. 每日作業步驟
BEGIN TRAN;
-- 取得當前天數
DECLARE @Days INT;
SELECT @Days = Days FROM Misc;
-- 判斷是否發放
IF @Days % 7 = 0 OR (@Days - 3) % 7 = 0
BEGIN
-- 執行獎金結算邏輯 (略)
PRINT '今日發放獎金';
END
-- 遞增計數器
UPDATE Misc SET Days = Days + 1;
COMMIT;
**核心洞見**:將「週一、週四」這類**業務語義**轉化為**數學不變量 (模運算)**,消除硬編碼日期陣列、行事曆表查詢,實現零維護排程邏輯。
award_repeat)Member (MemberID, TotalAward, RepeatNum) — 累計總獎金、已產生重複消費次數AwardDay (MemberID, DayAward, DayDeduction) — 當期獎金、當期扣款AwardRepeat (MemberID, RepeatDate) — 重複消費記錄Award (MemberID, DayAward, DayDeduction, NetAward) — 最終發放明細
UPDATE m
SET m.TotalAward = m.TotalAward + ad.DayAward
FROM Member m
JOIN AwardDay ad ON m.MemberID = ad.MemberID;
邏輯:應扣款 = FLOOR(TotalAward / 4000) * 400 - RepeatNum * 400
但需處理「單期不足 400 累計」→ 實際上 TotalAward 已包含歷史累計,故公式成立。
-- 產生重複消費記錄 (每滿 4000 產生一筆)
INSERT INTO AwardRepeat (MemberID, RepeatDate)
SELECT m.MemberID, GETDATE()
FROM Member m
WHERE m.TotalAward >= (m.RepeatNum + 1) * 4000;
-- 更新重複消費次數
UPDATE m
SET m.RepeatNum = m.RepeatNum + 1
FROM Member m
WHERE m.TotalAward >= (m.RepeatNum + 1) * 4000;
**關鍵**:WHERE 條件精確捕捉「剛跨越 4000 倍數門檻」的會員,單次集合操作完成所有會員的判斷與更新。
UPDATE ad
SET ad.DayDeduction = CASE
WHEN m.TotalAward >= (m.RepeatNum + 1) * 4000 THEN 400 -- 本期觸發新門檻
ELSE m.TotalAward - m.RepeatNum * 400 -- 累計未滿 400,全扣
END
FROM AwardDay ad
JOIN Member m ON ad.MemberID = m.MemberID;
-- 最終發放表
INSERT INTO Award (MemberID, DayAward, DayDeduction, NetAward)
SELECT MemberID, DayAward, DayDeduction, DayAward - DayDeduction
FROM AwardDay;
因為 TotalAward 是**單調遞增**的累計值,RepeatNum 是**單調遞增**的門檻計數器。單次比較 TotalAward >= (RepeatNum + 1) * 4000 即可精確判斷「本期是否跨越新門檻」,無需迭代。
掌握這三大法則,配合窗口函數 (ROW_NUMBER, SUM() OVER)、CTE 遞迴、MERGE 等現代 T-SQL 特性,絕大多數複雜業務演算法皆可轉化為高效、可維護、可並行的 SQL 實作。