अलग-अलग सीमाओं को सबसे बड़ी संभव सन्निहित सीमाओं में संयोजित करना


20

मैं कई तिथि सीमाओं को संयोजित करने का प्रयास कर रहा हूं (मेरा भार अधिकतम 500, अधिकांश मामलों 10) के बारे में है जो सबसे बड़ी संभव सन्निहित तिथि सीमाओं में ओवरलैप कर सकते हैं या नहीं कर सकते हैं। उदाहरण के लिए:

डेटा:

CREATE TABLE test (
  id SERIAL PRIMARY KEY NOT NULL,
  range DATERANGE
);

INSERT INTO test (range) VALUES 
  (DATERANGE('2015-01-01', '2015-01-05')),
  (DATERANGE('2015-01-01', '2015-01-03')),
  (DATERANGE('2015-01-03', '2015-01-06')),
  (DATERANGE('2015-01-07', '2015-01-09')),
  (DATERANGE('2015-01-08', '2015-01-09')),
  (DATERANGE('2015-01-12', NULL)),
  (DATERANGE('2015-01-10', '2015-01-12')),
  (DATERANGE('2015-01-10', '2015-01-12'));

तालिका इस प्रकार है:

 id |          range
----+-------------------------
  1 | [2015-01-01,2015-01-05)
  2 | [2015-01-01,2015-01-03)
  3 | [2015-01-03,2015-01-06)
  4 | [2015-01-07,2015-01-09)
  5 | [2015-01-08,2015-01-09)
  6 | [2015-01-12,)
  7 | [2015-01-10,2015-01-12)
  8 | [2015-01-10,2015-01-12)
(8 rows)

वांछित परिणाम:

         combined
--------------------------
 [2015-01-01, 2015-01-06)
 [2015-01-07, 2015-01-09)
 [2015-01-10, )

दृश्य प्रतिनिधित्व:

1 | =====
2 | ===
3 |    ===
4 |        ==
5 |         =
6 |             =============>
7 |           ==
8 |           ==
--+---------------------------
  | ====== == ===============>

जवाबों:


22

मान्यताओं / स्पष्टीकरण

  1. infinityऊपरी सीमा को खोलने और खोलने के बीच अंतर करने की आवश्यकता नहीं है upper(range) IS NULL। (आप इसे वैसे भी कर सकते हैं, लेकिन यह इस तरह से सरल है।)

  2. चूंकि dateएक असतत प्रकार है, सभी श्रेणियों में डिफ़ॉल्ट [)सीमाएं हैं। प्रति प्रलेखन:

    बिल्ट-इन रेंज प्रकार int4range, int8rangeऔर daterangeसभी एक कैनोनिकल फॉर्म का उपयोग करते हैं जिसमें निचला बाउंड शामिल होता है और ऊपरी बाउंड को बाहर करता है; वह यह है कि [)

    अन्य प्रकारों के लिए (जैसे tsrange!) यदि संभव हो तो मैं इसे लागू करूंगा:

शुद्ध एसक्यूएल के साथ समाधान

स्पष्टता के लिए सीटीई के साथ:

WITH a AS (
   SELECT range
        , COALESCE(lower(range),'-infinity') AS startdate
        , max(COALESCE(upper(range), 'infinity')) OVER (ORDER BY range) AS enddate
   FROM   test
   )
, b AS (
   SELECT *, lag(enddate) OVER (ORDER BY range) < startdate OR NULL AS step
   FROM   a
   )
, c AS (
   SELECT *, count(step) OVER (ORDER BY range) AS grp
   FROM   b
   )
SELECT daterange(min(startdate), max(enddate)) AS range
FROM   c
GROUP  BY grp
ORDER  BY 1;

या , उपश्रेणियों के साथ ही, तेज लेकिन कम आसान भी पढ़ें:

