Достичь иерархии, Родительского/Детского отношения эффективным и простым способом

У меня есть таблица типа

create table site
(
site_Id int(5),
parent_Id int(5),
site_desc varchar2(100)
);

Значение полей:

  • site_Id: Идентификатор сайтов
  • parent_Id: родительский идентификатор сайта
  • site_desc: хотя и не относится к вопросу, но имеет описание сайта

Требование состоит в том, что если у меня есть site_id как вход, и мне нужны все идентификаторы, помеченные под сайтом. Например:

                    A
                   / \
                  B   C
                / | \ /\
               D  E F G H
              /\
             I  J

Все узлы являются site_Id.

Таблица содержит следующие данные:

Site_id  | Parent_ID  |  site_desc
_________|____________|___________
 A       |   -1       |   
 B       |    A       |
 C       |    A       |
 D       |    B       |
 E       |    B       |
 F       |    B       |
 I       |    D       |
 J       |    D       |

......

A является родительским элементом B и C и т.д.

Если B - введенный ввод, тогда запрос должен получить D, E, I, F, J

В настоящее время это достигается несколькими запросами в цикле, но я думал, чтобы достичь этого в минимальном количестве запросов.

В настоящее время я делаю следующее:

down vote

Алгоритм выглядит следующим образом:

Initially create a data set object which you will populate, by fetching data from the data base. 
Create a method which takes the parent id as parameter and returns its child nodes if present, and returns -1, if it doesnt have a child. 
Step1: Fetch all the rows, which doesn't have a parent(root) node. 
Step2: Iterate through this result. For example if prod1 and prod2 are the initial returned nodes, in the resultset. 
Iterating this RS we get prod1, and we insert a row in our DataSET obj. 
Then we send the id of prod1 to getCHILD method, to get its child, and then again we iterate the returned resultset, and again call the getCHILD method, till we dont get the lowest node.

Мне нужна лучшая оптимизированная техника в рамках ограничения моей модели данных. Не стесняйтесь отвечать, если у вас есть какие-либо предложения.
Пожалуйста, предложите что-нибудь. Спасибо заранее.

Ответ 1

К сожалению, если вы не можете изменить модель данных и используете MySQL, вы застряли в ситуации, когда вам нужны рекурсивные запросы, и вы используете СУБД, которая не поддерживает рекурсивные запросы.

Quassnoi написал интересную серию статей в блогах, демонстрируя методы для запросов к иерархическим данным. Его решения довольно умны, но очень сложны. http://explainextended.com/2009/03/17/hierarchical-queries-in-mysql/

PostgreSQL - это еще одна RDBMS с открытым исходным кодом, которая поддерживает рекурсивные запросы, поэтому вы можете получить целые деревья, хранящиеся в том виде, в котором вы показываете, Но если вы не можете изменить модель данных, я бы предположил, что вы не можете переключиться на другую СУБД.

Существует несколько альтернативных моделей данных, которые значительно облегчают выборку произвольных глубинных деревьев:

  • Таблица закрытия
  • Вложенные наборы aka Измененный обход дерева предзаказов
  • Перечисление пути aka Материализованный путь

Я рассказываю об этом в своей презентации Модели для иерархических данных с SQL и PHP, и в моей книге SQL Antipatterns: предотвращение ошибок программирования баз данных.

Наконец, есть другое решение, которое я видел в коде для Slashdot, для своих иерархий комментариев: они хранят "parent_id", как в Adjacency List, но они также хранят столбец "root_id". Каждый член данного дерева имеет такое же значение для root_id, что является наивысшим предком node в его дереве. Тогда легко получить целое дерево в одном запросе:

SELECT * FROM site WHERE root_id = 123;

Затем ваше приложение извлекает все узлы из базы данных в массив, и вам нужно написать код для цикла над этим массивом, вставив узлы в структуру данных дерева в памяти. Это хорошее решение, если у вас много отдельных деревьев, и каждое дерево имеет относительно немного записей. Это хорошо для случая Slashdot.

Ответ 2

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

Вы можете сделать это, используя только один вызов в базе данных, но есть вид catch: вы должны вернуть строки all из таблицы. MySQL не поддерживает рекурсивные запросы, поэтому вместо этого вам необходимо выполнить SELECT ing в коде приложения.

Я просто повторю свой ответ, который я связал выше, но в основном, если вы возвращаете результат (возможно, из PDOStatement->fetchAll(PDO::FETCH_ASSOC) или других методов) в формате чего-то вроде:

