ความลึกของลูกหลานแบบเรียกซ้ำ PostgreSQL


15

ฉันต้องคำนวณความลึกของลูกหลานจากบรรพบุรุษของมัน เมื่อมีการบันทึกobject_id = parent_id = ancestor_idจะถือว่าเป็นโหนดรูท (บรรพบุรุษ) ผมได้พยายามที่จะได้รับWITH RECURSIVEการสอบถามการทำงานกับ PostgreSQL 9.4

ฉันไม่ได้ควบคุมข้อมูลหรือคอลัมน์ data และ schema ของตารางมาจากแหล่งภายนอก ตารางจะเติบโตอย่างต่อเนื่อง ตอนนี้บันทึกประมาณ 30k ต่อวัน โหนดใด ๆ ในทรีสามารถหายไปและพวกเขาจะถูกดึงจากแหล่งภายนอกในบางจุด พวกเขามักจะถูกดึงcreated_at DESCตามลำดับ แต่ข้อมูลจะถูกดึงด้วยงานพื้นหลังแบบอะซิงโครนัส

เริ่มแรกเรามีวิธีแก้ไขปัญหาของรหัส แต่ตอนนี้มี 5M + แถวใช้เวลาเกือบ 30 นาทีจึงจะเสร็จสมบูรณ์

ตัวอย่างคำจำกัดความของตารางและข้อมูลการทดสอบ:

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

โปรดทราบว่าobject_idไม่ซ้ำกัน แต่การรวมกัน(customer_id, object_id)จะไม่ซ้ำกัน
เรียกใช้คิวรีแบบนี้:

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;

ฉันต้องการให้generationคอลัมน์ถูกตั้งค่าเป็นความลึกที่คำนวณ เมื่อบันทึกใหม่จะถูกเพิ่มคอลัมน์การสร้างจะถูกตั้งค่าเป็น -1 มีบางกรณีที่parent_idอาจยังไม่ถูกดึงออกมา หากparent_idไม่มีอยู่ควรปล่อยให้คอลัมน์สร้างตั้งค่าเป็น -1

ข้อมูลสุดท้ายควรมีลักษณะดังนี้:

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

ผลลัพธ์ของแบบสอบถามควรจะปรับปรุงคอลัมน์การสร้างความลึกที่ถูกต้อง

ผมเริ่มทำงานจากคำตอบของคำถามที่เกี่ยวข้องเกี่ยวกับเรื่องนี้ดังนั้น


ดังนั้นคุณต้องการupdateตารางด้วยผลลัพธ์ของ CTE แบบเรียกซ้ำได้หรือไม่
a_horse_with_no_name

ใช่ฉันต้องการให้คอลัมน์ Generation เป็น UPDATE ตามความลึกของมัน หากไม่มีพาเรนต์ (object.parent_id ไม่ตรงกับ object.object_id ใด ๆ ) การสร้างจะยังคงเป็น -1

ดังนั้นการancestor_idตั้งค่าแล้วดังนั้นคุณจะต้องกำหนดรุ่นจาก CTE.depth?

ใช่ object_id, parent_id และ ancestor_id ได้รับการตั้งค่าจากข้อมูลที่เราได้รับจาก API แล้ว ฉันต้องการตั้งค่าคอลัมน์สร้างเป็นความลึก อีกหมายเหตุหนึ่งคือ object_id ไม่ซ้ำกันเนื่องจาก customer_id 1 อาจมี object_id 1 และ customer_id 2 อาจมี object_id 1 รหัสหลักบนตารางไม่ซ้ำกัน

นี่เป็นการอัปเดตครั้งเดียวหรือคุณกำลังเพิ่มลงในตารางที่กำลังเติบโตอยู่ใช่ไหม ดูเหมือนว่ากรณีหลัง ทำให้ความแตกต่างใหญ่ และสามารถรูตโหนดหายไป (ยัง) หรือโหนดใด ๆ ในทรีได้หรือไม่
Erwin Brandstetter

คำตอบ:


14

แบบสอบถามที่คุณมีนั้นถูกต้องโดยทั่วไป ข้อผิดพลาดเดียวคือในส่วนที่สอง (เรียกซ้ำ) ของ CTE ที่คุณมี:

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

ควรเป็นวิธีอื่น ๆ :

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

คุณต้องการเข้าร่วมวัตถุกับผู้ปกครองของพวกเขา (ที่ได้พบแล้ว)