SELECT daterange(min(startdate), max(enddate)) AS range
FROM  (
   SELECT *, count(step) OVER (ORDER BY range) AS grp
   FROM  (
      SELECT *, lag(enddate) OVER (ORDER BY range) < startdate OR NULL AS step
      FROM  (
         SELECT range
              , COALESCE(lower(range),'-infinity') AS startdate
              , max(COALESCE(upper(range), 'infinity')) OVER (ORDER BY range) AS enddate
         FROM   test
         ) a
      ) b
   ) c
GROUP  BY grp
ORDER  BY 1;

या एक कम उप-स्तर के स्तर के साथ, लेकिन फ़्लिपिंग क्रम:

SELECT daterange(min(COALESCE(lower(range), '-infinity')), max(enddate)) AS range
FROM  (
   SELECT *, count(nextstart > enddate OR NULL) OVER (ORDER BY range DESC NULLS LAST) AS grp
   FROM  (
      SELECT range
           , max(COALESCE(upper(range), 'infinity')) OVER (ORDER BY range) AS enddate
           , lead(lower(range)) OVER (ORDER BY range) As nextstart
      FROM   test
      ) a
   ) b
GROUP  BY grp
ORDER  BY 1;
  • पूरी तरह से उल्टे क्रम क्रम को प्राप्त करने के लिए ORDER BY range DESC NULLS LAST(के साथ NULLS LAST) दूसरे चरण में विंडो को सॉर्ट करें। यह सस्ता होना चाहिए (उत्पादन करने में आसान, सुझावित सूचकांक के क्रमबद्ध रूप से पूरी तरह से मेल खाता है) और कोने के मामलों के लिए सटीक हैrank IS NULL

समझाना

a: द्वारा आदेश देते समय range, एक विंडो फ़ंक्शन के साथ ऊपरी बाउंड ( ) के चल रहे अधिकतम की गणना करें enddate
NULL बाउंड्स (अनबाउंड) को +/- के साथ बदलें ( infinityकेवल विशेष NULL केस को सरल बनाने के लिए)।

b: उसी तरह के क्रम में, यदि पिछला enddateपहले से है , तो startdateहमारे पास एक अंतर है और एक नई सीमा शुरू करें ( step)।
याद रखें, ऊपरी सीमा हमेशा बाहर रखी जाती है।

c: grpअन्य विंडो फ़ंक्शन के साथ चरणों की गणना करके फॉर्म समूह ( )।

बाहरी SELECTनिर्माण में प्रत्येक समूह में निचले से ऊपरी हिस्से तक होता है। देखा।
अधिक विवरण के साथ एसओ पर बारीकी से संबंधित जवाब:

Plpgsql के साथ प्रक्रियात्मक समाधान

किसी भी तालिका / स्तंभ नाम के लिए काम करता है, लेकिन केवल प्रकार के लिए daterange
छोरों के साथ प्रक्रियात्मक समाधान आमतौर पर धीमे होते हैं, लेकिन इस विशेष मामले में मुझे उम्मीद है कि फ़ंक्शन काफी तेजी से होगा क्योंकि इसे केवल एकल अनुक्रमिक स्कैन की आवश्यकता है :

CREATE OR REPLACE FUNCTION f_range_agg(_tbl text, _col text)
  RETURNS SETOF daterange AS
$func$
DECLARE
   _lower     date;
   _upper     date;
   _enddate   date;
   _startdate date;
BEGIN
   FOR _lower, _upper IN EXECUTE
      format($$SELECT COALESCE(lower(t.%2$I),'-infinity')  -- replace NULL with ...
                    , COALESCE(upper(t.%2$I), 'infinity')  -- ... +/- infinity
               FROM   %1$I t
               ORDER  BY t.%2$I$$
            , _tbl, _col)
   LOOP
      IF _lower > _enddate THEN     -- return previous range
         RETURN NEXT daterange(_startdate, _enddate);
         SELECT _lower, _upper  INTO _startdate, _enddate;

      ELSIF _upper > _enddate THEN  -- expand range
         _enddate := _upper;

      -- do nothing if _upper <= _enddate (range already included) ...

      ELSIF _enddate IS NULL THEN   -- init 1st round
         SELECT _lower, _upper  INTO _startdate, _enddate;
      END IF;
   END LOOP;

   IF FOUND THEN                    -- return last row
      RETURN NEXT daterange(_startdate, _enddate);
   END IF;
