İlişkisel veritabanında hiyerarşik verileri saklama seçenekleri nelerdir? [kapalı]


1333

İyi Genel Bakış

Genel olarak konuşursak, hızlı okuma süreleri (örneğin, iç içe küme) veya hızlı yazma süreleri (bitişiklik listesi) arasında karar verirsiniz. Genellikle, aşağıdaki seçeneklerin ihtiyaçlarınıza en uygun kombinasyonu ile sonuçlanırsınız. Aşağıda bazı derinlemesine okumalar verilmektedir:

Seçenekler

Biliyorum olanlar ve genel özellikleri:

  1. Bitişiklik Listesi :
    • Sütunlar: Kimlik, ParentID
    • Uygulaması kolaydır.
    • Ucuz düğüm hareket eder, ekler ve siler.
    • Seviyeyi, soyları ve soyundan gelenleri, yolu bulmak için pahalı
    • Onları destekleyen veritabanlarında Ortak Tablo İfadeleri ile N + 1'den kaçının
  2. İç İçe Set (diğer adıyla Modifiye Preorder Ağacı Geçişi )
    • Sütunlar: Sol, Sağ
    • Ucuz soy, torunları
    • Çok pahalı O(n/2)hareketler, uçlar, uçucu kodlama nedeniyle siler
  3. Köprü Tablosu (aka Kapanış Tablosu / w tetikleyicileri )
    • Aşağıdakilerle ayrı birleştirme tablosu kullanır: ata, torun, derinlik (isteğe bağlı)
    • Ucuz soylar ve kökenler
    • Ekleme O(log n), güncelleme, silme için maliyetleri (alt ağacın boyutu) yazar
    • Normalleştirilmiş kodlama: birleşimlerde RDBMS istatistikleri ve sorgu planlayıcısı için iyi
    • Düğüm başına birden çok satır gerektirir
  4. Kök Sütun (diğer adıyla Malzeme Yolu , Yol Numaralandırma)
    • Sütun: soy (örn. / Ebeveyn / çocuk / torun / vb ...)
    • Önek sorgusu üzerinden ucuz torunlar (ör. LEFT(lineage, #) = '/enumerated/path')
    • Ekleme O(log n), güncelleme, silme için maliyetleri (alt ağacın boyutu) yazar
    • İlişkisel olmayan: Dizi veri türüne veya serileştirilmiş dize biçimine dayanır
  5. İç İçe Aralıklar
    • Yuvalanmış küme gibi, ancak gerçek / float / ondalık ile kodlamanın geçici olmaması için (ucuz taşıma / ekleme / silme)
    • Gerçek / kayan nokta / ondalık gösterim / kesinlik sorunları var
    • Matris kodlama varyantı , "serbest" için atama kodlaması (materyalize yol) ekler, ancak lineer cebirin hileliğini arttırır.
  6. Düz Masa
    • Her kayda bir Düzey ve Sıra (ör. Sipariş) sütunu ekleyen değiştirilmiş bir Bitişiklik Listesi.
    • Ucuz yineleme / sayfalandırma
    • Pahalı taşıma ve silme
    • İyi Kullanım: dişli tartışma - forumlar / blog yorumları
  7. Birden çok köken sütunu
    • Sütunlar: her soy seviyesi için bir tane, köke kadar tüm ebeveynleri ifade eder, öğenin seviyesinden aşağı seviyeler NULL olarak ayarlanır
    • Atalar, torunları, seviye
    • Ucuz ekleme, silme, yaprakların hareketi
    • İç düğümlerin pahalı ekleme, silme, taşıma
    • Hiyerarşinin ne kadar derin olabileceğinin zor sınırı

Veritabanına Özel Notlar

MySQL

torpil

  • Bitişiklik Listeleri arasında geçiş yapmak için CONNECT BY kullanın

PostgreSQL

SQL Server

  • Genel Özet
  • 2008, Hiyerarşi Kimliği veri türünün Kök Sütun yaklaşımına yardımcı olduğu ve temsil edilebilecek derinliği artırdığı görülmektedir.

5
Göre slideshare.net/billkarwin/sql-antipatterns-strike-back sayfa 77, Closure Tablesüstündür Adjacency List, Path Enumerationve Nested Setskullanım kolaylığı açısından (ve ayrıca performansı tahmin ediyorum).
Gili

Burada çok basit bir sürümü özlüyorum: basit bir BLOB. Hiyerarşinizde yalnızca birkaç dozend öğesi varsa, serileştirilmiş bir kimlik ağacı en iyi seçenek olabilir.
Lothar

@Lothar: soru bir topluluk wiki'si, bu yüzden ona sahip olmaktan çekinmeyin. Bu açıdan düşüncem, bunu sadece XPATH gibi kararlı bir sorgu dili olan XML gibi bir tür blob yapılandırmasını destekleyen veritabanlarıyla yapacağım. Aksi takdirde, SQL değil kodu almak, serisini kaldırmak ve munge kenara sorgulama iyi bir yol görmüyorum. Ve gerçekten çok fazla keyfi öğeye ihtiyacınız olan bir sorununuz varsa, kullandığım ve beğendiğim Neo4J gibi Node veritabanını kullanmaktan daha iyi olabilirsiniz, ancak asla üretime geçmezsiniz.
orangepips


2
"Genel Özet" için MSDN bağlantısı artık makaleyi göstermiyor. CHM dosyası olarak indirebileceğiniz veya web.archive.org/web/20080913041559/http://msdn.microsoft.com:80/
kͩeͣmͮpͥ ͩ

Yanıtlar:


66

En sevdiğim cevap bu konudaki ilk cümlenin önerdiği şey. Hiyerarşiyi korumak için bir Bitişiklik Listesi kullanın ve hiyerarşiyi sorgulamak için İç İçe Kümeler kullanın.

Şimdiye kadar sorun, bir Bitişiklik Listesinden İç içe Kümelere kadar örtme yönteminin korkutucu derecede yavaş olmasıydı çünkü çoğu insan, dönüşümü yapmak için "Yığın Yığını" olarak bilinen aşırı RBAR yöntemini kullanıyor ve pahalı bir yol olarak görülüyor. Bitişiklik Listesi ve iç içe geçmiş kümelerin muhteşem performansı ile bakımın basitliğinin Nirvanasına ulaşmak Sonuç olarak, çoğu insan, özellikle 100.000'den fazla düğüm veya daha fazlası varsa, biri veya diğeri için yerleşmek zorunda kalır. İtme yığını yöntemini kullanmak, MLM'lerin küçük bir milyon düğüm hiyerarşisi olarak neyi düşündükleri üzerine dönüşüm yapmak için bir gün sürebilir.

Bir Bitişiklik Listesi'ni, imkansız görünen hızlarda Nested setlerine dönüştürmek için bir yöntem geliştirerek Celko'ya biraz rekabet edeceğimi düşündüm. İşte i5 dizüstü bilgisayarımdaki itme yığını yönteminin performansı.

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'

Ve işte yeni yöntemin süresi (parantez içinde itme yığını yöntemi ile).

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!!!)

Evet doğru. 1 milyon düğüm bir dakikadan kısa sürede ve 100.000 düğüm 4 saniyenin altında dönüştü.

Yeni yöntem hakkında bilgi edinebilir ve kodun bir kopyasını aşağıdaki URL'den alabilirsiniz. http://www.sqlservercentral.com/articles/Hierarchy/94040/

Benzer yöntemleri kullanarak "önceden toplanmış" bir hiyerarşi geliştirdim. MLM'ler ve malzeme listesi yapan kişiler bu makaleyle özellikle ilgilenecekler. http://www.sqlservercentral.com/articles/T-SQL/94570/

Her iki makaleye de göz atmak için uğraşıyorsanız, "Tartışmaya katıl" bağlantısına atlayın ve ne düşündüğünüzü bana bildirin.


MLMer nedir?
David Mann

MLM = "Çok Seviyeli Pazarlama". Amway, Shaklee, ACN, vb.
Jeff Moden

31

Bu sorunuza çok kısmi bir cevap, ama umarım hala faydalıdır.

Microsoft SQL Server 2008, hiyerarşik verileri yönetmek için son derece yararlı olan iki özellik uygular:

  • HierarchyId veri türü.
  • genel tablo ifadeleri, with anahtar sözcüğünü kullanarak .

Kent Tegels tarafından başlatılan "SQL Server 2008 ile Veri Hiyerarşilerinizi Modelleyin" e bir göz atın . Ayrıca bkz. Kendi sorum: SQL Server 2008'de özyinelemeli aynı tablo sorgusu


2
İlginç, Hiyerarşi
Kimliği

1
Aslında. Yinelemeli hiyerarşik verilerle çalışıyorum ve ortak tablo ifadelerini son derece kullanışlı buluyorum. Bkz msdn.microsoft.com/en-us/library/ms186243.aspx bir intro.
CesarGon

28

Bu tasarımdan henüz bahsedilmedi:

Birden çok köken sütunu

Sınırlamaları olmasına rağmen, onlara katlanabiliyorsanız, çok basit ve çok verimlidir. Özellikleri:

  • Sütunlar: her köken seviyesi için bir tane, köke kadar tüm ebeveynleri ifade eder, mevcut öğelerin seviyesinin altındaki seviyeler 0 (veya NULL) olarak ayarlanır
  • Hiyerarşinin ne kadar derin olabileceğine dair sabit bir sınır vardır
  • Atalar, torunları, seviye
  • Ucuz ekleme, silme, yaprakların hareketi
  • İç düğümlerin pahalı ekleme, silme, taşıma

İşte bir örnek - taksonomik kuş ağacı, böylece hiyerarşi Sınıf / Sıra / Aile / Cins / Türler - türler en düşük seviyedir, 1 sıra = 1 takson (yaprak düğümleri durumunda türlere karşılık gelir):

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 ''
);

