PostgreSQL Özyinelemeli Descendant Derinliği


15

Bir torunun atalarından derinliğini hesaplamam gerekiyor. Bir kayıt olduğunda object_id = parent_id = ancestor_id, bu bir kök düğüm (atası) olarak kabul edilir. WITH RECURSIVEPostgreSQL 9.4 ile çalışan bir sorgu almaya çalışıyorum .

Verileri veya sütunları kontrol etmiyorum. Veri ve tablo şeması harici bir kaynaktan gelir. Masa sürekli büyüyor . Şu anda günde yaklaşık 30 bin kayıt. Ağaçtaki herhangi bir düğüm eksik olabilir ve bir noktada harici bir kaynaktan çekilirler. Genellikle created_at DESCsırayla alınır, ancak veriler eşzamansız arka plan işleriyle çekilir.

Başlangıçta bu soruna bir kod çözümü vardı, ancak şimdi 5M + satırları var, tamamlanması neredeyse 30 dakika sürüyor.

Örnek tablo tanımı ve test verileri:

CREATE TABLE objects (
  id          serial NOT NULL PRIMARY KEY,
  customer_id integer NOT NULL,
  object_id   integer NOT NULL,
  parent_id   integer,
  ancestor_id integer,
  generation  integer NOT NULL DEFAULT 0
);

INSERT INTO objects(id, customer_id , object_id, parent_id, ancestor_id, generation)
VALUES (2, 1, 2, 1, 1, -1), --no parent yet
       (3, 2, 3, 3, 3, -1), --root node
       (4, 2, 4, 3, 3, -1), --depth 1
       (5, 2, 5, 4, 3, -1), --depth 2
       (6, 2, 6, 5, 3, -1), --depth 3
       (7, 1, 7, 7, 7, -1), --root node
       (8, 1, 8, 7, 7, -1), --depth 1
       (9, 1, 9, 8, 7, -1); --depth 2

Bunun object_idbenzersiz olmadığını, ancak kombinasyonun (customer_id, object_id)benzersiz olduğunu unutmayın.
Bunun gibi bir sorgu çalıştırma:

WITH RECURSIVE descendants(id, customer_id, object_id, parent_id, ancestor_id, depth) AS (
  SELECT id, customer_id, object_id, parent_id, ancestor_id, 0
  FROM objects
  WHERE object_id = parent_id

  UNION

  SELECT o.id, o.customer_id, o.object_id, o.parent_id, o.ancestor_id, d.depth + 1
  FROM objects o
  INNER JOIN descendants d ON d.parent_id = o.object_id
  WHERE
    d.id <> o.id
  AND
    d.customer_id = o.customer_id
) SELECT * FROM descendants d;

generationSütunun hesaplanan derinlik olarak ayarlanmasını istiyorum . Yeni bir kayıt eklendiğinde, oluşturma sütunu -1 olarak ayarlanır. Henüz parent_idçekilmemiş olabilecek bazı durumlar vardır . Bu parent_idyoksa, oluşturma sütununu -1 olarak ayarlanmış olarak bırakmalısınız.

Nihai veriler şöyle görünmelidir:

id | customer_id | object_id | parent_id | ancestor_id | generation
2    1             2           1           1            -1
3    2             3           3           3             0
4    2             4           3           3             1
5    2             5           4           3             2
6    2             6           5           3             3
7    1             7           7           7             0
8    1             8           7           7             1
9    1             9           8           7             2

Sorgunun sonucu, oluşturma sütununu doğru derinliğe güncellemek olmalıdır.

SO ile ilgili bu sorunun cevabından çalışmaya başladım .


Yani updateözyinelemeli CTE sonucunuzla masaya mı girmek istiyorsunuz ?
a_horse_with_no_name

Evet, oluşturma sütununun derinliğine göre GÜNCELLENMESİ istiyorum. Üst öğe yoksa (objects.parent_id, hiçbir nesne.object_id ile eşleşmez), nesil -1 olarak kalır.

Yani ancestor_idzaten ayarlanmış, bu yüzden sadece CTE.depth'den nesneyi atamanız mı gerekiyor?

Evet, object_id, parent_id ve ancestor_id, API'dan aldığımız verilerden zaten ayarlanmış. Üretim sütununu derinlik ne olursa olsun ayarlamak istiyorum. Diğer bir not, object_id benzersiz değil, çünkü customer_id 1 nesne_kimliği 1 ve customer_id 2 nesne_kimliği 1 olabilir. Tablodaki birincil kimlik benzersizdir.

Bu bir defalık güncelleme mi yoksa sürekli büyüyen bir tabloya mı ekliyorsunuz? İkinci davaya benziyor. Bir yapar büyük bir fark. Ve sadece kök düğümler eksik (henüz) veya ağaçtaki herhangi bir düğüm olabilir mi?
Erwin Brandstetter

Yanıtlar:


14