END
$func$  LANGUAGE plpgsql;

कॉल करें:

SELECT * FROM f_range_agg('test', 'range');  -- table and column name

तर्क SQL समाधान के समान है, लेकिन हम एक पास से कर सकते हैं।

एसक्यूएल फिडल।

सम्बंधित:

डायनामिक SQL में उपयोगकर्ता इनपुट को संभालने के लिए सामान्य ड्रिल:

सूची

इनमें से प्रत्येक समाधान के लिए एक सादा (डिफ़ॉल्ट) बीटीआरई सूचकांक rangeबड़े तालिकाओं में प्रदर्शन के लिए महत्वपूर्ण होगा:

CREATE INDEX foo on test (range);

Btree इंडेक्स सीमित प्रकारों के लिए सीमित उपयोग का है , लेकिन हम पूर्व-सॉर्ट किए गए डेटा और शायद एक इंडेक्स-केवल स्कैन भी प्राप्त कर सकते हैं।


@Villiers: मुझे बहुत दिलचस्पी होगी कि इनमें से प्रत्येक समाधान आपके डेटा के साथ कैसा प्रदर्शन करता है। हो सकता है कि आप परीक्षा परिणाम और अपनी टेबल डिजाइन और कार्डिनैलिटी पर कुछ जानकारी के साथ एक और उत्तर पोस्ट कर सकते हैं? सबसे EXPLAIN ( ANALYZE, TIMING OFF)अच्छा और पांच के साथ तुलना करें।
इरविन ब्रान्डेसटेटर

इस तरह की समस्याओं की कुंजी अंतराल SQL फ़ंक्शन (लीड का उपयोग भी किया जा सकता है) जो क्रमबद्ध पंक्तियों के मूल्यों की तुलना करते हैं। इसने स्वयं के जुड़ने की आवश्यकता को समाप्त कर दिया जिसका उपयोग एक सीमा में विलीन अतिव्याप्त श्रेणियों में भी किया जा सकता है। सीमा के बजाय, दो कॉलम some_star को शामिल करने वाली कोई समस्या, some_end इस रणनीति का उपयोग कर सकती है।
केमिन झोउ

@ErwinBrandstetter अरे, मैं इस क्वेरी (CTE के साथ एक) को समझने की कोशिश कर रहा हूं, लेकिन मैं यह पता नहीं लगा सकता कि (CTE A) max(COALESCE(upper(range), 'infinity')) OVER (ORDER BY range) AS enddateक्या है? क्या यह सिर्फ नहीं हो सकता COALESCE(upper(range), 'infinity') as enddate? AFAIK max() + over (order by range)बस upper(range)यहीं लौट आएगी ।
user606521

1
@ user606521: यदि आप देखते हैं कि क्या मामला है अगर ऊपरी सीमा निरंतर बढ़ जाती है जब रेंज द्वारा क्रमबद्ध किया जाता है - जिसे कुछ डेटा वितरणों के लिए गारंटी दी जा सकती है और फिर आप सुझाव के अनुसार सरल कर सकते हैं। उदाहरण: निश्चित लंबाई पर्वतमाला। लेकिन मनमाने ढंग से लंबाई की सीमाओं के लिए अगली रेंज में एक बड़ी निचली सीमा हो सकती है, लेकिन फिर भी एक ऊपरी ऊपरी सीमा होती है। इसलिए हमें अब तक की सबसे बड़ी ऊपरी सीमा की आवश्यकता है।
एरविन ब्रान्डस्टेट्टर

6

मैं इसके साथ आया हूँ:

