পৃথকতম ব্যাপ্তিকে সবচেয়ে বড় সংমিশ্রিত রেঞ্জের সাথে সংমিশ্রণ করা


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) খোলার দরকার নেই । (আপনি এটি যে কোনও উপায়ে পেতে পারেন, তবে এটি সহজ উপায় simp)

  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;

বা একটি কম subquery স্তর সঙ্গে, কিন্তু উল্টানো বাছাই ক্রম:

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;

ব্যাখ্যা করা

a: দ্বারা অর্ডার করার সময় range, উইন্ডো ফাংশন সহ উপরের বাউন্ডের সর্বোচ্চ চলমান গণনা করুন enddate। কেবলমাত্র
সরলকরণের infinityজন্য (বিশেষ কোনও NULL কেস নেই) + /- দিয়ে ন্যূনাল বাউন্ডস (আনবাউন্ডেড) প্রতিস্থাপন করুন ।

b: একই সাজানোর ক্রমে, পূর্ববর্তীটি যদি আমাদের ফাঁক enddateহওয়ার চেয়ে আগের হয় startdateএবং একটি নতুন পরিসর ( step) শুরু করে।
মনে রাখবেন, উপরের সীমাটি সর্বদা বাদ থাকে।

c: grpঅন্য উইন্ডো ফাংশন সহ পদক্ষেপ গণনা করে গ্রুপগুলি ( ) ফর্ম করুন ।

বাইরের SELECTবিল্ডে প্রতিটি গ্রুপের নিম্ন থেকে উপরের সীমানা পর্যন্ত রয়েছে। Voila।
আরও ব্যাখ্যা সহ এসও তে ঘনিষ্ঠভাবে সম্পর্কিত উত্তর:

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

যুক্তিটি এসকিউএল সমাধানগুলির অনুরূপ, তবে আমরা একক পাস দিয়ে করতে পারি।

এসকিউএল ফিডল।

সম্পর্কিত:

গতিশীল এসকিউএল এ ব্যবহারকারী ইনপুট পরিচালনা করার জন্য সাধারণ ড্রিল:

সূচক

এই প্রতিটি সমাধানের জন্য একটি সাধারণ (ডিফল্ট) বিটি্রি সূচক rangeবড় টেবিলগুলিতে পারফরম্যান্সের জন্য সহায়ক হবে:

CREATE INDEX foo on test (range);

বিটিরি সূচকটি সীমার ধরণের জন্য সীমিত ব্যবহারের জন্য , তবে আমরা প্রাক-সাজানো ডেটা পেতে পারি এবং এমনকি কেবল সূচক-কেবল স্ক্যান করতে পারি।


@ ভিলিয়ার্স: এই সমাধানগুলির প্রতিটি কীভাবে আপনার ডেটা দিয়ে সম্পাদন করে তা আমি খুব আগ্রহী হব। হতে পারে আপনি পরীক্ষার ফলাফল এবং আপনার টেবিলের নকশা এবং কার্ডিনালিটির উপর কিছু তথ্য সহ আরও একটি উত্তর পোস্ট করতে পারেন? সেরা EXPLAIN ( ANALYZE, TIMING OFF)পাঁচটির সাথে সেরা এবং তুলনা করুন।
এরউইন ব্র্যান্ডস্টেটর

এই ধরণের সমস্যার মূল চাবিকাঠি হ'ল লেগ এসকিউএল ফাংশন (সীসা এছাড়াও ব্যবহার করা যেতে পারে) যা সাজানো সারিগুলির মানগুলির তুলনা করে। এটি স্ব-যোগদানের প্রয়োজনীয়তা দূর করেছে যা ওভারল্যাপিং রেঞ্জকে একক পরিসরে একত্রিত করতেও ব্যবহার করা যেতে পারে। পরিসরের পরিবর্তে, দুটি কলাম জড়িত যে কোনও সমস্যা কেউ_ স্টার, কিছু_এন্ড এই কৌশলটি ব্যবহার করতে পারে।
কেমিন ঝো

@ এরউইন ব্র্যান্ডসটেটার আরে, আমি এই প্রশ্নটি (সিটিই সহ একটি) বোঝার চেষ্টা করছি, তবে (সিটিই এ) কীসের জন্য তা বুঝতে পারি না max(COALESCE(upper(range), 'infinity')) OVER (ORDER BY range) AS enddate? এটা কি ঠিক হতে পারে না COALESCE(upper(range), 'infinity') as enddate? এএফআইকে max() + over (order by range)ঠিক upper(range)এখানে ফিরে আসবে ।
ব্যবহারকারী 606521