Array
(
    [0] => Array
    (
        [site_id] => A
        [parent_id] => -1
        [site_desc] => testtext
    )
    [1] => Array
    (
        [site_id] => B
        [parent_id] => A
        [site_desc] => testtext
    )
    [2] => Array
    (
        [site_id] => C
        [parent_id] => A
        [site_desc] => testtext
    )
    [3] => Array
    (
        [site_id] => D
        [parent_id] => B
        [site_desc] => testtext
    )
    [4] => Array
    (
        [site_id] => E
        [parent_id] => B
        [site_desc] => testtext
    )
    [5] => Array
    (
        [site_id] => F
        [parent_id] => B
        [site_desc] => testtext
    )
    [6] => Array
    (
        [site_id] => I
        [parent_id] => D
        [site_desc] => testtext
    )
    [7] => Array
    (
        [site_id] => J
        [parent_id] => D
        [site_desc] => testtext
    )
)

С помощью этой рекурсивной функции вы можете получить всех детей/внуков/великих детей/так далее любого site_id (при условии, что вы знаете его):

function fetch_recursive($src_arr, $id, $parentfound = false, $cats = array())
{
    foreach($src_arr as $row)
    {
        if((!$parentfound && $row['site_id'] == $id) || $row['parent_id'] == $id)
        {
            $rowdata = array();
            foreach($row as $k => $v)
                $rowdata[$k] = $v;
            $cats[] = $rowdata;
            if($row['parent_id'] == $id)
                $cats = array_merge($cats, fetch_recursive($src_arr, $row['site_id'], true));
        }
    }
    return $cats;
}

Например, скажем, что вы хотите получить все дочерние элементы site_id D, вы должны использовать такую ​​функцию:

$nodelist = fetch_recursive($pdostmt->fetchAll(PDO::FETCH_ASSOC), 'D');
print_r($nodelist);

Вывести:

[0] => Array
(
    [site_id] => D
    [parent_id] => B
    [site_desc] => testtext
)
[1] => Array
(
    [site_id] => I
    [parent_id] => D
    [site_desc] => testtext
)
[2] => Array
(
    [site_id] => J
    [parent_id] => D
    [site_desc] => testtext
)

Обратите внимание, что мы сохраняем информацию родителя вместе со своими дочерьми, внуками и т.д. (как бы глубока ни была вложенность).

Ответ 3

Проверьте модель вложенного набора, если вы хотите иметь возможность сделать это в одном запросе: http://mikehillyer.com/articles/managing-hierarchical-data-in-mysql/

Другой альтернативой является включение всех связей в таблицу ссылок. Таким образом, каждый сайт будет иметь ссылку на своего родителя, его дедушку и бабушку и т.д. Все отношения явны. Затем вы просто запрашиваете эту таблицу связей для получения всех потомков.

Ответ 4

Начнем с того, что я бы порекомендовал немного другой метод хранения дерева: Closure Table. Если вы хотите узнать больше об этом, вы можете найти SQL Antipatterns книгу довольно интересную.

Это сказало. Самый простой способ, на мой взгляд, создать такую ​​структуру: http://jsbin.com/omexix/3/edit#javascript

Надеюсь, у вас нет проблем с чтением кода JavaScript. Я использовал его, потому что создание неклассифицированных объектов в JavaScript не выглядит таким хакером. Можно реализовать то же самое без ретрансляции на объекте (или ссылках) с помощью многомерного массива, но это выглядит запутанно.

Вот что делает алгоритм:

  • мы перебираем список узлов, один раз
  • Если родитель node не выходит, в массиве создается файл-заполнитель
  • если node не имеет родителя, он помещается в список корневых узлов
  • Если node не имеет заполнителя в массиве, создается заполнитель
  • значения из node присваиваются заполнителю
  • node зарегистрирован для родителя, если у него есть родительский

Это о нем. В основном вы создаете два списка: со всеми узлами и только с корнем node.

Ответ 5

Возможно, вам захочется взглянуть на шаблон закрытия таблицы. Я нашел этот сайт информативным. Насколько мне известно, также есть несколько вопросов StackOverflow об этой концепции, например здесь.

Ответ 6

Если вы не обновляете таблицу site часто, вы можете использовать следующую стратегию:

