面對千萬級單表需按業務鍵拆分為主表(帖子)與子表(回覆),利用雙層游標在存儲過程中分批處理,避免單次事務過大、鎖衝突與內存溢出。本文解析核心邏輯與優化要點。
bbs_fromask_importask_tmp:臨時導入表,含 pkid(帖子分組鍵)、qtitle、qdesc、answer(多行回覆)等字段。pre_data_thread_tmp(id, title, age, rule, param) —— 帖子主表pre_data_post_tmp(tid, text, sort) —— 回覆子表,sort=1 為樓主內容,>=2 為回覆DELIMITER $$
CREATE PROCEDURE `bbs_split_thread_post`()
BEGIN
DECLARE v_source_count INT DEFAULT 0;
DECLARE v_thread_pkid INT;
DECLARE v_thread_pkid_uni_count INT;
DECLARE done1 INT DEFAULT 0;
DECLARE done2 INT DEFAULT 0;
-- 外層游標:按 pkid 分組,獲取每組行數
DECLARE v_cur_thread_pkid CURSOR FOR
SELECT pkid, COUNT(*) FROM bbs_fromask_importask_tmp GROUP BY pkid;
-- 外層 NOT FOUND 處理器
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done1 = 1;
-- 清空目標臨時表
TRUNCATE TABLE pre_data_thread_tmp;
TRUNCATE TABLE pre_data_post_tmp;
-- 源表無數據直接返回
SELECT COUNT(*) INTO v_source_count FROM bbs_fromask_importask_tmp;
IF v_source_count = 0 THEN
LEAVE;
END IF;
-- 獲取目標主表當前最大 ID 作為起始值
SELECT IFNULL(MAX(id), 0) INTO @thread_max_id FROM yaolanbbs.pre_data_thread;
OPEN v_cur_thread_pkid;
REPEAT
FETCH v_cur_thread_pkid INTO v_thread_pkid, v_thread_pkid_uni_count;
IF NOT done1 THEN
SET @thread_max_id = @thread_max_id + 1;
-- 1. 寫入帖子主表(取每組第一條作為標題)
INSERT INTO pre_data_thread_tmp (id, title, age, rule, param)
SELECT @thread_max_id, t1.qtitle, t1.age, 'age', t1.age
FROM bbs_fromask_importask_tmp t1
WHERE t1.pkid = v_thread_pkid
LIMIT 1;
-- 2. 內層游標:遍歷該 pkid 下所有 answer 寫入回覆表
BEGIN
DECLARE v_post_answer TEXT;
DECLARE v_cur_post_record CURSOR FOR
SELECT t1.answer FROM bbs_fromask_importask_tmp t1 WHERE t1.pkid = v_thread_pkid;
DECLARE EXIT HANDLER FOR NOT FOUND BEGIN END; -- 內層結束不影響外層
-- 首條回覆(樓主內容 qdesc)
INSERT INTO pre_data_post_tmp (tid, text, sort)
SELECT @thread_max_id, t1.qdesc, 1
FROM bbs_fromask_importask_tmp t1
WHERE t1.pkid = v_thread_pkid
LIMIT 1;
SET @sort = 2;
OPEN v_cur_post_record;
REPEAT
FETCH v_cur_post_record INTO v_post_answer;
IF NOT done2 THEN
INSERT INTO pre_data_post_tmp (tid, text, sort) VALUES (@thread_max_id, v_post_answer, @sort);
SET @sort = @sort + 1;
END IF;
UNTIL done2 END REPEAT;
CLOSE v_cur_post_record;
END;
END IF;
UNTIL done1 END REPEAT;
CLOSE v_cur_thread_pkid;
END$$
DELIMITER ;
NOT FOUND 處理器隔離:外層用 CONTINUE HANDLER 設置 done1=1;內層用 EXIT HANDLER 直接結束 BEGIN...END 塊,避免內層結束觸發外層退出。@thread_max_id、@sort 跨游標傳遞:會話級變量,無需聲明,靈活計算自增鍵。COMMIT,避免超大事務鎖住目標表、Undo Log 膨脹。_tmp 表,驗證無誤後再 INSERT ... SELECT 進正式表,降低風險。| 指標 | 原始方案 | 優化方案 |
|---|---|---|
| 848 萬行拆分耗時 | 39 分鐘(單線程游標) | 分區並行:按 pkid 範圍拆分多個存儲過程並發跑,或用 mydumper 導出按 pkid 分文件,多線程 LOAD DATA 導入 |
| 鎖衝突 | 長事務持有目標表 X 鎖 | 批次提交(每 1000 組 pkid COMMIT),innodb_lock_wait_timeout 調小 |
| 內存壓力 | 游標逐行 Fetch | 改用 INSERT ... SELECT 批量寫入,避免逐行網絡交互 |