在有序數據表中實現多記錄上移下移置頂置底算法思路

  1. 引言

    數據庫應用中常須要在一個有序數據子集中,對指定的若干條記錄進行上下移動。例如,管理員須要對新聞列表中的若干條新聞置頂,考試出卷時須要對選定題目進行上下移動重排順序,等等。sql

    總的應該場景在數據表中能夠歸納爲以下模型:數據庫

    數據表 TblData(id,fid,rank),id表示記錄的惟一標識,fid指記錄的父節點,rank表明父節點下兄弟的先後順序,依次從1遞增,沒有空隙。oop

    問題是要對相同fid下選中的若干個節點進行上下移動,如圖1中的2個示例:3d

    圖 1 上移操做示例圖code

    左側示例是對第五、6兩個連續的記錄上移,右側示例是對第4和6個有間隔的記錄上移。上移後,要移動的記錄間的間隔保持不變。現實中,選中記錄的位置和數量都是隨機的,移動方向有「上移」1位,「下移」1位,「置頂」和「置底」。該需求進入存儲過程的參數可歸結爲  PROCEDURE Reorder(OUT out_errno INT, IN in_ids TEXT, IN in_n INT UNSIGNED, IN in_direction),其中,out_errno 用於返回狀態碼,in_ids 是要移動的多個 id,以「,」分隔;in_n 是要移動的 id 數量;in_direction 是移動方向,取值是"up","down","top","bottom"。blog

  2. 上移操做

    以塊爲單位的上移

    以向上移動(上移或置頂)爲例(如圖2所示),將連續的記錄看做一個塊,按升序依次對各塊進行上移操做。示例中有3個塊,所以須要移動3次。pdo

    圖2 上移操做塊示意圖io

    特定塊的上移

    對具體的塊的移動,須要肯定移動的步長(step)和塊中的記錄數(size)。經過第1塊能夠肯定移動的步長,而且該步長對全部塊都適用,即,全部塊移動的步長都和第1塊相同。圖2中,若是進行「上移」,則移動的步長是 step = 1;若是進行「置頂」,則移動的步長是 step = 4。不一樣塊中的記錄數可能不一樣。圖2中第1塊的記錄是5和6,所以記錄數是 size = 2。class

    以塊1的「置頂」操做爲例(如圖3所示)。以要移動的記錄5做爲基準點,current=5,對其上面步長範圍內的記錄下移,並對其下面塊中記錄數範圍內的記錄上移,即,對[current-step,current-1]範圍的記錄做下移 (+size),對[current,current+size-1]範圍的記錄上移 (-step)。效率

     

    圖3 特定塊的上移示意圖

    該過程經過1條SQL語句能夠實現:

    UPDATE TblData
    SET rank=rank+IF(rank<current,size,-step)
    WHERE fid=l_fid -- 其中,l_fid 是指記錄共同的父節點編號
    AND rank BETWEEN current-step AND current+size-1
  3. 下移操做

    下移過程和上移過程相似,區別是移動要從下面的塊開始(即,編號大的塊),與下面的記錄進行交換(如圖4所示)。

    圖4 特定塊的下移示意圖

  4. 以塊爲操做單位的可選方案

    從前文能夠看出,肯定移動步長(step)和塊內記錄數(size)起到關鍵做用。 移動步長根據第1塊(上移操做)或最後1塊(下移操做)很容易肯定。 但對於塊內記錄數卻不那麼容易,須要設置遊標判斷相鄰兩條記錄是否連續,是否有間隙,還要進行記數,並在存在間隙時須要暫停,轉去執行具體塊的移動操做。 可選方案是對要移動的記錄不做分塊處理,即每條要移動的記錄都看做一個塊進行移動。 這樣帶來的後果是:
    • 圖2示例中,若按塊移動,則需移3次(由於有3個塊須要上移);若不分塊移動,則需移動4次(由於有4條記錄須要上移)。移動次數取決於塊數和記錄數的區別。
    • 圖2示例中,若按塊移動,則須要肯定每1塊的記錄數(第1塊是2,第2和3塊都是1);若不分塊移動,則不須要肯定每塊記錄數,或能夠當作每塊記錄數都是1。是否省去了統計塊內記錄數的區別。
    • 效率上,主要取決於塊數和記錄數;邏輯上,後者更簡單清晰。所以,本人傾向後者。
  5. 完整實現代碼

    使用MySQL存儲過程實現上述功能,參考代碼以下。假設傳入參數在數據庫外的合法性已經保證,數據庫內的合法性須要驗證;假設要移動的記錄數大於0;假設數據表中記錄都是有序連續的。
    DROP PROCEDURE IF EXISTS Reorder; CREATE PROCEDURE Reorder(   OUT out_errno INT,   IN in_ids TEXT,   IN in_n INT UNSIGNED,   IN in_direction VARCHAR(16) ) -- block0 是整個程序最外層的範圍,其中還包含 block1(檢測合法性) 和 block2(實際移動操做) block0: BEGIN   DECLARE       l_rank_max,  -- 用於記錄最大Rank值,也即記錄總數       l_fid            -- 用於記錄父節點編號   INT UNSIGNED;   -- block1 對傳入參數在數據庫端合法性進行檢測   block1: BEGIN     -- 將要移動的記錄存入臨時表 t0(id,rank,processed)     -- 其中,processed用於block2中循環處理待移動記錄的標記     IF TRUE THEN        DROP TEMPORARY TABLE IF EXISTS t0;       SET @sql = CONCAT('         CREATE TEMPORARY TABLE t0         SELECT id, rank,'no' processed         FROM TblData          WHERE id IN(',in_ids,')         ORDER BY rank ',IF(in_direction IN('up','top'),'ASC','DESC'),'   -- 要移動的記錄,若上移,則按rank升序排列,不然降序       ');       PREPARE s FROM @sql;EXECUTE PREPARE s;DEALLOCATE PREPARE s;     END IF;     -- 101 檢測要移動的記錄是否都存在     IF (SELECT COUNT(id) FROM t0)<>in_n THEN       SET out_errno = 101;       LEAVE block0;     END IF;     -- 102 要移動的記錄都隸屬於同一父節點     IF (       SELECT COUNT(DISTINCT fid) FROM t0     )>1 THEN        SET out_errno = 102;       LEAVE block0;     END IF;     SET l_fid = (SELECT fid FROM t0 LIMIT 1);     SET l_rank_max = (SELECT MAX(rank) FROM TblData WHERE fid=l_fid);     IF in_direction IN('up','top') THEN       -- 103 若是上移,是否已經在頂端       IF (         SELECT rank=1 FROM t0          LIMIT 1       ) THEN          SET out_errno = 103;         LEAVE block0;       END IF;     ELSE       -- 104 若是下移,是否已經在底端       IF (         SELECT rank=l_rank_max         FROM t0          LIMIT 1       ) THEN          SET out_errno = 104;         LEAVE block0;       END IF;     END IF;   END block1;   block2: BEGIN     DECLARE l_step, l_size, l_current INT UNSIGNED;     SET l_size = 1;      IF in_direction IN('up','top') THEN       -- 上移操做       SET l_step = IF(         in_direction='up',         1,         (SELECT rank-1 FROM t0 LIMIT 1)       );       myloopup: LOOP         -- 全部要移動的記錄都已經處理完         IF NOT (           SELECT id FROM t0           WHERE processed='no'           LIMIT 1         ) THEN            LEAVE myloopup;         END IF;         -- 對要移動的記錄 t0.processed='no' 的記錄進行上移          SET l_current = (SELECT rank FROM t0 WHERE processed='no' LIMIT 1);         UPDATE TblData         SET rank=IF(rank<l_current,+l_size,-l_step)         WHERE fid=l_fid         AND rank BETWEEN l_current-l_step AND l_current+l_size-1;         -- 設置標記位,表示已經處理過         UPDATE t0 SET processed='yes' WHERE processed='no' LIMIT 1;       END LOOP myloopup;     ELSE       -- 下移操做       SET l_step = IF(         in_direction='down',         1,         (SELECT l_rank_max-rank FROM t0 LIMIT 1)       );       myloopdown: LOOP         IF NOT(           SELECT id FROM t0           WHERE processed='no'           LIMIT 1         ) THEN            LEAVE myloopdown;         END IF;         SET l_current = (SELECT rank FROM t0 WHERE processed='no' LIMIT 1);         UPDATE TblData         SET rank=rank+IF(rank>=l_current,-l_size,+l_step)         WHERE fid=l_fid         AND rank BETWEEN l_current-l_size+1 AND l_current+l_step;         -- 設置標記位,表示已經處理過         UPDATE t0 SET processed='yes' WHERE processed='no' LIMIT 1;       END loop myloopdown;     END IF;     SET out_errno = 0; -- 表示移動完成   END block2; END block0
    上述源代碼中,上移和下移的過程能夠合併,但展現的邏輯會略複雜,相對難理解。讀者若但願合併,可自行改寫代碼。
相關文章
相關標籤/搜索