create table site
(
site_Id int(5),
parent_Id int(5),
site_desc varchar2(100),
parents_path varchar(X)
);

parents_path равен пути к выбранному node от root. Например, для листа J он должен быть |A|B|D|.

Плюсы: - для получения результата вам понадобится один запрос;

Против: - больше запросов во время обновлений (но вы можете делать обновления с умом);

Надеюсь, что это поможет

Ответ 7

Другие уже предложили, как это сделать, с небольшими изменениями в таблице структура.

Если вы не хотите изменять структуру (даже если это было бы лучше), вы можете сделать например:

  • SELECT * FROM ORDER ORDER BY Parent_ID, Site_id;

Обычно можно с уверенностью предположить, что после присвоения идентификаторы не изменяются; если идентификаторы не перетасовываться, т.е. node C не перемещается под node B, тогда это будет true, что дочерние узлы всегда имеют более высокие идентификаторы, чем их родители, и сортировка выше, гарантирует, что все родители будут доставлены перед их детьми.

Итак, это гипотезы:

- we prefer not to change the table layout
- we never change the IDs once assigned
- we never reorder the tree, moving IDs around

Следовательно, становится возможным создать дерево в памяти (и даже уменьшить запрос сам добавляя WHERE Site_ID >= B).

Первый node, который должен пройти, будет B и будет помещен в дерево.

Все последующие узлы могут храниться в их Parent_ID-th node, что, безусловно, было загружен раньше.

В Python это будет очень хорошо (вы напрямую изменяете родительский node).

На запрос "Получить всех потомков B" можно ответить на PHP следующим образом:

$nodes  = array( $parent_id );

$cursor = SQLQuery("SELECT * FROM site WHERE Site_ID > ? "
        .  "ORDER BY Parent_ID, Site_Id ;", $parent_id);

while ($tuple = SQLFetchTuple($cursor))
    if (in_array($tuple['Parent_ID'], $nodes))
        $nodes[] = $tuple['Site_Id'];
SQLFree($cursor);

// The first node is the global parent, and may be array_shift'ed away
    // if desired.

Другой способ
довольно грубая сила

Другая возможность заключается в том, чтобы рекурсивно сохранять отношение "descendant_of" в другом Таблица:

    TRUNCATE descendants;
    INSERT INTO descendants ( node, of ) VALUES ( -1, NULL );

    INSERT INTO descendants SELECT SiteId, ParentId FROM site JOIN
           descendants ON ( site.ParentId = descendants.of );

И повторите INSERT, пока количество вставленных строк не будет равно нулю (или общее количество ряды потомков перестают расти; размер таблицы запросов очень быстр в большинстве БД).

На этом этапе вы сохраните все одноуровневые отношения. Сейчас:

INSERT IGNORE INTO descendants SELECT s1.node, s2.of FROM
           descendants AS s1 JOIN descendants AS s2 ON (s1.of = s2.node);

... снова, пока потомки не перестанут увеличиваться (для этого потребуется количество вставок, равное максимальное количество уровней). Общее количество СОЕДИНЕНИЙ будет в два раза больше уровней.

Теперь, если вы хотите получить все потомки node 16, вы просто запрашиваете

SELECT node FROM descendants WHERE of = 16;

Ответ 8

Вы можете создать хранимую процедуру для этого.

Вот моя реализация в mysql

DROP PROCEDURE IF EXISTS SearchTree;
DELIMITER go

CREATE PROCEDURE SearchTree( IN root CHAR(1) )
BEGIN
  DECLARE rows SMALLINT DEFAULT 0;
  DROP TABLE IF EXISTS reached;
  CREATE TABLE reached (
    site_Id CHAR(1) PRIMARY KEY
  ) ENGINE=HEAP;
  INSERT INTO reached VALUES (root);
  SET rows = ROW_COUNT();
  WHILE rows > 0 DO
    INSERT IGNORE INTO reached 
      SELECT DISTINCT s.site_Id 
      FROM site AS s 
      INNER JOIN reached AS r ON s.parent_Id = r.site_Id;
    SET rows = ROW_COUNT();
    DELETE FROM reached 
      WHERE site_Id = root;
  END WHILE;
  SELECT * FROM reached;
  DROP TABLE reached;
END;
go
DELIMITER ;
CALL SearchTree('B');

Он возвращает ожидаемый результат.

Ответ 9

