PostgreSQL पुनरावर्ती वंश गहराई


15

मुझे इसके पूर्वज से एक वंश की गहराई की गणना करने की आवश्यकता है। जब कोई रिकॉर्ड होता है object_id = parent_id = ancestor_id, तो इसे रूट नोड (पूर्वज) माना जाता है। मैं WITH RECURSIVEPostgreSQL 9.4 के साथ एक क्वेरी चलाने की कोशिश कर रहा हूं ।

मैं डेटा या स्तंभों को नियंत्रित नहीं करता हूं। डेटा और टेबल स्कीमा एक बाहरी स्रोत से आता है। टेबल लगातार बढ़ रही है । अभी लगभग 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

हां, मैं चाहता हूं कि जेनरेशन कॉलम को UPDATE'd किया जाए जो इसकी गहराई है। यदि कोई माता-पिता (ऑब्जेक्ट्स नहीं है ।parent_id किसी भी ऑब्जेक्ट से मेल नहीं खाता है ।object_id) पीढ़ी -1 के रूप में बनी रहेगी।

तो ancestor_idपहले से ही सेट है, इसलिए आपको केवल CTE.depth से पीढ़ी आवंटित करने की आवश्यकता है?

हां, ऑब्जेक्ट_आईडी, पेरेंट_ड, और पूर्वज_आईडी पहले से ही हमारे द्वारा प्राप्त डेटा से सेट है। मैं जो कुछ भी गहराई है, पीढ़ी कॉलम सेट करना चाहूंगा। एक अन्य नोट, object_id अद्वितीय नहीं है, क्योंकि customer_id 1 में object_id 1 हो सकता है, और customer_id 2 में object_id 1 हो सकता है। तालिका में प्राथमिक आईडी अद्वितीय है।

क्या यह एक बार का अपडेट है या आप लगातार बढ़ती हुई तालिका में जोड़ रहे हैं? बाद वाले मामले की तरह लगता है। एक बनाता है बड़ा अंतर है। और क्या केवल पेड़ में जड़ (अभी तक) या कोई भी नोड गायब हो सकता है?
एरविन ब्रान्डसेट्टर

जवाबों:


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, सीटीई के परिणाम को जोड़कर, तालिका में वापस आते हैं:

-- 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 रन से नहीं छुआ गया था) को अपडेट के लिए माना जाएगा। सीटीई के रूप में इसे एक बड़ा परिणाम तैयार करना होगा। अंतिम अद्यतन में यह सुनिश्चित करें कि पंक्तियों को 1 समय में अपडेट किए गए थे फिर से अपडेट नहीं किया जाएगा कर देगा लेकिन CTE अभी भी एक महंगी हिस्सा है।
    AND o.generation = -1

निम्नलिखित इन मुद्दों को संबोधित करने का एक प्रयास है: CTE को यथासंभव कुछ पंक्तियों पर विचार करने के लिए सुधारें और पंक्तियों की पहचान करने के (customer_id, obejct_id)बजाय उपयोग करें (id)(इसलिए idइसे क्वेरी से पूरी तरह से हटा दिया जाता है। इसे 1 अद्यतन या बाद में उपयोग किया जा सकता है:

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

ध्यान दें कि सीटीई के 3 भाग कैसे होते हैं। पहले दो स्थिर भाग हैं। पहले भाग में रूट नोड्स हैं जो पहले अपडेट नहीं किए गए हैं और अभी भी हैं generation=-1इसलिए उन्हें नए जोड़े गए नोड्स होने चाहिए। दूसरा भाग उन बच्चों ( 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;

गैर-पुनरावर्ती भाग SELECTइस तरह से एकल है , लेकिन तार्किक रूप से @ ypercube के दो संघवाद के बराबर है SELECT। निश्चित नहीं है कि कौन सा तेज़ है, आपको परीक्षण करना होगा।
प्रदर्शन के लिए बहुत अधिक महत्वपूर्ण बिंदु है:

सूचकांक!

यदि आप बार-बार एक बड़ी तालिका में पंक्तियों को जोड़ते हैं, तो एक आंशिक सूचकांक जोड़ें :

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

यह अब तक की गई अन्य सभी सुधारों की तुलना में प्रदर्शन के लिए अधिक प्राप्त करेगा - एक बड़ी तालिका में बार-बार छोटे परिवर्धन के लिए।

मैंने क्वेरी योजनाकार को यह समझने में मदद करने के लिए कि CTE के पुनरावर्ती भाग (भले ही तार्किक रूप से निरर्थक) में सूचकांक स्थिति को जोड़ा, यह समझने में मदद करता है कि आंशिक सूचकांक लागू है।

इसके अलावा आपको शायद उस @ypercube पर पहले से उल्लेखित UNIQUEबाधा भी होनी चाहिए (object_id, customer_id)। या, यदि आप किसी कारण के लिए विशिष्टता को लागू नहीं कर सकते हैं (क्यों?) इसके बजाय एक सादा सूचकांक जोड़ें। सूचकांक कॉलम का क्रम मायने रखता है, btw:


1
मैं आपके और @ypercube द्वारा सुझाए गए अनुक्रमित और बाधाओं को जोड़ूंगा। डेटा के माध्यम से देखते हुए, मुझे ऐसा कोई कारण नहीं दिखता है कि वे नहीं हो सकते (विदेशी कुंजी के अलावा कभी-कभी पेरेंट_एड अभी सेट नहीं हुआ है)। मैं जनरेशन कॉलम को अशक्त और डिफ़ॉल्ट सेट को -1 के बजाय NULL के रूप में सेट करूंगा। फिर मेरे पास बहुत सारे "-1" फिल्टर नहीं होंगे और आंशिक अनुक्रमित हो सकते हैं, जहां जनरेशन हो सकती है NULL, आदि
Diggity

@ डाइट: अगर आप बाकी चीजों को अपनाते हैं तो NULL को ठीक काम करना चाहिए।
एरविन ब्रान्डेसटेटर

@ इरविन अच्छा। मैंने मूल रूप से आपके जैसा ही सोचा था। एक इंडेक्स ON objects (customer_id, parent_id, object_id) WHERE generation = -1;और दूसरा शायद ON objects (customer_id, object_id) WHERE generation > -1;। अद्यतन को एक सूचकांक से दूसरे में सभी अद्यतन पंक्तियों को "स्विच" करना होगा, इसलिए यह सुनिश्चित न करें कि क्या यह UPDATE के प्रारंभिक रन के लिए एक अच्छा विचार है।
ypercube y

पुनरावर्ती प्रश्नों के लिए अनुक्रमण वास्तव में कठिन हो सकता है।
ypercube y
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.