DO $$                                                                             
DECLARE 
    i date;
    a daterange := 'empty';
    day_as_range daterange;
    extreme_value date := '2100-12-31';
BEGIN
    FOR i IN 
        SELECT DISTINCT 
             generate_series(
                 lower(range), 
                 COALESCE(upper(range) - interval '1 day', extreme_value), 
                 interval '1 day'
             )::date
        FROM rangetest 
        ORDER BY 1
    LOOP
        day_as_range := daterange(i, i, '[]');
        BEGIN
            IF isempty(a)
            THEN a := day_as_range;
            ELSE a = a + day_as_range;
            END IF;
        EXCEPTION WHEN data_exception THEN
            RAISE INFO '%', a;
            a = day_as_range;
        END;
    END LOOP;

    IF upper(a) = extreme_value + interval '1 day'
    THEN a := daterange(lower(a), NULL);
    END IF;

    RAISE INFO '%', a;
END;
$$;

अभी भी थोड़ा सा सम्मान चाहिए, लेकिन विचार निम्नलिखित है:

  1. अलग-अलग तिथियों के लिए सीमाएँ विस्फोट
  2. ऐसा करने से, अनंत ऊपरी सीमा को कुछ चरम मान से प्रतिस्थापित करें
  3. (1) से ऑर्डर करने के आधार पर, श्रेणियों का निर्माण शुरू करें
  4. जब संघ ( +) विफल हो जाता है, तो पहले से ही बनाई गई सीमा लौटाएं और पुन: निर्माण करें
  5. अंत में, बाकी को वापस लौटाएं - यदि पूर्वनिर्धारित चरम मान तक पहुँच गया है, तो इसे अपर अपर बाउंड प्राप्त करने के लिए NULL से बदलें

यह मुझे generate_series()हर पंक्ति के लिए चलाने के लिए महंगा के रूप में
हड़ताली करता है

@ErwinBrandstetter हाँ, यह एक मुद्दा है जिसे मैं परीक्षण करना चाहता था (मेरी पहली चरम 9999-12-31 के बाद :)। उसी समय, मैं सोच रहा हूं कि मेरे जवाब में आपकी तुलना में अधिक उत्थान क्यों है। यह संभवतः समझना आसान है ... इसलिए, भविष्य के मतदाता: इरविन का जवाब मेरे लिए बेहतर है! वहां वोट दें!
dezso

3

कुछ साल पहले मैंने टेराडाटा सिस्टम पर ओवरलैपिंग पीरियड्स को मर्ज करने के लिए अलग-अलग सॉल्यूशंस (@ErwinBrandstetter के कुछ समान लोगों के बीच) का परीक्षण किया था और मैंने पाया कि सबसे कुशल एक (एनालिटिकल फंक्शंस का उपयोग करके, टेराडाटा के नए संस्करण में निर्मित कार्यों के लिए है) वह कार्य)।

  1. आरंभ तिथि तक पंक्तियों को क्रमबद्ध करें
  2. पिछली सभी पंक्तियों की अधिकतम समाप्ति तिथि ज्ञात करें: maxEnddate
  3. यदि यह तिथि वर्तमान प्रारंभ तिथि से कम है, तो आपको एक अंतर मिला है। केवल उन पंक्तियों को पार्टीशन के भीतर पहली पंक्ति में रखें (जो कि NULL द्वारा इंगित की गई है) और अन्य सभी पंक्तियों को फ़िल्टर करें। अब आपको प्रत्येक सीमा के लिए आरंभ तिथि और पिछली सीमा की अंतिम तिथि मिल जाएगी।
  4. तब आप बस अगली पंक्ति का maxEnddateउपयोग LEADकर रहे हैं और आप लगभग पूरा कर चुके हैं। केवल अंतिम पंक्ति के लिए LEADएक रिटर्न NULL, इस को हल करने के लिए चरण 2 में विभाजन की सभी पंक्तियों की अधिकतम समाप्ति तिथि की गणना करें और COALESCEइसे।