Основываясь на ваших комментариях, я предполагаю, что вы не хотите менять существующую модель данных, потому что сотни приложений используют ее (и будут разбиты, если вы замените ее чем-то другим).

Корень проблемы заключается в том, что для любого сайта мы знаем только его непосредственный родитель, поэтому нам нужно искать родительский элемент этого родителя рекурсивно, пока мы не найдем корневой сайт.

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

Увольнение нескольких запросов умножает все накладные расходы, поэтому мы этого не хотим. Выполнение SELECT *, а затем вычисление в логике приложения означает, что вы будете извлекать все данные каждый раз, максимизируя накладные расходы сети, поэтому мы этого не хотим.

Если предел глубины дерева допустим, вы можете объединить несколько запросов в один огромный запрос, который выполняет всю работу и возвращает точный набор результатов, который вам нужен. В качестве примера я использовал ваши данные, но с заменой A, B, C и т.д. На 1, 2, 3 (поскольку ваши столбцы являются int).

Чтобы получить все прямые дочерние элементы корня node (с site_id = 1), выполните следующие действия:

select site_id from site where parent_id = 1

Чтобы получить внуков от корня node, сделайте следующее:

select grandchild.site_id 
from site grandchild, site child 
where grandchild.parent_id = child.site_id 
and child.parent_id = 1

Чтобы получить правнуков корня node, сделайте следующее:

select greatgrandchild.site_id 
from site greatgrandchild, site grandchild, site child 
where greatgrandchild.parent_id = grandchild.site_id 
and grandchild.parent_id = child.site_id 
and child.parent_id = 1

Чтобы получить все потомки корня node, просто объедините вышеуказанные запросы в один огромный запрос, например:

select site_id
from site
where site_id in (
    select site_id 
    from site 
    where parent_id = 1
)
or site_id in (
    select grandchild.site_id 
    from site grandchild, site child 
    where grandchild.parent_id = child.site_id 
    and child.parent_id = 1
)
or site_id in (
    select greatgrandchild.site_id 
    from site greatgrandchild, site grandchild, site child 
    where greatgrandchild.parent_id = grandchild.site_id 
    and grandchild.parent_id = child.site_id 
    and child.parent_id = 1
)

Я думаю, вы видите, как это работает. Для каждого дополнительного уровня создайте запрос, который найдет узлы, которые находятся на многих уровнях, от сайта, на котором вы ищете потомков, и добавьте этот запрос в супер-запрос с дополнительным 'или site_id in()'...

Теперь, как вы можете видеть, только для трех уровней, это уже становится большим запросом. Если вам нужно поддерживать, скажем, 10 уровней, этот запрос станет огромным, и все OR и IN в нем замедлят его работу... Тем не менее, он по-прежнему будет быстрее, чем просто получить все или использовать несколько запросов. Если вам нужно поддерживать произвольное количество возможных уровней, то этот запрос не сможет вам помочь. Он должен был бы стать бесконечно большим. В этом случае все, что остается, это использовать лучший способ...

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

ЛУЧШИЙ ПУТЬ

Добавьте дополнительные столбцы parent_paths, используя что-то вроде ravnur, упомянутое в его ответе, чтобы закодировать полный путь от каждого node вплоть до корня

Заполните этот столбец динамически, используя triggers для вставки, обновления и удаления. Теперь вы сохраняете избыточные данные. Это не повредит другим программам, но может принести вам значительную выгоду. Убедитесь, что триггеры являются пуленепробиваемыми (возможно, самая сложная часть), поскольку данные в дополнительном столбце должны всегда синхронизироваться с обычными данными в таблице

Используйте короткий и сладкий запрос, например, один ravnur, показывающий, что ищет место в site_id в любом месте в столбце parent_paths, чтобы напрямую получить всех потомков сайта с этим site_id без какой-либо рекурсии.

Ответ 10

я также спросил себя, как рекурсивно обрабатывать отношения, и мой мозг создал это решение (:

SELECT * FROM
(
    SELECT t2.* FROM table t1, table t2 where t2.parent = t1.id OR t2.parent 0 GROUP BY t2.id, t2.parent
) as all_relations
WHERE all_relations.parent >= '_the_id_'

# if you dont want a subtree use only the inner select

Я не уверен на 100%, но я думаю, что до тех пор, пока идентификатор автоматически увеличивается, а у ребенка никогда не будет меньшего id в качестве родителя (это должен быть нормальный случай), то это может быть решением?