Решение проблемы сортировки деревьев с помощью бинарного Materialized Path

в 23:44, , рубрики: treeview, Алгоритмы, метки:

Столкнулся с проблемой реализации древовидных комментариев на одном проекте, в этой заметке выкладываю своё решение.

Описание задачи

  • Хранение иерархических данных (древовидных комментариев) в реляционной БД MySQL
  • Простое добавление узлов/ветвей
  • Выборка всего дерева одним запросом, с отсортированными в нужном порядке ветвями

В принципе, задача классическая, и сначала я её решил с помощью объединения Adjacency List и Materialized Path. Другими словами, в таблице добавлены колонки

id INT(11)
parentid INT(11)
mpath VARCHAR(255)

В mpath хранился полный путь ветки в дереве, например /1/3/4/5/8/9, где через знак "/" перечислялись ID комментария.

При таком подходе очень легко добавлять новые комментарии практически любого уровня вложенности. Выборка при этом производилась запросом:

SELECT *
FROM messages
WHERE postid=$postid
ORDER BY mpath ASC

Проблема возникла при росте числа комментариев. Например, имеется дерево со следующими путями:

1
1.2
1.2.10
1.2.3
1.2.7
4
4.8
4.8.9
5
5.6

Здесь цифрами указаны ID, а порядок такой, какой бы он получился при использовании ORDER BY mpath ASC.
Комментарий 1.2.10 идёт до 1.2.3 и др, хотя был добавлен позже (судя по ID).

Решение задачи

Решение проблемы частично было описано здесь: http://habrahabr.ru/post/125729/. Идея — использовать не десятичные ID в пути, а кодировать в другую систему счисления, чтобы иметь меньше ограничений в длине пути. При этом, разделители в виде точек или других символов не нужны, так как поле будет использоваться только для сортировки.

Я использовал основание 95, это как раз число печатаемых символов в ASCII.

!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~

Если для каждого значения в пути мы будем использовать фиксированную длину, получим следующий верхний порог ID:
1 — 95^1 — 1 = 94
2 — 95^2 — 1 = 9 024
3 — 95^3 — 1 = 857 374
4 — 95^4 — 1 = 81 450 624
5 — 95^5 — 1 = 7 737 809 374

5-ти символов вполне достаточно, чтобы не волноваться о максимальном количестве комментариев.
Максимальный уровень вложенности, при этом, для VARCHAR 255/5 = 51

Теоретически, можно брать не BASE95, а BASE256 (вместе с непечатаемыми символами), но хранить это всё бинарно в BLOB, правда тут я не уверен с производительностью при сортировке (надо проверить). Тогда уже 3 символами мы могли бы кодировать 16 777 215 вариантов, а 4 — 4 294 967 295.

Как это выглядит в коде

Приведу свой пример реализации всей этой теории.

// $mpath - хранит стандартный materialized path с разделителем "/"

// При добавлении нового комментария:
$db->Execute("
	UPDATE messages SET 
	mpath=?, 
	bpath=?,
	depth=? 
	WHERE id=?
", array(
	$mpath, 
	bpath($mpath),
	$depth, 
	$messid
));

// . . . 

// константа с символами для кодирования
define('BASE95', '!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~');

// . . . 

// функция bpath
function bpath($mpath, $sep = '/', $max_len = 5) {
	$bpath = '';
	$chunks = explode($sep, trim($mpath, $sep));
	$zero = substr(BASE95, 0, 1);
    
	foreach($chunks as $chunk) 
		$bpath .= str_pad(dec2base($chunk, 95, BASE95), $max_len, $zero, STR_PAD_LEFT);
    
	return $bpath;
}

И далее выборка:

SELECT *
FROM messages
WHERE postid=$postid
ORDER BY bpath ASC

Функция dec2base взята отсюда.

Такое вот решение, на мой взгляд очень простое. Если у вас также имеется в арсенале красивые решения, делитесь ими в комментариях.

Автор: devaka

Источник

Поделиться

* - обязательные к заполнению поля