Автор:
RXLНаписано: 10.12.2009.
Права на статью принадлежат автору и
Клубу программистов «Весельчак У».
Небольшое дополнение к ранее вышедшей статье «
MySQL. Иерархические запросы». Здесь предлагаю решение той же самой задачи — выборки поддерева на связях id — parent_id, но без динамического SQL — на временных таблицах. Такой вариант работает много быстрее, но процедуры оказываются жестко привязанными к таблицам.
Для начала напомню особенности временных таблиц. Имя временной таблицы не должно совпадать с именем уже существующей таблицы, но в разных сессиях могут быть временные таблицы с одинаковыми именами. Также удобно то, что они автоматически удаляются при завершении сессии и не замусоривают базу. Временные таблицы могут быть форматов MEMORY (что полезно для скорости работы) и MYISAM.
Для начала создаем тестовую таблицу:
CREATE TABLE t1 (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
parent_id INT UNSIGNED NOT NULL,
lvl SMALLINT UNSIGNED NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY parent (parent_id, id),
UNIQUE KEY lvl (lvl, id)
) ENGINE = MYISAM;
Этой процедурой заполним таблицу тестовыми данными:
CREATE PROCEDURE fill2(IN levels INT UNSIGNED, IN branches INT UNSIGNED)
BEGIN
DECLARE lvl INT UNSIGNED DEFAULT 1;
DECLARE n INT UNSIGNED;
CREATE TEMPORARY TABLE _fill_tmp1 (id INT UNSIGNED NOT NULL) ENGINE=MEMORY;
INSERT INTO t1 (parent_id, lvl) VALUES (0, 0);
WHILE lvl < levels DO
INSERT INTO _fill_tmp1
SELECT t1.id FROM t1
WHERE t1.lvl = lvl - 1;
SET n = 0;
while n < branches DO
INSERT INTO t1 (parent_id, lvl)
SELECT id, lvl FROM _fill_tmp1;
SET n = n + 1;
END WHILE;
TRUNCATE TABLE _fill_tmp1;
SET lvl = lvl + 1;
END WHILE;
DROP TABLE _fill_tmp1;
END;
Параметрами можно регулировать количество уровней — levels и степень ветвления дерева (сколько у каждого узла будет потомков) — branches.
Заполним таблицу деревом из 6 уровней и с степенью ветвления 10. Должно получиться SUM(branchesn) узлов, где n от 0 до levels - 1. В нашем случае должно получиться 111111 узлов.
mysql> call fill2(6, 10);
Query OK, 0 rows affected (1.78 sec)
mysql> select count(*) from t1;
+----------+
| count(*) |
+----------+
| 111111 |
+----------+
1 row in set (0.00 sec)
mysql> select * from t1 order by lvl, id limit 31;
+----+-----------+-----+
| id | parent_id | lvl |
+----+-----------+-----+
| 1 | 0 | 0 |
| 2 | 1 | 1 |
| 3 | 1 | 1 |
| 4 | 1 | 1 |
| 5 | 1 | 1 |
| 6 | 1 | 1 |
| 7 | 1 | 1 |
| 8 | 1 | 1 |
| 9 | 1 | 1 |
| 10 | 1 | 1 |
| 11 | 1 | 1 |
| 12 | 2 | 2 |
| 13 | 3 | 2 |
| 14 | 4 | 2 |
| 15 | 5 | 2 |
| 16 | 6 | 2 |
| 17 | 7 | 2 |
| 18 | 8 | 2 |
| 19 | 9 | 2 |
| 20 | 10 | 2 |
| 21 | 11 | 2 |
| 22 | 2 | 2 |
| 23 | 3 | 2 |
| 24 | 4 | 2 |
| 25 | 5 | 2 |
| 26 | 6 | 2 |
| 27 | 7 | 2 |
| 28 | 8 | 2 |
| 29 | 9 | 2 |
| 30 | 10 | 2 |
| 31 | 11 | 2 |
+----+-----------+-----+
31 rows in set (0.00 sec)
Похоже на правду...
Процедура для выборки заданного поддерева:
CREATE PROCEDURE fetch_subtree_ids2(
IN base INT UNSIGNED,
IN max_levels INT UNSIGNED
)
BEGIN
DECLARE currlevel INT DEFAULT 1;
DECLARE cnt INT DEFAULT 0;
DROP TABLE IS EXISTS _fst_parents, _fst_tmp, _fst_result;
CREATE TEMPORARY TABLE _fst_parents (id INT UNSIGNED NOT NULL) ENGINE=MEMORY;
CREATE TEMPORARY TABLE _fst_tmp (id INT UNSIGNED NOT NULL) ENGINE=MEMORY;
CREATE TEMPORARY TABLE _fst_result (id INT UNSIGNED NOT NULL) ENGINE=MEMORY;
INSERT INTO _fst_parents VALUES (base);
INSERT INTO _fst_result VALUES (base);
-- В случае если max_levels равно 0, то допускаем 100000 уровней.
SET max_levels = IF(max_levels, max_levels, 100000);
REPEAT
INSERT INTO _fst_tmp
SELECT h.id
FROM _fst_parents t, t1 h
WHERE h.parent_id = t.id;
SELECT COUNT(*) INTO cnt FROM _fst_tmp;
INSERT INTO _fst_result
SELECT id FROM _fst_tmp;
TRUNCATE TABLE _fst_parents;
INSERT INTO _fst_parents
SELECT id FROM _fst_tmp;
TRUNCATE TABLE _fst_tmp;
SET currlevel = currlevel + 1;
UNTIL (cnt = 0 OR currlevel > max_levels) END REPEAT;
SELECT COUNT(id) FROM _fst_result;
DROP TABLE _fst_parents, _fst_tmp, _fst_result;
END;
Процедура чисто для демонстрации — чтобы замерить время, затрачиваемое на выборку идентификаторов поддерева. Для реальной работы надо заменить строку
SELECT COUNT(id) FROM _fst_result;
на
SELECT id FROM _fst_result;
Проверим работу процедуры на практике! Напоминаю, что у нас 6 уровней, фактор ветвления 10 и в итоге — 111111 узлов. Выберем все дерево:
mysql> call fetch_subtree_ids2(1, 0);
+-----------+
| COUNT(id) |
+-----------+
| 111111 |
+-----------+
1 row in set (0.67 sec)
Query OK, 0 rows affected (0.67 sec)
Полное дерево выбралось за 0.67 секунды. Это, конечно, долго, но и узлов много. Попробуем выбрать в 100 раз меньший фрагмент:
mysql> call fetch_subtree_ids2(50, 0);
+-----------+
| COUNT(id) |
+-----------+
| 1111 |
+-----------+
1 row in set (0.01 sec)
Query OK, 0 rows affected (0.01 sec)
На выборку поддерева из 1111 элементов (4 уровня) затрачено всего 10 миллисекунд! Замечу, что такое поддерево великовато для типичных задач с каталогами и на практике может потребоваться еще меньше времени.
Еще быстрее работает метод вложенных деревьев, но у него есть существенный недостаток — большие вычислительные затраты при изменении дерева (добавлении новых узлов или сжатии дерева после удаления узлов).