1
@ ইউজার 606521: আপনি যা পর্যবেক্ষণ করেন তা হ'ল যদি উপরের সীমানা ধারাবাহিকভাবে বাছাই করার সময় অবিচ্ছিন্নভাবে বৃদ্ধি পায় - যা কিছু ডেটা বিতরণের জন্য গ্যারান্টিযুক্ত হতে পারে এবং তারপরে আপনি প্রস্তাবিত হিসাবে সহজ করতে পারবেন। উদাহরণ: নির্দিষ্ট দৈর্ঘ্যের ব্যাপ্তি। তবে স্বেচ্ছাসেবী দৈর্ঘ্যের ব্যাপ্তির জন্য পরবর্তী পরিসীমাটির উচ্চতর বাউন্ডটি আরও বেশি হতে পারে তবে তারপরেও নিম্নতর আবদ্ধ থাকতে পারে। সুতরাং আমাদের এখন পর্যন্ত সমস্ত রেঞ্জের সর্বাধিক upperর্ধ্বসীমা প্রয়োজন।
এরউইন ব্র্যান্ডস্টেটার

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()প্রতি সারির জন্য দৌড়ানোর চেয়ে ব্যয়বহুল হিসাবে আঘাত করে, বিশেষত যদি সেখানে খোলা রেঞ্জ থাকতে পারে ...
এরউইন ব্র্যান্ডসেটেটার

@ এরউইন ব্র্যান্ডস্টেটর হ্যাঁ, এটি এমন একটি বিষয় যা আমি পরীক্ষা করতে চেয়েছিলাম (আমার প্রথম চূড়ান্ত হওয়ার পরে 9999-12-31 :) ছিল। একই সাথে, আমি ভাবছি কেন আমার উত্তরটি আপনার চেয়ে বেশি উত্সাহী। এটি সম্ভবত বোঝার পক্ষে সহজ ... সুতরাং, ভবিষ্যতের ভোটাররা: এরউইনের উত্তর আমার চেয়ে শ্রেষ্ঠ! সেখানে ভোট দিন!
dezso

3

কয়েক বছর আগে আমি একটি টেরাদাতা সিস্টেমে ওভারল্যাপিং পিরিয়ডগুলি মার্জ করার জন্য বিভিন্ন সমাধান (অন্যদের মধ্যে @ এরউইন ব্র্যান্ডসেটটারের সাথে কিছু অনুরূপ) পরীক্ষা করেছিলাম এবং আমি নিম্নলিখিতটি সবচেয়ে কার্যকর একটি (অ্যানালিটিকাল ফাংশন ব্যবহার করে, টেরাদাতার নতুন সংস্করণে অন্তর্নির্মিত ফাংশনগুলি ব্যবহার করেছি) পেয়েছি যে কাজ)।

  1. শুরুর তারিখ অনুসারে সারিগুলি সাজান
  2. সমস্ত পূর্ববর্তী সারির সর্বাধিক শেষের তারিখটি সন্ধান করুন: maxEnddate
  3. যদি এই তারিখটি বর্তমান শুরু হওয়ার তারিখের চেয়ে কম হয় তবে আপনি একটি ফাঁক খুঁজে পেয়েছেন। পার্টিশনের মধ্যে কেবল প্রথম সারিটি যুক্ত করুন (যা কোনও নুল দ্বারা নির্দেশিত) এবং অন্যান্য সমস্ত সারিগুলিকে ফিল্টার করুন। এখন আপনি প্রতিটি ব্যাপ্তির আরম্ভের তারিখ এবং পূর্ববর্তী ব্যাপ্তির শেষ তারিখ পান।
  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;  

যেহেতু এটি টেরাদাতার সবচেয়ে দ্রুত ছিল, আমি জানি না পোস্টগ্র্রেএসকিউএল এর জন্য এটি একই কিনা, কিছু প্রকৃত পারফরম্যান্স নম্বর পেতে ভাল লাগবে।


কেবলমাত্র ব্যাপ্তি শুরু করে অর্ডার করার পক্ষে এটি কি যথেষ্ট? যদি আপনার একই স্টার্টের সাথে আলাদা হলেও শেষের সাথে তিনটি রেঞ্জ থাকে তবে এটি কাজ করে?
সালমান এ

1
এটি কেবল প্রারম্ভের তারিখের সাথেই কাজ করে,
অবতীর্ণের

-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')
এরউইন ব্র্যান্ডসটেটার
আমাদের সাইট ব্যবহার করে, আপনি স্বীকার করেছেন যে আপনি আমাদের কুকি নীতি এবং গোপনীয়তা নীতিটি পড়েছেন এবং বুঝতে পেরেছেন ।
Licensed under cc by-sa 3.0 with attribution required.