यह तेज क्यों था? वास्तविक डेटा चरण के आधार पर # 2 पंक्तियों की संख्या को बहुत कम कर सकता है, इसलिए अगले चरण को केवल एक छोटे से उपसमुच्चय पर संचालित करने की आवश्यकता है, इसके अलावा यह एकत्रीकरण को हटा देता है।

बेला

SELECT
   daterange(startdate
            ,COALESCE(LEAD(maxPrevEnddate) -- next row's end date
                      OVER (ORDER BY startdate) 
                     ,maxEnddate)          -- or maximum end date
            ) AS range

FROM
 (
   SELECT
      range
     ,COALESCE(LOWER(range),'-infinity') AS startdate

   -- find the maximum end date of all previous rows
   -- i.e. the END of the previous range
     ,MAX(COALESCE(UPPER(range), 'infinity'))
      OVER (ORDER BY range
            ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) AS maxPrevEnddate

   -- maximum end date of this partition
   -- only needed for the last range
     ,MAX(COALESCE(UPPER(range), 'infinity'))
      OVER () AS maxEnddate
   FROM test
 ) AS dt
WHERE maxPrevEnddate < startdate -- keep the rows where a range start
   OR maxPrevEnddate IS NULL     -- and keep the first row
ORDER BY 1;  

यह टेराडाटा पर सबसे तेज़ था, मुझे नहीं पता कि क्या यह PostgreSQL के लिए समान है, कुछ वास्तविक प्रदर्शन संख्याएं प्राप्त करना अच्छा होगा।


क्या केवल रेंज के शुरू होने से ऑर्डर करना पर्याप्त है? क्या यह काम करता है यदि आपके पास एक ही शुरुआत के साथ तीन रेंज हैं लेकिन अलग-अलग अंत हैं?
सलमान ए

1
यह केवल प्रारंभ तिथि के साथ काम करता है, अंतिम तिथि को अवरोही क्रम में जोड़ने की कोई आवश्यकता नहीं है (आप केवल अंतराल के लिए जांच करते हैं, इसलिए जो भी किसी तिथि के लिए पहली पंक्ति है वह मेल खाएगा)
dnoeth

-1

मनोरंजन के लिए, मैंने इसे एक शॉट दिया। मैंने ऐसा करने के लिए सबसे तेज और सबसे साफ तरीका पाया । पहले हम एक फ़ंक्शन को परिभाषित करते हैं जो एक ओवरलैप होता है या यदि दो इनपुट आसन्न हैं, अगर कोई ओवरलैप या आसन्न नहीं है, तो हम बस पहले ड्रैटर को वापस करते हैं। संकेत +श्रेणियों के संदर्भ में एक सीमा संघ है।

CREATE FUNCTION merge_if_adjacent_or_overlaps (d1 daterange, d2 daterange)
RETURNS daterange AS $$
  SELECT
    CASE WHEN d1 && d2 OR d1 -|- d2
    THEN d1 + d2
    ELSE d1
    END;
$$ LANGUAGE sql
IMMUTABLE;

फिर हम इसे इस तरह उपयोग करते हैं,

SELECT DISTINCT ON (lower(cumrange)) cumrange
FROM (
  SELECT merge_if_adjacent_or_overlaps(
    t1.range,
    lag(t1.range) OVER (ORDER BY t1.range)
  ) AS cumrange
  FROM test AS t1
) AS t
ORDER BY lower(cumrange)::date, upper(cumrange)::date DESC NULLS first;

1
विंडो फ़ंक्शन केवल एक समय में दो आसन्न मूल्यों पर विचार करता है और जंजीरों को याद करता है। के साथ प्रयास करें ('2015-01-01', '2015-01-03'), ('2015-01-03', '2015-01-05'), ('2015-01-05', '2015-01-06')
एरविन ब्रान्डसेट्टर
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.