22

lftrghtおよびparent_id列を使用してMySQLに保存された100,000を超えるレコードのMPTTツリーがあります。これで、親IDはそのままで、左/右の値が破損しました。アプリケーション層で修復するには、大量のクエリが必要になります。データベースに負担をかけ、SQLのみを使用して左右の値を再計算させる良い方法はありますか?


明確にするために、隣接するレコードのIDではなく、ネストされたセットの数値のlft/rght値を再計算する必要があります。

ネストされたセット
(ソース:mysql.com

4

4 に答える 4

22

これが@Lievenの回答から採用したもので、パフォーマンスを向上させるためにここからのフィードバックを取り入れています。

DROP PROCEDURE IF EXISTS tree_recover;

DELIMITER //

CREATE PROCEDURE tree_recover ()
MODIFIES SQL DATA
BEGIN

    DECLARE currentId, currentParentId  CHAR(36);
    DECLARE currentLeft                 INT;
    DECLARE startId                     INT DEFAULT 1;

    # Determines the max size for MEMORY tables.
    SET max_heap_table_size = 1024 * 1024 * 512;

    START TRANSACTION;

    # Temporary MEMORY table to do all the heavy lifting in,
    # otherwise performance is simply abysmal.
    CREATE TABLE `tmp_tree` (
        `id`        char(36) NOT NULL DEFAULT '',
        `parent_id` char(36)          DEFAULT NULL,
        `lft`       int(11)  unsigned DEFAULT NULL,
        `rght`      int(11)  unsigned DEFAULT NULL,
        PRIMARY KEY      (`id`),
        INDEX USING HASH (`parent_id`),
        INDEX USING HASH (`lft`),
        INDEX USING HASH (`rght`)
    ) ENGINE = MEMORY
    SELECT `id`,
           `parent_id`,
           `lft`,
           `rght`
    FROM   `tree`;

    # Leveling the playing field.
    UPDATE  `tmp_tree`
    SET     `lft`  = NULL,
            `rght` = NULL;

    # Establishing starting numbers for all root elements.
    WHILE EXISTS (SELECT * FROM `tmp_tree` WHERE `parent_id` IS NULL AND `lft` IS NULL AND `rght` IS NULL LIMIT 1) DO

        UPDATE `tmp_tree`
        SET    `lft`  = startId,
               `rght` = startId + 1
        WHERE  `parent_id` IS NULL
          AND  `lft`       IS NULL
          AND  `rght`      IS NULL
        LIMIT  1;

        SET startId = startId + 2;

    END WHILE;

    # Switching the indexes for the lft/rght columns to B-Trees to speed up the next section, which uses range queries.
    DROP INDEX `lft`  ON `tmp_tree`;
    DROP INDEX `rght` ON `tmp_tree`;
    CREATE INDEX `lft`  USING BTREE ON `tmp_tree` (`lft`);
    CREATE INDEX `rght` USING BTREE ON `tmp_tree` (`rght`);

    # Numbering all child elements
    WHILE EXISTS (SELECT * FROM `tmp_tree` WHERE `lft` IS NULL LIMIT 1) DO

        # Picking an unprocessed element which has a processed parent.
        SELECT     `tmp_tree`.`id`
          INTO     currentId
        FROM       `tmp_tree`
        INNER JOIN `tmp_tree` AS `parents`
                ON `tmp_tree`.`parent_id` = `parents`.`id`
        WHERE      `tmp_tree`.`lft` IS NULL
          AND      `parents`.`lft`  IS NOT NULL
        LIMIT      1;

        # Finding the element's parent.
        SELECT  `parent_id`
          INTO  currentParentId
        FROM    `tmp_tree`
        WHERE   `id` = currentId;

        # Finding the parent's lft value.
        SELECT  `lft`
          INTO  currentLeft
        FROM    `tmp_tree`
        WHERE   `id` = currentParentId;

        # Shifting all elements to the right of the current element 2 to the right.
        UPDATE `tmp_tree`
        SET    `rght` = `rght` + 2
        WHERE  `rght` > currentLeft;

        UPDATE `tmp_tree`
        SET    `lft` = `lft` + 2
        WHERE  `lft` > currentLeft;

        # Setting lft and rght values for current element.
        UPDATE `tmp_tree`
        SET    `lft`  = currentLeft + 1,
               `rght` = currentLeft + 2
        WHERE  `id`   = currentId;

    END WHILE;

    # Writing calculated values back to physical table.
    UPDATE `tree`, `tmp_tree`
    SET    `tree`.`lft`  = `tmp_tree`.`lft`,
           `tree`.`rght` = `tmp_tree`.`rght`
    WHERE  `tree`.`id`   = `tmp_tree`.`id`;

    COMMIT;

    DROP TABLE `tmp_tree`;

END//

DELIMITER ;

いくつかのテストデータでうまく機能しましたが、それはまだ私の100,000レコードツリーで実行されているので、まだ最終的な判断を下すことができません。物理テーブルで直接動作するナイーブスクリプトのパフォーマンスはひどく、少なくとも数時間、場合によっては数日間実行されます。一時的なMEMORYテーブルに切り替えると、この時間は約1時間に短縮され、適切なインデックスを選択すると10分に短縮されます。

于 2010-09-03T08:53:45.667 に答える
8

SQL Server を使用すると、次のスクリプトが機能しているようです。

出力テストスクリプト

category_id name                 parent      lft         rgt         lftcalc     rgtcalc
----------- -------------------- ----------- ----------- ----------- ----------- -----------
1           ELECTRONICS          NULL        1           20          1           20
2           TELEVISIONS          1           2           9           2           9
3           TUBE                 2           3           4           3           4
4           LCD                  2           5           6           5           6
5           PLASMA               2           7           8           7           8
6           PORTABLE ELECTRONICS 1           10          19          10          19
7           MP3 PLAYERS          6           11          14          11          14
8           FLASH                7           12          13          12          13
9           CD PLAYERS           6           15          16          15          16
10          2 WAY RADIOS         6           17          18          17          18

脚本

SET NOCOUNT ON
GO

DECLARE @nested_category TABLE (
 category_id INT PRIMARY KEY,
 name VARCHAR(20) NOT NULL,
 parent INT,
 lft INT,
 rgt INT
);

DECLARE @current_Category_ID INTEGER
DECLARE @current_parent INTEGER
DECLARE @SafeGuard INTEGER
DECLARE @myLeft INTEGER
SET @SafeGuard = 100

INSERT INTO @nested_category 
SELECT           1,'ELECTRONICS',NULL,NULL,NULL
UNION ALL SELECT 2,'TELEVISIONS',1,NULL,NULL
UNION ALL SELECT 3,'TUBE',2,NULL,NULL
UNION ALL SELECT 4,'LCD',2,NULL,NULL
UNION ALL SELECT 5,'PLASMA',2,NULL,NULL
UNION ALL SELECT 6,'PORTABLE ELECTRONICS',1,NULL,NULL
UNION ALL SELECT 7,'MP3 PLAYERS',6,NULL,NULL
UNION ALL SELECT 8,'FLASH',7,NULL,NULL
UNION ALL SELECT 9,'CD PLAYERS',6,NULL,NULL
UNION ALL SELECT 10,'2 WAY RADIOS',6,NULL,NULL

/* Initialize */
UPDATE  @nested_category 
SET     lft = 1
        , rgt = 2
WHERE   parent IS NULL

UPDATE  @nested_category 
SET     lft = NULL
        , rgt = NULL
WHERE   parent IS NOT NULL

WHILE EXISTS (SELECT * FROM @nested_category WHERE lft IS NULL) AND @SafeGuard > 0
BEGIN
  SELECT  @current_Category_ID = MAX(nc.category_id)
  FROM    @nested_category nc
          INNER JOIN @nested_category nc2 ON nc2.category_id = nc.parent
  WHERE   nc.lft IS NULL
          AND nc2.lft IS NOT NULL

  SELECT  @current_parent = parent
  FROM    @nested_category
  WHERE   category_id = @current_category_id

  SELECT  @myLeft = lft
  FROM    @nested_category
  WHERE   category_id = @current_parent

  UPDATE @nested_category SET rgt = rgt + 2 WHERE rgt > @myLeft;
  UPDATE @nested_category SET lft = lft + 2 WHERE lft > @myLeft;
  UPDATE @nested_category SET lft = @myLeft + 1, rgt = @myLeft + 2 WHERE category_id = @current_category_id

  SET @SafeGuard = @SafeGuard - 1
END

SELECT * FROM @nested_category ORDER BY category_id

SELECT  COUNT(node.name), node.name, MIN(node.lft)
FROM    @nested_category AS node,
        @nested_category AS parent
WHERE   node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY 
        node.name
ORDER BY
        3, 1

テストスクリプト ##

SET NOCOUNT ON
GO

DECLARE @nested_category TABLE (
 category_id INT PRIMARY KEY,
 name VARCHAR(20) NOT NULL,
 parent INT,
 lft INT,
 rgt INT, 
 lftcalc INT,
 rgtcalc INT
);

INSERT INTO @nested_category 
SELECT           1,'ELECTRONICS',NULL,1,20,NULL,NULL
UNION ALL SELECT 2,'TELEVISIONS',1,2,9,NULL,NULL
UNION ALL SELECT 3,'TUBE',2,3,4,NULL,NULL
UNION ALL SELECT 4,'LCD',2,5,6,NULL,NULL
UNION ALL SELECT 5,'PLASMA',2,7,8,NULL,NULL
UNION ALL SELECT 6,'PORTABLE ELECTRONICS',1,10,19,NULL,NULL
UNION ALL SELECT 7,'MP3 PLAYERS',6,11,14,NULL,NULL
UNION ALL SELECT 8,'FLASH',7,12,13,NULL,NULL
UNION ALL SELECT 9,'CD PLAYERS',6,15,16,NULL,NULL
UNION ALL SELECT 10,'2 WAY RADIOS',6,17,18,NULL,NULL

/* Initialize */
UPDATE  @nested_category 
SET     lftcalc = 1
        , rgtcalc = 2
WHERE   parent IS NULL

DECLARE @current_Category_ID INTEGER
DECLARE @current_parent INTEGER
DECLARE @SafeGuard INTEGER
DECLARE @myRight INTEGER
DECLARE @myLeft INTEGER
SET @SafeGuard = 100
WHILE EXISTS (SELECT * FROM @nested_category WHERE lftcalc IS NULL) AND @SafeGuard > 0
BEGIN
  SELECT  @current_Category_ID = MAX(nc.category_id)
  FROM    @nested_category nc
          INNER JOIN @nested_category nc2 ON nc2.category_id = nc.parent
  WHERE   nc.lftcalc IS NULL
          AND nc2.lftcalc IS NOT NULL

  SELECT  @current_parent = parent
  FROM    @nested_category
  WHERE   category_id = @current_category_id

  SELECT  @myLeft = lftcalc
  FROM    @nested_category
  WHERE   category_id = @current_parent

  UPDATE @nested_category SET rgtcalc = rgtcalc + 2 WHERE rgtcalc > @myLeft;
  UPDATE @nested_category SET lftcalc = lftcalc + 2 WHERE lftcalc > @myLeft;
  UPDATE @nested_category SET lftcalc = @myLeft + 1, rgtcalc = @myLeft + 2 WHERE category_id = @current_category_id

  SELECT * FROM @nested_category WHERE category_id = @current_parent
  SELECT * FROM @nested_category ORDER BY category_id
  SET @SafeGuard = @SafeGuard - 1
END

SELECT * FROM @nested_category ORDER BY category_id

SELECT  COUNT(node.name), node.name, MIN(node.lft)
FROM    @nested_category AS node,
        @nested_category AS parent
WHERE   node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY 
        node.name
ORDER BY
        3, 1
于 2010-09-02T07:25:56.827 に答える
4

あなたは私を救ってください!!! 私は混合ツリー モデルを使用しているため、その日が来ると、私のツリー (30000+) が破損していました。私はあなたの両方の技術から学びますが、回復ではありません。ソートと逆ツリーのすべてを失って完全に再構築するだけです...古い cat_left を覚えておく必要があると思います....完全性の可能性のために。だから、それはおそらく...のように見えます

DROP PROCEDURE IF EXISTS tree_recover;

区切り文字 |

CREATE PROCEDURE tree_recover ()
SQL データを変更します
始める

    DECLARE currentId, currentParentId CHAR(36);
    DECLARE currentLeft INT;
    DECLARE startId INT DEFAULT 1;

    # MEMORY テーブルの最大サイズを決定します。
    SET max_heap_table_size = 1024 * 1024 * 512;

    取引を開始します。

    # すべての面倒な作業を行うための一時的な MEMORY テーブル、
    # それ以外の場合、パフォーマンスは単に最悪です。
    DROP TABLE IF EXISTS `tmp_cat`;
    CREATE TABLE `tmp_cat` (
        `cat_id` char(36) NOT NULL DEFAULT '',
        `cat_parent` char(36) DEFAULT NULL,
        `cat_left` int(11) unsigned DEFAULT NULL,
        `cat_right` int(11) unsigned DEFAULT NULL,
        `cat_left_old` int(11) unsigned DEFAULT NULL,
        主キー (`cat_id`)、
        ハッシュを使用したインデックス (`cat_parent`)、
        ハッシュを使用したインデックス (`cat_left`),
        ハッシュを使用したインデックス (`cat_right`)、
    ハッシュを使用したインデックス (`cat_left_old`)
    ) エンジン = メモリ
    SELECT `cat_id`、
           `cat_parent`、
           `cat_left`、
           `cat_right`、
       `cat_left` as cat_left_old
    FROM `カタログ`;

    # 競技場の平準化。
    UPDATE `tmp_cat`
    SET `cat_left` = NULL,
            `cat_right` = NULL;

    # すべてのルート要素の開始番号を確立します。
    WHILE EXISTS (SELECT * FROM `tmp_cat` WHERE `cat_parent` IS NULL AND `cat_left` IS NULL AND `cat_right` IS NULL ORDER BY cat_left_old LIMIT 1) DO

        UPDATE `tmp_cat`
        SET `cat_left` = startId,
               `cat_right` = startId + 1
        WHERE `cat_parent` IS NULL
          AND `cat_left` IS NULL
          AND `cat_right` IS NULL
        リミット 1;

        SET startId = startId + 2;

    終了まで;

    # cat_left/rght 列のインデックスを B ツリーに切り替えて、範囲クエリを使用する次のセクションを高速化します。
    DROP INDEX `cat_left` ON `tmp_cat`;
    DROP INDEX `cat_right` ON `tmp_cat`;
    DROP INDEX `cat_left_old` ON `tmp_cat`;

    CREATE INDEX `cat_left` USING BTREE ON `tmp_cat` (`cat_left`);
    CREATE INDEX `cat_right` USING BTREE ON `tmp_cat` (`cat_right`);
    CREATE INDEX `cat_left_old` USING BTREE ON `tmp_cat` (`cat_left_old`);

    # すべての子要素に番号を付ける
    WHILE EXISTS (SELECT * FROM `tmp_cat` WHERE `cat_left` IS NULL ORDER BY cat_left_old LIMIT 1) DO

        # 処理された親を持つ未処理の要素を選択します。
        SELECT `tmp_cat`.`cat_id`
          INTO currentId
        FROM `tmp_cat`
        INNER JOIN `tmp_cat` AS `parents`
                ON `tmp_cat`.`cat_parent` = `parents`.`cat_id`
        WHERE `tmp_cat`.`cat_left` IS NULL
          AND `parents`.`cat_left` IS NOT NULL
    ORDER BY `tmp_cat`.cat_left_old DESC
        リミット 1;

        # 要素の親を見つける。
        SELECT `猫の親`
          INTO currentParentId
        FROM `tmp_cat`
        WHERE `cat_id` = currentId;

        # 親の cat_left 値を見つける。
        SELECT `cat_left`
          INTO currentLeft
        FROM `tmp_cat`
        WHERE `cat_id` = currentParentId;

        # 現在の要素 2 の右にあるすべての要素を右にシフトします。
        UPDATE `tmp_cat`
        SET `cat_right` = `cat_right` + 2
        WHERE `cat_right` > currentLeft;

        UPDATE `tmp_cat`
        SET `cat_left` = `cat_left` + 2
        WHERE `cat_left` > currentLeft;

        # 現在の要素の cat_left と rght の値を設定します。
        UPDATE `tmp_cat`
        SET `cat_left` = currentLeft + 1,
               `cat_right` = 現在の左 + 2
        WHERE `cat_id` = currentId;

    終了まで;

    # 計算値を物理テーブルに書き戻す。
    UPDATE `catalog`, `tmp_cat`
    SET `catalog`.`cat_left` = `tmp_cat`.`cat_left`,
           `catalog`.`cat_right` = `tmp_cat`.`cat_right`
    WHERE `catalog`.`cat_id` = `tmp_cat`.`cat_id`;

    専念;

    DROP TABLE IF EXISTS `tmp_cat`;

終わり|
于 2012-07-26T10:16:19.093 に答える
1

提供されたすべてのソリューションで、MySQLRunning queryが何時間もかかるというメッセージを表示するが、何も起こらないという問題が発生していました。

その後、tmp_tree テーブルの最初のレコード ( parent_id = 0. これを自動的に行うには、手順を更新する必要があるかもしれません。

于 2012-10-21T10:18:06.167 に答える