ve veri örneği:

+---------+---------+---------+----------+---------+-------------------------------+
| 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                   |

Bu harika çünkü iç kategoriler ağaçtaki seviyelerini değiştirmediği sürece tüm gerekli işlemleri çok kolay bir şekilde gerçekleştiriyorsunuz.


22

Bitişiklik Modeli + İç İçe Set Modeli

Bunun için gittim çünkü ağaca yeni öğeler ekleyebildim (sadece yeni bir öğe eklemek için bir şubenin kimliğine ihtiyacınız var) ve ayrıca oldukça hızlı bir şekilde sorgulayın.

+-------------+----------------------+--------+-----+-----+
| 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 |
+-------------+----------------------+--------+-----+-----+
  • Herhangi bir ebeveynin tüm çocuklarına her ihtiyacınız olduğunda parentsütunu sorgulamanız yeterlidir .
  • Herhangi bir üst öğenin tüm torunlarına ihtiyacınız varsa, lftaralarında lftve rgtüst öğelerinde olan öğeleri sorgulayın .
  • Eğer ağacın köküne herhangi düğümün kadar tüm anne Gerekirse, sahip öğeler için sorgulamak lftdüğüm en düşük lftve rgtdüğüm daha büyüktü rgtve sıralama tarafından parent.

Ağaca erişmeyi ve sorgulamayı eklerden daha hızlı yapmam gerekiyordu, bu yüzden bunu seçtim

