Які варіанти зберігання ієрархічних даних у реляційній базі даних? [зачинено]


1333

Хороший огляд

Взагалі кажучи, ви приймаєте рішення між часом швидкого читання (наприклад, вкладеним набором) або часом швидкого запису (список суміжності). Зазвичай у вас є комбінація наведених нижче варіантів, які найкраще відповідають вашим потребам. Нижче наведено деяке поглиблене читання:

Параметри

Мені відомі і загальні риси:

  1. Список суміжності :
    • Стовпці: ідентифікатор, ParentID
    • Легкий у виконанні.
    • Дешевий вузол переміщується, вставляє та видаляє.
    • Дорогий, щоб знайти рівень, рід і нащадків, шлях
    • Уникайте N + 1 через загальні вирази таблиць у базах даних, які їх підтримують
  2. Вкладений набір (він же модифікований обхід дерева попереднього замовлення )
    • Стовпці: зліва, справа
    • Дешевий рід, нащадки
    • Дуже дорогі O(n/2)ходи, вставки, видалення за рахунок летючого кодування
  3. Таблиця мостів (також тригери закриття / w тригери )
    • Використовує окрему таблицю приєднання з: предком, нащадком, глибиною (необов'язково)
    • Дешеві предки та нащадки
    • Записує витрати O(log n)(розмір піддерева) на вставлення, оновлення, видалення
    • Нормалізоване кодування: добре для RDBMS-статистики та планувальників запитів приєднується
    • Потрібно кілька рядків на вузол
  4. Стовпець рядків (він же Матеріалізований шлях , перерахування шляху)
    • Стовпець: родовий (наприклад, / батько / дитина / онук / тощо ...)
    • Дешеві нащадки за допомогою префіксального запиту (наприклад LEFT(lineage, #) = '/enumerated/path')
    • Записує витрати O(log n)(розмір піддерева) на вставлення, оновлення, видалення
    • Нереляційний: спирається на тип даних масиву або серіалізований формат рядків
  5. Вкладені інтервали
    • Як і вкладений набір, але з реальним / float / decimal, щоб кодування не було мінливим (недороге переміщення / вставка / видалення)
    • Має проблеми з реальним / плаваючим / десятковим поданням / точністю
    • Варіант кодування матриці додає кодування предків (матеріалізований шлях) для "вільного", але з додатковою хитрістю лінійної алгебри.
  6. Плоский стіл
    • Модифікований список суміжності, який додає стовпець рівня та рейтингу (наприклад, замовлення) до кожного запису.
    • Недорогий перегляд / пагінат
    • Дороге переміщення та видалення
    • Хороше використання: поточна дискусія - форуми / коментарі до блогу
  7. Кілька стовпців рядків
    • Стовпці: по одному для кожного рівня рядка, стосується всіх батьків до кореня; рівні вниз від рівня елемента встановлюються на NULL
    • Дешеві предки, нащадки, рівень
    • Дешеві вставити, видалити, перемістити листя
    • Дороге вставлення, видалення, переміщення внутрішніх вузлів
    • Важкий обмеження наскільки глибокою може бути ієрархія

Конкретні примітки до бази даних

MySQL

Oracle

  • Використовуйте CONNECT BY для переходу до списків суміжності

PostgreSQL

SQL Server

  • Загальний підсумок
  • 2008 пропонує тип даних HierarchyId , що допомагає підходити до рядка Lineage Column та розширювати глибину, яку можна представити.

5
Згідно slideshare.net/billkarwin/sql-antipatterns-strike-back сторінці 77, Closure Tablesперевершують Adjacency List, Path Enumerationі Nested Setsз точки зору простоти використання (і я припускаю , що продуктивність, а).
Гілі

Тут я пропускаю дуже просту версію: простий BLOB. Якщо у вашій ієрархії є лише кілька пунктів, то найкращим варіантом може бути серіалізоване дерево id.
Лотар

@Lothar: питання - це вікі спільноти, тому не соромтеся. Я вважаю, що я б це зробив лише з тими базами даних, які підтримують структуру блобів, таких як XML зі стійкою мовою запитів, як XPATH. Інакше я не бачу хорошого способу запиту окрім отримання, деаріалізації та об'єднання коду, а не SQL. І якщо у вас дійсно є проблеми, коли вам потрібно багато довільних елементів, вам може бути краще використовувати базу даних Node, наприклад Neo4J, яку я використовував і сподобався, хоча і ніколи не брався до виробництва.
orangepips


2
Це посилання MSDN для "Загального резюме" стаття більше не відображає. Це було у випуску журналу MSDN у вересні 2008 року, який ви можете завантажити як файл CHM або переглянути через веб-архів за адресою: web.archive.org/web/20080913041559/http://msdn.microsoft.com:80/ …
kͩeͣmͮpͥ ͩ

Відповіді:


66

Моя улюблена відповідь - це те, що запропонувало перше речення в цій темі. Використовуйте список суміжності для підтримання ієрархії та використовуйте вкладені набори для запиту ієрархії.

До цих пір проблема полягала в тому, що метод покриття зі списку суміжності до вкладених наборів був страшенно повільним, оскільки більшість людей використовують екстремальний метод RBAR, відомий як "Push Stack" для перетворення, і вважається, що це дорого коштує щоб досягти Нірвани простоти обслуговування за списком суміжності та дивовижною роботою вкладених наборів. Як результат, більшості людей в кінцевому підсумку доводиться влаштовуватися на те чи інше, особливо якщо є, скажімо, паршиві 100 000 вузлів чи так. Використання методу push stack може зайняти цілий день для перетворення того, що MLM'ers вважає ієрархією невеликих мільйонів вузлів.

Я думав, що я змусив би Селко трохи конкурувати, придумавши метод перетворення списку суміжності у вкладені набори зі швидкістю, яка просто здається неможливою. Ось ефективність методу push stack на моєму ноутбуці i5.

Duration for     1,000 Nodes = 00:00:00:870 
Duration for    10,000 Nodes = 00:01:01:783 (70 times slower instead of just 10)
Duration for   100,000 Nodes = 00:49:59:730 (3,446 times slower instead of just 100) 
Duration for 1,000,000 Nodes = 'Didn't even try this'

А ось тривалість нового методу (з методом push stack в дужках).

Duration for     1,000 Nodes = 00:00:00:053 (compared to 00:00:00:870)
Duration for    10,000 Nodes = 00:00:00:323 (compared to 00:01:01:783)
Duration for   100,000 Nodes = 00:00:03:867 (compared to 00:49:59:730)
Duration for 1,000,000 Nodes = 00:00:54:283 (compared to something like 2 days!!!)

Так, це правильно. 1 мільйон вузлів перетворюється менш ніж за хвилину, а 100 000 вузлів - за 4 секунди.

Ви можете прочитати про новий метод і отримати копію коду за наступною URL-адресою. http://www.sqlservercentral.com/articles/Hierarchy/94040/

Я також розробив "попередньо агреговану" ієрархію, використовуючи подібні методи. MLM'ers та люди, що роблять купюри матеріалів, будуть особливо зацікавлені у цій статті. http://www.sqlservercentral.com/articles/T-SQL/94570/

Якщо ви зупинитесь, щоб ознайомитись із будь-якою статтею, перейдіть до посилання "Приєднайтесь до дискусії" і дайте мені знати, що ви думаєте.


Що таке MLMer?
Девід Манн

MLM = "Багаторівневий маркетинг". Amway, Shaklee, ACN тощо, тощо.
Джефф Моден

31

Це дуже часткова відповідь на ваше запитання, але я сподіваюся, що все-таки корисно.

Microsoft SQL Server 2008 реалізує дві функції, надзвичайно корисні для управління ієрархічними даними:

  • hierarchyid` в тип даних.
  • загальні табличні вирази, використовуючи з ключовим словом.

Перегляньте "Моделі своїх ієрархій даних за допомогою SQL Server 2008" від Kent Tegels на MSDN для запуску. Дивіться також моє власне запитання: Рекурсивний запит за однією таблицею в SQL Server 2008


2
Цікаво, що Ієрархічний Ід
orangepips

1
Справді. Я працюю з великою кількістю рекурсивно ієрархічних даних, і вважаю загальні вирази таблиці надзвичайно корисними. Див. Msdn.microsoft.com/en-us/library/ms186243.aspx для вступу.
CesarGon

28

Цей дизайн ще не згадувався:

Кілька стовпців рядків

Хоча це має обмеження, якщо ви можете їх нести, це дуже просто і дуже ефективно. Особливості:

  • Стовпці: по одному для кожного рівня рядка, стосується всіх батьків до кореня; рівні нижче рівня поточних елементів встановлюються на 0 (або NULL)
  • Існує фіксований межа того, наскільки глибокою може бути ієрархія
  • Дешеві предки, нащадки, рівень
  • Дешеві вставити, видалити, перемістити листя
  • Дороге вставлення, видалення, переміщення внутрішніх вузлів

Ось наступний приклад - таксономічне дерево птахів, тому ієрархія - Клас / Порядок / Родина / Рід / Вид - вид - найнижчий рівень, 1 ряд = 1 таксон (що відповідає видам у випадку з листяними вузлами):

CREATE TABLE `taxons` (
  `TaxonId` smallint(6) NOT NULL default '0',
  `ClassId` smallint(6) default NULL,
  `OrderId` smallint(6) default NULL,
  `FamilyId` smallint(6) default NULL,
  `GenusId` smallint(6) default NULL,
  `Name` varchar(150) NOT NULL default ''
);

та приклад даних:

+---------+---------+---------+----------+---------+-------------------------------+
| TaxonId | ClassId | OrderId | FamilyId | GenusId | Name                          |
+---------+---------+---------+----------+---------+-------------------------------+
|     254 |       0 |       0 |        0 |       0 | Aves                          |
|     255 |     254 |       0 |        0 |       0 | Gaviiformes                   |
|     256 |     254 |     255 |        0 |       0 | Gaviidae                      |
|     257 |     254 |     255 |      256 |       0 | Gavia                         |
|     258 |     254 |     255 |      256 |     257 | Gavia stellata                |
|     259 |     254 |     255 |      256 |     257 | Gavia arctica                 |
|     260 |     254 |     255 |      256 |     257 | Gavia immer                   |
|     261 |     254 |     255 |      256 |     257 | Gavia adamsii                 |
|     262 |     254 |       0 |        0 |       0 | Podicipediformes              |
|     263 |     254 |     262 |        0 |       0 | Podicipedidae                 |
|     264 |     254 |     262 |      263 |       0 | Tachybaptus                   |

Це чудово, тому що таким чином ви виконуєте всі необхідні операції дуже просто, доки внутрішні категорії не змінюють свій рівень у дереві.


22

Модель суміжності + вкладена модель наборів

Я пішов за цим, тому що я міг легко вставити нові елементи в дерево (вам просто потрібен ідентифікатор гілки, щоб вставити до нього новий елемент), а також досить швидко запитати його.

+-------------+----------------------+--------+-----+-----+
| category_id | name                 | parent | lft | rgt |
+-------------+----------------------+--------+-----+-----+
|           1 | ELECTRONICS          |   NULL |   1 |  20 |
|           2 | TELEVISIONS          |      1 |   2 |   9 |
|           3 | TUBE                 |      2 |   3 |   4 |
|           4 | LCD                  |      2 |   5 |   6 |
|           5 | PLASMA               |      2 |   7 |   8 |
|           6 | PORTABLE ELECTRONICS |      1 |  10 |  19 |
|           7 | MP3 PLAYERS          |      6 |  11 |  14 |
|           8 | FLASH                |      7 |  12 |  13 |
|           9 | CD PLAYERS           |      6 |  15 |  16 |
|          10 | 2 WAY RADIOS         |      6 |  17 |  18 |
+-------------+----------------------+--------+-----+-----+
  • Кожен раз, коли вам потрібні всі діти будь-якого батька, ви просто запитуєте parentстовпець.
  • Якщо вам потрібні всі нащадки будь-якого з батьків, ви запитаєте предмети, які мають їх lftміж батьками lftта rgtбатьками.
  • Якщо вам потрібні були всі батьки будь-якого вузла до кореня дерева, ви запитуєте елементи, що мають lftнижчий за вузол lftі rgtбільший за вузол, rgtі сортуйте по parent.

Мені потрібно було зробити доступ до дерева і запитувати його швидше, ніж вставляти, тому я вибрав саме це

Єдина проблема - це виправити стовпці leftта rightстовпці під час вставки нових елементів. добре, я створив для нього збережену процедуру і називав її кожен раз, коли я вставляв новий елемент, що було рідко в моєму випадку, але це дуже швидко. Я отримав ідею з книги Джо Селко, а збережена процедура та те, як я її придумав, пояснено тут у DBA SE https://dba.stackexchange.com/q/89051/41481


3
+1 це законний підхід. З мого власного досвіду, ключ вирішує, чи добре ви з брудними читаннями, коли відбуваються великі операції з оновлення. Якщо ні, то це стає предметом або заважає людям безпосередньо запитувати таблиці та завжди переходити через API - проростки / функції або код БД.
orangepips

1
Це цікаве рішення; однак я не впевнений, що запит батьківського стовпця справді пропонує якусь головну перевагу при спробі пошуку дітей - саме тому у нас в першу чергу ліві та праві стовпці.
Томас

2
@Thomas, є різниця між childrenі descendants. leftі rightвикористовуються для пошуку нащадків.
azerafati

14

Якщо ваша база даних підтримує масиви, ви також можете реалізувати стовпець рядка або матеріалізований шлях як масив батьківських ідентифікаторів.

Зокрема, за допомогою Postgres ви можете використовувати задані оператори для запиту ієрархії та отримання відмінної продуктивності за допомогою індексів GIN. Це робить батьків, дітей та глибину досить дрібницею в одному запиті. Оновлення також досить керовані.

У мене є повне написання використання масивів для матеріалізованих шляхів, якщо вам цікаво.


9

Це справді квадратний кілочок, питання з круглим отвором.

Якщо реляційні бази даних і SQL є єдиним молотом, який ви маєте або готові використовувати, то відповіді, що були розміщені до цього часу, є адекватними. Однак чому б не використати інструмент, призначений для обробки ієрархічних даних? Графічна база даних ідеально підходить для складних ієрархічних даних.

Неефективність реляційної моделі, а також складність будь-якого рішення коду / запиту для відображення графіка / ієрархічної моделі на реляційну модель просто не вартує зусиль у порівнянні з простотою, з якою рішення бази даних графіків може вирішити ту саму проблему.

Розглянемо законопроект як загальну ієрархічну структуру даних.

class Component extends Vertex {
    long assetId;
    long partNumber;
    long material;
    long amount;
};

class PartOf extends Edge {
};

class AdjacentTo extends Edge {
};

Найкоротший шлях між двома підскладами : Простий алгоритм переходу графіків. Прийнятні шляхи можна кваліфікувати за критеріями.

Схожість : Який ступінь подібності між двома збірками? Виконайте обхід обох під дерев, обчислюючи перетин та об’єднання двох під дерев. Відсоток схожий на перетин, поділений союзом.

Перехідне закриття : пройдіть по під дереву та підведіть підсумки поля (поля), яке цікавить, наприклад, "Скільки алюмінію знаходиться в підскладі?"

Так, ви можете вирішити проблему за допомогою SQL та реляційної бази даних. Однак є набагато кращі підходи, якщо ви готові використовувати правильний інструмент для роботи.


5
Ця відповідь була б надзвичайно кориснішою, якби випадки використання продемонстрували або, ще краще, протиставили, як запитувати базу даних графіків із SPARQL, наприклад, замість SQL в RDBMS.
orangepips

1
SPARQL має відношення до баз даних RDF, які є підкласом більшої області баз даних графіків. Я працюю з InfiniteGraph, який не є базою даних RDF і на даний момент не підтримує SPARQL. InfiniteGraph підтримує декілька різних механізмів запитів: (1) графічний API навігації для налаштування представлень, фільтрів, класифікаторів контурів та обробників результатів, (2) складний шаблон графічного контуру, що відповідає мові, та (3) Gremlin.
djhallx

6

Я використовую PostgreSQL з таблицями закриття для своїх ієрархій. У мене є одна універсальна збережена процедура для всієї бази даних:

CREATE FUNCTION nomen_tree() RETURNS trigger
    LANGUAGE plpgsql
    AS $_$
DECLARE
  old_parent INTEGER;
  new_parent INTEGER;
  id_nom INTEGER;
  txt_name TEXT;
BEGIN
-- TG_ARGV[0] = name of table with entities with PARENT-CHILD relationships (TBL_ORIG)
-- TG_ARGV[1] = name of helper table with ANCESTOR, CHILD, DEPTH information (TBL_TREE)
-- TG_ARGV[2] = name of the field in TBL_ORIG which is used for the PARENT-CHILD relationship (FLD_PARENT)
    IF TG_OP = 'INSERT' THEN
    EXECUTE 'INSERT INTO ' || TG_ARGV[1] || ' (child_id,ancestor_id,depth) 
        SELECT $1.id,$1.id,0 UNION ALL
      SELECT $1.id,ancestor_id,depth+1 FROM ' || TG_ARGV[1] || ' WHERE child_id=$1.' || TG_ARGV[2] USING NEW;
    ELSE                                                           
    -- EXECUTE does not support conditional statements inside
    EXECUTE 'SELECT $1.' || TG_ARGV[2] || ',$2.' || TG_ARGV[2] INTO old_parent,new_parent USING OLD,NEW;
    IF COALESCE(old_parent,0) <> COALESCE(new_parent,0) THEN
      EXECUTE '
      -- prevent cycles in the tree
      UPDATE ' || TG_ARGV[0] || ' SET ' || TG_ARGV[2] || ' = $1.' || TG_ARGV[2]
        || ' WHERE id=$2.' || TG_ARGV[2] || ' AND EXISTS(SELECT 1 FROM '
        || TG_ARGV[1] || ' WHERE child_id=$2.' || TG_ARGV[2] || ' AND ancestor_id=$2.id);
      -- first remove edges between all old parents of node and its descendants
      DELETE FROM ' || TG_ARGV[1] || ' WHERE child_id IN
        (SELECT child_id FROM ' || TG_ARGV[1] || ' WHERE ancestor_id = $1.id)
        AND ancestor_id IN
        (SELECT ancestor_id FROM ' || TG_ARGV[1] || ' WHERE child_id = $1.id AND ancestor_id <> $1.id);
      -- then add edges for all new parents ...
      INSERT INTO ' || TG_ARGV[1] || ' (child_id,ancestor_id,depth) 
        SELECT child_id,ancestor_id,d_c+d_a FROM
        (SELECT child_id,depth AS d_c FROM ' || TG_ARGV[1] || ' WHERE ancestor_id=$2.id) AS child
        CROSS JOIN
        (SELECT ancestor_id,depth+1 AS d_a FROM ' || TG_ARGV[1] || ' WHERE child_id=$2.' 
        || TG_ARGV[2] || ') AS parent;' USING OLD, NEW;
    END IF;
  END IF;
  RETURN NULL;
END;
$_$;

Потім для кожної таблиці, де у мене є ієрархія, я створюю тригер

CREATE TRIGGER nomenclature_tree_tr AFTER INSERT OR UPDATE ON nomenclature FOR EACH ROW EXECUTE PROCEDURE nomen_tree('my_db.nomenclature', 'my_db.nom_helper', 'parent_id');

Для заповнення таблиці закриття з існуючої ієрархії я використовую цю збережену процедуру:

CREATE FUNCTION rebuild_tree(tbl_base text, tbl_closure text, fld_parent text) RETURNS void
    LANGUAGE plpgsql
    AS $$
BEGIN
    EXECUTE 'TRUNCATE ' || tbl_closure || ';
    INSERT INTO ' || tbl_closure || ' (child_id,ancestor_id,depth) 
        WITH RECURSIVE tree AS
      (
        SELECT id AS child_id,id AS ancestor_id,0 AS depth FROM ' || tbl_base || '
        UNION ALL 
        SELECT t.id,ancestor_id,depth+1 FROM ' || tbl_base || ' AS t
        JOIN tree ON child_id = ' || fld_parent || '
      )
      SELECT * FROM tree;';
END;
$$;

Таблиці закриття визначені 3 стовпцями - ANCESTOR_ID, DESCENDANT_ID, DEPTH. Можна (і я навіть радити) зберігати записи з однаковим значенням для ANCESTOR і DESCENDANT, а також значенням нуля для DEPTH. Це спростить запити щодо отримання ієрархії. І вони справді дуже прості:

-- get all descendants
SELECT tbl_orig.*,depth FROM tbl_closure LEFT JOIN tbl_orig ON descendant_id = tbl_orig.id WHERE ancestor_id = XXX AND depth <> 0;
-- get only direct descendants
SELECT tbl_orig.* FROM tbl_closure LEFT JOIN tbl_orig ON descendant_id = tbl_orig.id WHERE ancestor_id = XXX AND depth = 1;
-- get all ancestors
SELECT tbl_orig.* FROM tbl_closure LEFT JOIN tbl_orig ON ancestor_id = tbl_orig.id WHERE descendant_id = XXX AND depth <> 0;
-- find the deepest level of children
SELECT MAX(depth) FROM tbl_closure WHERE ancestor_id = XXX;
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.