Prawdopodobnie nieraz zdarzyło się wam budować drzewiaste struktury danych w bazach danych. Typowa tabela do tego celu wygląda mniej więcej tak:

id | parent_id | name
---+-----------+-------------
 1 |         0 | Lorem
 2 |         0 | Ipsum
 3 |         1 | Dolor
 4 |         1 | Sit
 5 |         2 | Consectetuer
 6 |         4 | Adipiscing
 7 |         4 | Elit

Pozwala na dowolnie głębokie zagnieżdżanie danych, zaś obiekty o parent_id równym 0 są elementami najwyższego poziomu hierarchii.

Problem pojawia się, kiedy dane te trzeba zwizualizować w postaci drzewa. Początkujący programiści często używają do tego celu rekursji (właściwie, to używają jej do wszystkiego), jednak ma ona podstawową wadę. O ile w przypadku powyższej tabeli nie ma zasadniczo różnicy, czy dane będą pobierane rekurencyjnie czy liniowo, o tyle przy tysiącu kategorii, gdzie w najwyższym poziomie znajduje się ich kilkaset, liczba zapytań wywoływanych przez skrypt sięga setek na każde wyświetlenie strony.

Jest to problemem zwłaszcza, kiedy serwer SQL znajduje się na innej maszynie i każde zapytanie wymaga przesyłania danych w obie strony połączenia. Wprowadza to dodatkowe opóźnienia, a z czasem staje się nader widoczne.

Rozwiązaniem jest jednorazowe pociągniecie wszystkich potrzebnych danych za pomocą jednego zapytania, a następnie przebudowanie w strukturę drzewiastą po stronie serwera docelowego.

Jako przykład takiego działania posłużyć może poniższy skrypt PHP:

require_once('arraySort.class.php');

/**
 * Create a tree structure from an SQL query
 */

class sqlTree
{
	var $conn;

	function sqlTree(&$conn)
	{
		$this->conn = & $conn;
	}

	/**
	 * Sort an array
	 * @access private
	 */
	function _multiSort(&$array, $key, $asc = true)
	{
		$sorter = & new arraySort($array, $key, $asc);
		return $sorter->sortit();
	}

	/**
	 * Sort a tree
	 * @access private
	 */
	function _treeSort(&$item)
	{
		reset($item);
		foreach($item as $key => $val)
		{
			if (isset($val['tree']))
				$this->_treeSort($item[$key]['tree']);
		}
		$item = $this->_multiSort($item, 'sort', true);
	}

	/**
	 * Build the tree, elements with no known parent are ignored
	 * @param string SQL query, has to contain fields 'id' and 'parent_id'
	 * @param int parent_id to be considered root level
	 * @return array the tree structure
	 */
	function build($query, $root = 0)
	{
		$result = mysql_query($query, $this->conn);

		$queue = array();
		while($item = mysql_fetch_assoc($result))
		{
			array_push($queue, $item);
		}

		$queue = $this->_multiSort($queue, 'id', false);
		$effect = array();

		while($queue)
		{
			$found = false;
			reset($queue);
			$nextIter = $queue;
			foreach($queue as $key => $item)
			{
				if ($item['parent_id'] != $root)
				{
					reset($nextIter);
					foreach($nextIter as $keyTarg => $target)
					if ($target['id'] == $item['parent_id'])
					{
						if (!isset($nextIter[$keyTarg]['tree']))
							$nextIter[$keyTarg]['tree'] = array();

						array_push($nextIter[$keyTarg]['tree'], $item);
						unset($nextIter[$key]);
						$found = true;
						break;
					}

					if ($found)
						break;

					// not found
					unset($nextIter[$key]);
				}
				else
				{
					array_push($effect, $item);
					unset($nextIter[$key]);
					break;
				}
			}
			$queue = $nextIter;
		}
		$this->_treeSort($effect);
		return $effect;
	}
}

Jest to klasa, która jako parametr konstruktora pobiera identyfikator połączenia do bazy danych MySQL i udostępnia funkcję build(), która jako pierwszy parametr przyjmuje zapytanie wybierające dane, zaś jako drugi opcjonalny identyfikator korzenia drzewa.

Wymagania odnośnie zapytania są następujące: musi ono zawierać w wynikach kolumny o nazwach id, parent_id i sort, przy czym ten ostatni decyduje o kolejności pozycji drzewa. Dodatkowym ograniczeniem (powyższej implementacji) jest założenie, że dla każdego elementu zachodzi parent_id < id.

Przykładowe użycie:

$st = & new sqlTree($connection);
$tree = $st->build(
'
	SELECT id, parent_id, name AS sort
	FROM przykladowa_tabela
');

print_r($tree);

Do działania wymagany jest jeszcze plik arraySort.class.php, czyli skrypt sortowania wielowymiarowych tablic autorstwa Oliwiera Ptaka:

/**
 * Handles multidimentional array sorting by a key (not recursive)
 *
 * @author Oliwier Ptak <aleczapka at gmx dot net>
 */

class arraySort
{
	var $skey = false;
	var $sarray = false;
	var $sasc = true;

	/**
	 * Constructor
	 *
	 * @access public
	 * @param mixed $array array to sort
	 * @param string $key array key to sort by
	 * @param boolean $asc sort order (ascending or descending)
	 */
	function arraySort(&$array, $key, $asc = true)
	{
		$this->sarray = $array;
		$this->skey = $key;
		$this->sasc = $asc;
	}

	/**
	 * Sort method
	 *
	 * @access public
	 * @param boolean $remap if true reindex the array to rewrite indexes
	 */
	function sortit($remap = true)
	{
		$array = &$this->sarray;
		uksort($array, array(&$this, "_as_cmp"));
		if ($remap)
		{
			$tmp = array();
			while (list($id, $data) = each($array))
				array_push($tmp, $data);
			return $tmp;
		}
		return $array;
	}

	/**
	 * Custom sort function
	 *
	 * @access private
	 * @param mixed $a an array entry
	 * @param mixed $b an array entry
	 */
	function _as_cmp($a, $b)
	{
		//since uksort will pass here only indexes get real values from our array
		if (!is_array($a) && !is_array($b))
		{
			$a = $this->sarray[$a][$this->skey];
			$b = $this->sarray[$b][$this->skey];
		}

		//if string - use string comparision
		if (!is_numeric($a) && !is_numeric($b))
		{
			if ($this->sasc)
				return strcasecmp($a, $b);
			else
				return strcasecmp($b, $a);
		}
		else
		{
			if (intval($a) == intval($b))
				return 0;

			if ($this->sasc)
				return (intval($a) > intval($b)) ? 1 : -1;
			else
				return (intval($a) > intval($b)) ? -1 : 1;
		}
	}
}

Zapraszam do analizy kodu. Zaproponowana implementacja nie jest oczywiście najdoskonalsza, ale służyć ma jedynie za przykład, że zadanie jest do rozwiązania bez stosowania rekursji w zapytaniach do bazy danych, co jest operacją bardzo kosztowną czasowo.