ดังนั้นเคียวรีที่คำนวณความลึกสามารถเขียนได้ (ไม่มีสิ่งใดเปลี่ยนแปลงการจัดรูปแบบเท่านั้น):

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

สำหรับการอัปเดตคุณเพียงแค่แทนที่ตัวสุดท้ายSELECTด้วยการUPDATEเข้าร่วมผลลัพธ์ของ cte กลับไปที่ตาราง:

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

ความคิดเห็นเพิ่มเติม:

  • ancestor_idและparent_idไม่จำเป็นที่จะอยู่ในรายการเลือก (บรรพบุรุษเห็นได้ชัดแม่บิตหากินที่จะคิดออกว่าทำไม) เพื่อให้คุณสามารถเก็บไว้ในSELECTแบบสอบถามถ้าคุณต้องการ UPDATEแต่คุณสามารถลบออกจาก
  • (customer_id, object_id)ดูเหมือนว่าผู้สมัครรับเลือกตั้งเป็นUNIQUEข้อ จำกัด หากข้อมูลของคุณเป็นไปตามนี้ให้เพิ่มข้อ จำกัด ดังกล่าว การรวมที่ดำเนินการใน CTE แบบเรียกซ้ำจะไม่สมเหตุสมผลถ้ามันไม่ซ้ำกัน (โหนดอาจมี 2 พาเรนต์เป็นอย่างอื่น)
  • ถ้าคุณเพิ่มข้อ จำกัด นั้น(customer_id, parent_id)จะลงสมัครรับเลือกตั้งเป็นFOREIGN KEYข้อ จำกัด ที่(ที่ไม่ซ้ำกัน)REFERENCES (customer_id, object_id)คุณส่วนใหญ่อาจทำไม่ได้ต้องการเพิ่มข้อ จำกัด FK นั้นเนื่องจากตามคำอธิบายของคุณคุณกำลังเพิ่มแถวใหม่และบางแถวสามารถอ้างอิงผู้อื่นที่ยังไม่ได้เพิ่ม
  • แน่นอนว่ามีปัญหาเกี่ยวกับประสิทธิภาพของการค้นหาหากจะต้องดำเนินการในตารางขนาดใหญ่ ไม่ใช่ในการเรียกใช้ครั้งแรกเนื่องจากเกือบทั้งตารางจะได้รับการอัปเดตต่อไป แต่ในครั้งที่สองคุณจะต้องการเฉพาะแถวใหม่ (และแถวที่ไม่ได้ถูกใช้ในการวิ่งครั้งที่ 1) เพื่อรับการพิจารณา CTE ตามที่มันจะต้องสร้างผลลัพธ์ที่ยิ่งใหญ่ ในการปรับปรุงขั้นสุดท้ายจะให้แน่ใจว่าแถวที่ได้รับการปรับปรุงในระยะที่ 1 จะไม่ได้รับการปรับปรุงอีกครั้ง แต่ CTE ยังคงเป็นส่วนที่แพง
    AND o.generation = -1