Tek sorun, yeni öğeler eklerken leftve rightsütunlarını düzeltmektir . Bunun için saklı bir prosedür oluşturdum ve her seferinde benim durumumda nadir olan yeni bir öğe eklediğimde çağırdım ama gerçekten hızlı. Fikri Joe Celko'nun kitabından aldım ve saklı prosedür ve bunun nasıl ortaya çıktığı burada DBA SE'de açıklandı https://dba.stackexchange.com/q/89051/41481


3
+1 bu yasal bir yaklaşımdır. Kendi deneyimlerime göre, anahtar, büyük güncelleme işlemleri gerçekleştiğinde kirli okumalar ile iyi olup olmadığınıza karar vermektir. Değilse, bir sorun haline gelir veya insanların tabloları doğrudan sorgulamasını ve her zaman bir API - DB sprocs / fonksiyon veya kodunu geçmesini engeller.
orangepips

1
Bu ilginç bir çözüm; Ancak, ana sütunu sorgulamanın çocukları bulmaya çalışırken gerçekten büyük bir avantaj sunduğundan emin değilim - bu yüzden ilk etapta sol ve sağ sütunlarımız var.
Thomas

2
@Thomas, childrenve arasında bir fark vardır descendants. leftve righttorunları bulmak için kullanılır.
azerafati