Sahip olduğunuz sorgu temel olarak doğrudur. Tek hata, CTE'nin sahip olduğunuz ikinci (özyinelemeli) kısımdır:

INNER JOIN descendants d ON d.parent_id = o.object_id

Öbür türlü olmalı:

INNER JOIN descendants d ON d.object_id = o.parent_id 

Nesneleri ebeveynleri ile (daha önce bulunan) birleştirmek istiyorsunuz.

Böylece derinliği hesaplayan sorgu yazılabilir (başka hiçbir şey değişmez, sadece biçimlendirme):

-- calculate generation / depth, no updates
WITH RECURSIVE descendants
  (id, customer_id, object_id, parent_id, ancestor_id, depth) AS
 AS ( SELECT id, customer_id, object_id, parent_id, ancestor_id, 0
      FROM objects
      WHERE object_id = parent_id

      UNION ALL

      SELECT o.id, o.customer_id, o.object_id, o.parent_id, o.ancestor_id, d.depth + 1
      FROM objects o
      INNER JOIN descendants d ON  d.customer_id = o.customer_id
                               AND d.object_id = o.parent_id  
      WHERE d.id <> o.id
    ) 
SELECT * 
FROM descendants d
ORDER BY id ;

Güncelleştirme için, sadece son değiştirmek SELECTile, UPDATECTE sonucunu katılarak, masaya geri:

-- update nodes
WITH RECURSIVE descendants
    -- nothing changes here except
    -- ancestor_id and parent_id 
    -- which can be omitted form the select lists
    ) 
UPDATE objects o 
SET generation = d.depth 
FROM descendants d
WHERE o.id = d.id 
  AND o.generation = -1 ;          -- skip unnecessary updates

SQLfiddle üzerinde test edildi

Ek Yorumlar:

  • ancestor_idve parent_idsen halde kalacak şekilde, (atası, ebeveyn nedenini anlamaya zor bariz bit edilir) Seçim listesinde olması gerekli değildir SELECTisterseniz sorguda ama güvenle kaldırabilirsiniz UPDATE.
  • (customer_id, object_id)Bir aday gibi görünüyor UNIQUEkısıtlaması. Verileriniz buna uygunsa, böyle bir kısıtlama ekleyin. Özyinelemeli CTE'de yapılan birleşimler benzersiz olmasaydı mantıklı olmazdı (bir düğüm başka türlü 2 ebeveyne sahip olabilir).
  • bu kısıtlamayı eklerseniz, bu (benzersiz) (customer_id, parent_id)bir FOREIGN KEYkısıtlamaya aday olur . En büyük olasılıkla yok değil açıklamanıza tarafından beri, yeni satırlar ekliyoruz olsa o FK kısıtlamayı eklemek istiyorum ve bazı satırlar henüz eklenmemiş diğerlerini başvurabilir.REFERENCES(customer_id, object_id)
  • Büyük bir tabloda yapılacaksa, sorgunun etkinliği konusunda kesinlikle sorunlar vardır. İlk çalıştırmada değil, neredeyse tüm tablo yine de güncellenecek. Ancak ikinci kez, yalnızca yeni satırların (ve 1. çalıştırma tarafından dokunulmamış olanların) güncelleme için dikkate alınmasını istersiniz. CTE olduğu gibi büyük bir sonuç oluşturmak zorunda kalacak. Nihai güncellemesinde 1 vadede güncellenen satırlar yeniden güncellenecektir olmayacağından emin yapacaktır ama CTE hala pahalı bir parçasıdır.
    AND o.generation = -1

Aşağıdakiler bu sorunları ele alma girişimidir: CTE'yi olabildiğince az sayıda satır olarak düşünün ve satırları tanımlamak (customer_id, obejct_id)yerine kullanın (id)(böylece idsorgudan tamamen kaldırılır. 1. güncelleme veya bir sonraki olarak kullanılabilir:

WITH RECURSIVE descendants 
  (customer_id, object_id, depth) 
 AS ( SELECT customer_id, object_id, 0
      FROM objects
      WHERE object_id = parent_id
        AND generation = -1

      UNION ALL

      SELECT o.customer_id, o.object_id, p.generation + 1
      FROM objects o
        JOIN objects p ON  p.customer_id = o.customer_id
                       AND p.object_id = o.parent_id
                       AND p.generation > -1
      WHERE o.generation = -1

      UNION ALL

      SELECT o.customer_id, o.object_id, d.depth + 1
      FROM objects o
      INNER JOIN descendants d ON  o.customer_id = d.customer_id
                               AND o.parent_id = d.object_id
      WHERE o.parent_id <> o.object_id
        AND o.generation = -1
    )
UPDATE objects o 
SET generation = d.depth 
FROM descendants d
WHERE o.customer_id = d.customer_id
  AND o.object_id = d.object_id
  AND o.generation = -1        -- this is not really needed

