原存儲過程使用 CURSOR 逐行讀取 t_person_plan(關聯 500 萬行 t_person_plan_record),並在循環內對每行執行多條 SELECT COUNT(*)、INSERT/UPDATE。游標屬於逐行處理(RBAR,Row-By-Agonizing-Row),上下文切換、鎖競爭、索引查找開銷巨大,導致 2 小時僅處理 10 萬行。
將「循環讀取 → 逐條判斷 → 逐條寫入」重構為 「單條 SQL 完成全量數據讀取、分組、寫入」。MySQL 8.0+ 支持 CTE(Common Table Expressions)與窗口函數,配合 INSERT ... ON DUPLICATE KEY UPDATE 可一次性完成「不存在則插入,存在則更新」。
WITH plan_base AS (
SELECT p1.id AS person_plan_id,
p1.start_learn_time,
p1.last_date,
p1.sum_study_time,
p1.target_study_time
FROM t_person_plan p1
WHERE p1.license_type = 7
AND p1.last_date <= CURRENT_DATE()
),
stage_times AS (
SELECT person_plan_id,
MAX(CASE WHEN chapter_id = 29611 THEN add_time END) AS first_end_time,
MAX(CASE WHEN chapter_id = 29625 THEN add_time END) AS sec_end_time,
MAX(CASE WHEN chapter_id = 29734 THEN add_time END) AS third_end_time
FROM t_person_plan_record
WHERE person_plan_id IN (SELECT person_plan_id FROM plan_base)
GROUP BY person_plan_id
),
stage_counts AS (
SELECT person_plan_id,
SUM(stage = 1) AS id1_count,
SUM(stage = 2) AS id2_count,
SUM(stage = 3) AS id3_count
FROM t_stage_info
WHERE person_plan_id IN (SELECT person_plan_id FROM plan_base)
GROUP BY person_plan_id
)
INSERT INTO t_stage_info (person_plan_id, start_time, end_time, sum_study_time, target_study_time, stage)
SELECT pb.person_plan_id,
CASE s.stage
WHEN 1 THEN pb.start_learn_time
WHEN 2 THEN COALESCE(st.first_end_time, pb.last_date)
WHEN 3 THEN COALESCE(st.sec_end_time, pb.last_date)
END AS start_time,
CASE s.stage
WHEN 1 THEN COALESCE(st.first_end_time, pb.last_date)
WHEN 2 THEN COALESCE(st.sec_end_time, pb.last_date)
WHEN 3 THEN COALESCE(st.third_end_time, pb.last_date)
END AS end_time,
pb.sum_study_time,
pb.target_study_time,
s.stage
FROM plan_base pb
JOIN stage_times st ON pb.person_plan_id = st.person_plan_id
JOIN stage_counts sc ON pb.person_plan_id = sc.person_plan_id
JOIN (SELECT 1 AS stage UNION SELECT 2 UNION SELECT 3) s
ON (s.stage = 1 AND pb.sum_study_time <= 12)
OR (s.stage = 2 AND pb.sum_study_time > 12 AND pb.sum_study_time <= 14)
OR (s.stage = 3 AND pb.sum_study_time > 14)
ON DUPLICATE KEY UPDATE
end_time = VALUES(end_time),
sum_study_time = VALUES(sum_study_time),
target_study_time = VALUES(target_study_time);
前提:t_stage_info 需在 (person_plan_id, stage) 上建立唯一索引(或主鍵),使 ON DUPLICATE KEY UPDATE 生效。
| 表 | 建議索引 | 用途 |
|---|---|---|
| t_person_plan_record | (person_plan_id, chapter_id, add_time) | 覆蓋 stage_times 聚合查詢,避免回表 |
| t_stage_info | UNIQUE KEY uk_plan_stage (person_plan_id, stage) | 支援 ON DUPLICATE KEY UPDATE 並保證業務唯一性 |
| t_person_plan | (license_type, last_date, id) | 加速 plan_base 篩選 |
person_plan_id 分批(如每 5 萬行)循環執行上述 SQL。COALESCE 確保階段結束時間不為 NULL 時回退到 last_date。核心原則:能用 SQL 集合操作解決的,絕不寫游標循環。這是關係型數據庫性能優化的黃金法則。