14

Veritabanınız dizileri destekliyorsa, bir üst kimlikler dizisi olarak bir köken sütunu veya somutlaştırılmış bir yol da uygulayabilirsiniz.

Özellikle Postgres ile hiyerarşiyi sorgulamak ve GIN indeksleri ile mükemmel performans elde etmek için set operatörlerini kullanabilirsiniz. Bu, ebeveynleri, çocukları ve derinliği bulmayı tek bir sorguda oldukça önemsiz hale getirir. Güncellemeler de oldukça yönetilebilir.

Merak ediyorsanız, maddileştirilmiş yollar için dizileri kullanarak tam bir yazım var .


9

Bu gerçekten kare bir delik, yuvarlak delikli bir soru.

İlişkisel veritabanları ve SQL, sahip olduğunuz veya kullanmak istediğiniz tek çekiçse, o zamana kadar gönderilen cevaplar yeterlidir. Ancak, neden hiyerarşik verileri işlemek için tasarlanmış bir araç kullanmıyorsunuz? Grafik veritabanı karmaşık hiyerarşik veriler için idealdir.

Bir grafiği / hiyerarşik modeli ilişkisel bir modelle eşleştirmek için herhangi bir kod / sorgu çözümünün karmaşıklıklarıyla birlikte ilişkisel modelin verimsizlikleri, bir grafik veritabanı çözümünün aynı sorunu çözme kolaylığına kıyasla çabaya değmez.

Ortak bir hiyerarşik veri yapısı olarak Malzeme Listesini düşünün.

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

class PartOf extends Edge {
};

class AdjacentTo extends Edge {
};

İki alt montaj arasındaki en kısa yol : Basit grafik geçiş algoritması. Kabul edilebilir yollar ölçütlere göre nitelendirilebilir.

Benzerlik : İki meclis arasındaki benzerlik derecesi nedir? İki alt ağacın kesişme noktasını ve birleşimini hesaplayan her iki alt ağaçta bir geçiş gerçekleştirin. Benzer yüzde, kesişmenin sendikaya bölünmesidir.

Geçişli kapatma : Alt ağacı yürütün ve ilgilenilen alanları toplayın, örneğin "Bir alt montajda ne kadar alüminyum var?"

Evet, sorunu SQL ve ilişkisel veritabanı ile çözebilirsiniz. Ancak, iş için doğru aracı kullanmak istiyorsanız, çok daha iyi yaklaşımlar vardır.


5
Kullanım örnekleri, bir RDBMS'de SQL yerine örneğin bir grafik veritabanının SPARQL ile nasıl sorgulanacağını gösterdiyse veya daha iyi tezat oluşturduysa, bu cevap çok daha yararlı olacaktır.
orangepips

1
SPARQL, grafik veritabanlarının daha geniş bir etki alanının alt sınıfı olan RDF veritabanlarıyla ilgilidir. Ben bir RDF veritabanı ve şu anda SPARQL desteklemiyor InfiniteGraph ile çalışır. InfiniteGraph birkaç farklı sorgu mekanizmasını destekler: (1) görünümleri, filtreleri, yol niteleyicileri ve sonuç işleyicileri ayarlamak için bir grafik gezinme API'sı, (2) karmaşık bir grafik yolu desen eşleştirme dili ve (3) Gremlin.
djhallx

6

Hiyerarşilerim için kapatma tablolarıyla PostgreSQL kullanıyorum. Tüm veritabanı için bir evrensel saklı yordam var:

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;
$_$;

Sonra bir hiyerarşim olan her tablo için bir tetikleyici oluşturuyorum

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');

Bir kapatma tablosunu varolan hiyerarşiden doldurmak için bu saklı yordamı kullanın:

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;
$$;

Kapatma tabloları 3 sütun ile tanımlanır - ANCESTOR_ID, DESCENDANT_ID, DEPTH. ANCESTOR ve DESCENDANT için aynı değere ve DEPTH için sıfır değerine sahip kayıtları saklamak mümkündür (ve hatta tavsiye ediyorum). Bu, hiyerarşinin alınması için sorguları basitleştirecektir. Ve gerçekten çok basitler:

-- 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;
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.