CTE'nin nasıl 3 bölüme sahip olduğuna dikkat edin. İlk ikisi kararlı kısımlardır. 1. bölüm, daha önce güncellenmemiş ve hala generation=-1yeni olan düğümlerin bulunması gereken kök düğümleri bulur . 2. bölüm generation=-1, daha önce güncellenmiş üst düğümlerin alt öğelerini (ile ) bulur .
3., özyinelemeli bölüm, ilk iki bölümün tüm torunlarını daha önce olduğu gibi bulur.

SQLfiddle-2 üzerinde test edildi


3

@ ypercube zaten geniş bir açıklama sağlıyor, bu yüzden eklemek zorunda olduğum kovalamaca keseceğim.

Bu parent_idyoksa, oluşturma sütununu -1 olarak ayarlanmış olarak bırakmalısınız.

Bu, yinelemeli uygulamak ağacın kalanı yani gerekiyordu varsayalım daima vardır generation = -1eksik düğüm sonra.

Ağaçtaki herhangi bir düğüm eksikse (henüz), bununla birlikte satırlar bulmamız gerekir generation = -1...
... kök düğümler
... veya bir ebeveynimiz var generation > -1.
Ve ağacı oradan hareket ettirin. Bu seçimin alt düğümleri de olmalıdır generation = -1.

generationKök düğümler için bir artırılmış üst öğeyi alın veya 0'a geri dönün:

WITH RECURSIVE tree AS (
   SELECT c.customer_id, c.object_id, COALESCE(p.generation + 1, 0) AS depth
   FROM   objects      c
   LEFT   JOIN objects p ON c.customer_id = p.customer_id
                        AND c.parent_id   = p.object_id
                        AND p.generation > -1
   WHERE  c.generation = -1
   AND   (c.parent_id = c.object_id OR p.generation > -1)
       -- root node ... or parent with generation > -1

   UNION ALL
   SELECT customer_id, c.object_id, p.depth + 1
   FROM   objects c
   JOIN   tree    p USING (customer_id)
   WHERE  c.parent_id  = p.object_id
   AND    c.parent_id <> c.object_id  -- exclude root nodes
   AND    c.generation = -1           -- logically redundant, but see below!
   )
UPDATE objects o 
SET    generation = t.depth
FROM   tree t
WHERE  o.customer_id = t.customer_id
AND    o.object_id   = t.object_id;

Özyinelemeli olmayan kısım SELECTbu şekilde tek , ancak mantıksal olarak @ ypercube'un iki sendikası'na eşdeğerdir SELECT. Hangisinin daha hızlı olduğundan emin değilsiniz, test etmeniz gerekecek.
Performans için çok daha önemli nokta:

Dizin!

Büyük bir tabloya tekrar tekrar bu şekilde satır eklerseniz, kısmi bir dizin ekleyin :

CREATE INDEX objects_your_name_idx ON objects (customer_id, parent_id, object_id)
WHERE  generation = -1;

Bu, performans için şu ana kadar tartışılan diğer tüm iyileştirmelerden daha fazlasını başaracaktır - büyük bir tabloya tekrarlanan küçük eklemeler için.

Ben sorgu planlayıcısı kısmi dizinin uygulanabilir olduğunu anlamak yardımcı olmak için (mantıksal olarak gereksiz olsa) CTE özyinelemeli bölümüne dizin koşulu ekledi.

Buna ek olarak, muhtemelen @ypercube üzerinde daha önce bahsi geçen UNIQUEkısıtlama da olmalıdır (object_id, customer_id). Veya, herhangi bir nedenden dolayı benzersizlik uygulayamıyorsanız (neden?) Bunun yerine düz bir dizin ekleyin. Dizin sütunlarının sırası önemlidir, btw:


1
Siz ve @ ypercube tarafından önerilen dizinleri ve kısıtlamaları ekleyeceğim. Verilere bakarak, (bazen parent_id henüz ayarlanmadığı için yabancı anahtar dışında) olmaları için herhangi bir neden görmüyorum. Ayrıca nesil sütun null ve varsayılan olarak -1 yerine NULL olarak ayarlayacağım. Sonra çok fazla "-1" filtrem olmayacak ve kısmi dizinler NEREDEN nesil NULL, vb. Olabilir.
Diggity

@Diggity: Gerisini uyarlarsanız NULL gayet iyi çalışmalıdır, evet.
Erwin Brandstetter

@Erwin güzel. Aslında seninle benzer düşündüm. Bir endeks ON objects (customer_id, parent_id, object_id) WHERE generation = -1;ve belki de başka ON objects (customer_id, object_id) WHERE generation > -1;. Güncellemenin ayrıca güncellenen tüm satırları bir dizinden diğerine "geçirmesi" gerekir, bu nedenle bunun UPDATE'in ilk çalıştırılması için iyi bir fikir olup olmadığından emin değilsiniz.
ypercubeᵀᴹ

Özyinelemeli sorgular için dizin oluşturmak gerçekten zor olabilir.
ypercubeᵀᴹ
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.