ต่อไปนี้คือความพยายามแก้ไขปัญหาเหล่านี้: ปรับปรุง CTE เพื่อพิจารณาแถวให้น้อยที่สุดเท่าที่จะเป็นไปได้และใช้(customer_id, obejct_id)แทน(id)การระบุแถว (ดังนั้นidจะถูกลบออกจากแบบสอบถามอย่างสมบูรณ์ซึ่งสามารถใช้เป็นการปรับปรุงครั้งแรกหรือครั้งต่อไป:

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 มี 3 ส่วนอย่างไร สองชิ้นแรกเป็นชิ้นส่วนที่มั่นคง ส่วนที่ 1 ค้นหารูตโหนดที่ไม่ได้รับการอัพเดตมาก่อนและยังคงมีgeneration=-1ดังนั้นจึงต้องเพิ่มโหนดใหม่ ส่วนที่ 2 ค้นหาส่วนย่อย (พร้อมgeneration=-1) ของโหนดหลักที่ได้รับการอัพเดตก่อนหน้านี้
ส่วนที่ 3 วนซ้ำค้นหาลูกหลานทั้งหมดของสองส่วนแรกเหมือนเมื่อก่อน

ทดสอบบนSQLfiddle-2


3

@ypercubeมีคำอธิบายที่เพียงพออยู่แล้วดังนั้นฉันจะตัดการไล่ล่าในสิ่งที่ฉันต้องเพิ่ม

หากparent_idไม่มีอยู่ควรปล่อยให้คอลัมน์สร้างตั้งค่าเป็น -1

ฉันคิดว่ามันควรจะใช้แบบวนซ้ำนั่นคือส่วนที่เหลือของต้นไม้จะมีgeneration = -1หลังจากโหนดที่หายไป

ถ้าโหนดใด ๆ ในโครงสร้างที่สามารถหายไป (ยัง) เราต้องไปหาแถวที่มีgeneration = -1ว่า ...
... เป็นโหนดราก
... generation > -1หรือมีบิดามารดาด้วย
และสำรวจต้นไม้จากที่นั่น โหนดย่อยของการเลือกนี้ต้องมีgeneration = -1เช่นกัน

รับพาเรนต์ที่generationเพิ่มขึ้นทีละหนึ่งหรือถอยกลับเป็น 0 สำหรับโหนดรูท:

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;

ส่วนที่ไม่ใช่ recursive เป็นหนึ่งในSELECTวิธีนี้ แต่เหตุผลเทียบเท่ากับ @ ypercube ของ SELECTunion'ed ไม่แน่ใจว่าเร็วกว่าคุณจะต้องทดสอบ
จุดสำคัญสำหรับประสิทธิภาพคือ:

ดัชนี!

หากคุณเพิ่มแถวไปยังตารางขนาดใหญ่ด้วยวิธีนี้ให้เพิ่มดัชนีบางส่วน :

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

สิ่งนี้จะบรรลุผลการดำเนินงานมากกว่าการปรับปรุงอื่น ๆ ทั้งหมดที่กล่าวถึงในตอนนี้ - สำหรับการเพิ่มเติมเล็กน้อยซ้ำ ๆ ในตารางขนาดใหญ่

ฉันเพิ่มเงื่อนไขดัชนีไปยังส่วนที่เรียกซ้ำของ CTE (แม้ว่าจะมีเหตุผลซ้ำซ้อน) เพื่อช่วยให้ผู้วางแผนแบบสอบถามเข้าใจว่าดัชนีบางส่วนนั้นมีผลบังคับใช้

นอกจากนี้คุณควรมีUNIQUEข้อ จำกัด ใน(object_id, customer_id)@ypercube ที่กล่าวถึงไปแล้ว หรือถ้าคุณไม่สามารถกำหนดความเป็นเอกลักษณ์ด้วยเหตุผลบางอย่าง (ทำไม?) ให้เพิ่มดัชนีธรรมดาแทน ลำดับของคอลัมน์ดัชนีมีความสำคัญ btw:


1
ฉันจะเพิ่มดัชนีและข้อ จำกัด ที่คุณและ @ypercube แนะนำ เมื่อดูข้อมูลฉันไม่เห็นเหตุผลที่พวกเขาไม่สามารถเกิดขึ้นได้ (นอกเหนือจากรหัสต่างประเทศเนื่องจากบางครั้ง parent_id ยังไม่ได้ตั้งค่า) ฉันจะตั้งคอลัมน์สร้างเป็นโมฆะและตั้งค่าเริ่มต้นเป็น NULL แทน -1 จากนั้นฉันจะไม่มีตัวกรอง "-1" จำนวนมากและดัชนีบางส่วนสามารถเป็นได้ว่ารุ่นใดเป็นโมฆะและอื่น ๆ
Diggity

@Diggity: NULL ควรทำงานได้ดีถ้าคุณปรับใช้ส่วนที่เหลือใช่
Erwin Brandstetter

@ เออร์วิน ตอนแรกฉันคิดว่าคล้ายกันกับคุณ ดัชนีและบางทีอาจจะอีกON objects (customer_id, parent_id, object_id) WHERE generation = -1; ON objects (customer_id, object_id) WHERE generation > -1;การอัปเดตจะต้อง "สลับ" แถวที่อัปเดตทั้งหมดจากดัชนีหนึ่งไปยังอีกดัชนีหนึ่งดังนั้นไม่แน่ใจว่านี่เป็นแนวคิดที่ดีสำหรับการเริ่มต้นใช้งาน UPDATE หรือไม่
ypercubeᵀᴹ

การจัดทำดัชนีสำหรับข้อความค้นหาแบบเรียกซ้ำอาจเป็นเรื่องยาก
ypercubeᵀᴹ
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.