Kết hợp các phạm vi riêng biệt thành các phạm vi tiếp giáp lớn nhất có thể


20

Tôi đang cố gắng kết hợp nhiều phạm vi ngày (tải của tôi khoảng tối đa 500, hầu hết các trường hợp 10) có thể hoặc không thể chồng lấp vào phạm vi ngày tiếp giáp lớn nhất có thể. Ví dụ:

Dữ liệu:

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

Bảng trông như:

 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)

Kết quả mong muốn:

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

Đại diện trực quan:

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

Câu trả lời:


22

Giả định / Làm rõ

  1. Không cần phân biệt giữa infinityvà mở giới hạn trên ( upper(range) IS NULL). (Bạn có thể có một trong hai cách, nhưng cách này đơn giản hơn.)

  2. datelà một loại riêng biệt, tất cả các phạm vi có [)giới hạn mặc định . Mỗi tài liệu:

    Việc xây dựng trong các loại dải int4range, int8rangedaterangetất cả sử dụng một hình thức kinh điển bao gồm các ràng buộc thấp hơn và không bao gồm phía trên ràng buộc; có nghĩa là, [).

    Đối với các loại khác (như tsrange!) Tôi sẽ thực thi tương tự nếu có thể:

Giải pháp với SQL thuần túy

Với CTE cho rõ ràng:

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;

Hoặc , tương tự với các truy vấn con, nhanh hơn nhưng ít dễ đọc hơn:

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;

Hoặc với một cấp độ truy vấn ít hơn, nhưng lật thứ tự sắp xếp:

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;
  • Sắp xếp cửa sổ trong bước thứ hai với ORDER BY range DESC NULLS LAST(với NULLS LAST) để có được thứ tự sắp xếp đảo ngược hoàn hảo . Điều này sẽ rẻ hơn (dễ sản xuất hơn, phù hợp với thứ tự sắp xếp của chỉ số được đề xuất một cách hoàn hảo) và chính xác cho các trường hợp góc với rank IS NULL.

Giải thích

a: Trong khi sắp xếp theo thứ tự range, hãy tính mức tối đa đang chạy của giới hạn trên ( enddate) với hàm cửa sổ.
Thay thế giới hạn NULL (không giới hạn) bằng +/- infinitychỉ để đơn giản hóa (không có trường hợp NULL đặc biệt nào).

b: Trong cùng một thứ tự sắp xếp, nếu trước enddateđó sớm hơn startdatechúng ta có một khoảng cách và bắt đầu một phạm vi mới ( step).
Hãy nhớ rằng, giới hạn trên luôn luôn bị loại trừ.

c: Tạo nhóm ( grp) bằng cách đếm các bước với chức năng cửa sổ khác.

Trong SELECTphạm vi xây dựng bên ngoài từ dưới đến giới hạn trên trong mỗi nhóm. Voilá.
Câu trả lời liên quan chặt chẽ về SO với giải thích thêm:

Giải pháp thủ tục với plpgsql

Hoạt động cho bất kỳ tên bảng / cột, nhưng chỉ cho loại daterange.
Các giải pháp thủ tục với các vòng lặp thường chậm hơn, nhưng trong trường hợp đặc biệt này, tôi hy vọng chức năng sẽ nhanh hơn đáng kể vì nó chỉ cần một lần quét tuần tự duy nhất :

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;

Gọi điện:

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

Logic tương tự như các giải pháp SQL, nhưng chúng ta có thể thực hiện với một lần duy nhất.

Câu đố SQL.

Liên quan:

Mũi khoan thông thường để xử lý đầu vào của người dùng trong SQL động:

Mục lục

Đối với mỗi giải pháp này, chỉ số btree (mặc định) đơn giản rangesẽ là công cụ để thực hiện trong các bảng lớn:

CREATE INDEX foo on test (range);

Một chỉ mục btree được sử dụng hạn chế cho các loại phạm vi , nhưng chúng tôi có thể nhận được dữ liệu được sắp xếp trước và thậm chí có thể quét chỉ mục.


@Villiers: Tôi sẽ rất quan tâm đến cách mỗi giải pháp này thực hiện với dữ liệu của bạn. Có lẽ bạn có thể đăng một câu trả lời khác với kết quả kiểm tra và một số thông tin về thiết kế bảng và số lượng của bạn? Tốt nhất với EXPLAIN ( ANALYZE, TIMING OFF)và so sánh tốt nhất trong năm.
Erwin Brandstetter

Chìa khóa của loại vấn đề này là hàm SQL lag (cũng có thể được sử dụng chì) để so sánh các giá trị của các hàng được sắp xếp. Điều này đã loại bỏ nhu cầu tự tham gia cũng có thể được sử dụng để hợp nhất các phạm vi chồng chéo thành một phạm vi duy nhất. Thay vì phạm vi, bất kỳ vấn đề nào liên quan đến hai cột some_star, some_end đều có thể sử dụng chiến lược này.
Kemin Zhou

@ErwinBrandstetter Này, tôi đang cố gắng tìm hiểu truy vấn này (câu hỏi có CTE), nhưng tôi không thể hiểu (CTE A) max(COALESCE(upper(range), 'infinity')) OVER (ORDER BY range) AS enddatedùng để làm gì? Không thể COALESCE(upper(range), 'infinity') as enddatenào? AFAIK max() + over (order by range)sẽ trở lại ngay upper(range)tại đây.
dùng606521

1
@ user606521: Những gì bạn quan sát là trường hợp nếu giới hạn trên tăng liên tục khi được sắp xếp theo phạm vi - có thể được đảm bảo cho một số phân phối dữ liệu và sau đó bạn có thể đơn giản hóa như bạn đề xuất. Ví dụ: phạm vi chiều dài cố định. Nhưng đối với phạm vi độ dài tùy ý, phạm vi tiếp theo có thể có giới hạn dưới lớn hơn, nhưng vẫn là giới hạn trên thấp hơn. Vì vậy, chúng ta cần giới hạn trên lớn nhất của tất cả các phạm vi cho đến nay.
Erwin Brandstetter

6

Tôi đã nghĩ ra điều này:

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

Vẫn cần một chút mài giũa, nhưng ý tưởng là như sau:

  1. bùng nổ các phạm vi đến ngày riêng lẻ
  2. làm điều này, thay thế giới hạn trên vô hạn bằng một số giá trị cực đoan
  3. dựa trên thứ tự từ (1), bắt đầu xây dựng các phạm vi
  4. khi union ( +) không thành công, hãy trả về phạm vi đã xây dựng và khởi tạo lại
  5. cuối cùng, trả về phần còn lại - nếu đạt được giá trị cực trị được xác định trước, thay thế nó bằng NULL để có giới hạn trên vô hạn

Nó chạy generate_series()với tôi khá tốn kém khi chạy cho mọi hàng, đặc biệt là nếu có thể có phạm vi mở ...
Erwin Brandstetter

@ErwinBrandstetter vâng, đó là một vấn đề tôi muốn kiểm tra (sau lần cực đoan đầu tiên của tôi là 9999-12-31 :). Đồng thời, tôi tự hỏi tại sao câu trả lời của tôi có nhiều upvote hơn của bạn. Điều này có thể dễ hiểu hơn ... Vì vậy, các cử tri trong tương lai: Câu trả lời của Erwin vượt trội hơn tôi! Bình chọn ở đó!
dezso

3

Cách đây vài năm, tôi đã thử nghiệm các giải pháp khác nhau (trong số các giải pháp khác tương tự với các giải pháp từ @ErwinBrandstetter) để hợp nhất các giai đoạn chồng chéo trên hệ thống Teradata và tôi đã tìm thấy giải pháp hiệu quả nhất sau (sử dụng Hàm phân tích, phiên bản mới hơn của Teradata có chức năng tích hợp sẵn cho nhiệm vụ đó).

  1. sắp xếp các hàng theo ngày bắt đầu
  2. tìm ngày kết thúc tối đa của tất cả các hàng trước đó: maxEnddate
  3. nếu ngày này nhỏ hơn ngày bắt đầu hiện tại, bạn đã tìm thấy một khoảng trống. Chỉ giữ các hàng đó cộng với hàng đầu tiên trong PHẦN THAM GIA (được chỉ định bởi NULL) và lọc tất cả các hàng khác. Bây giờ bạn có được ngày bắt đầu cho mỗi phạm vi và ngày kết thúc của phạm vi trước đó.
  4. Sau đó, bạn chỉ đơn giản là lấy hàng tiếp theo của maxEnddatesử dụng LEADvà bạn đã gần hoàn tất. Chỉ cho hàng cuối cùng LEADtrả về a NULL, để giải quyết điều này, tính ngày kết thúc tối đa của tất cả các hàng của phân vùng trong bước 2 và COALESCEnó.

Tại sao nó nhanh hơn? Tùy thuộc vào dữ liệu thực tế, bước số 2 có thể giảm đáng kể số lượng hàng, vì vậy bước tiếp theo chỉ cần hoạt động trên một tập hợp con nhỏ, ngoài ra, nó sẽ loại bỏ tổng hợp.

vĩ cầm

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;  

Vì đây là tốc độ nhanh nhất trên Teradata, tôi không biết nếu nó giống với PostgreSQL, sẽ rất tuyệt nếu có được một số con số hiệu suất thực tế.


Có đủ để đặt hàng chỉ khi bắt đầu phạm vi? Nó có hoạt động không nếu bạn có ba phạm vi, mỗi phạm vi có cùng một khởi đầu nhưng kết thúc khác nhau?
Salman A

1
Nó hoạt động với ngày bắt đầu mà thôi, không cần phải thêm ngày kết thúc được sắp xếp giảm dần (bạn chỉ kiểm tra sự chênh lệch, vì vậy bất cứ là hàng đầu tiên cho một ngày nhất định sẽ phù hợp)
dnoeth

-1

Để cho vui, tôi đã cho nó một shot. Tôi thấy đây là phương pháp nhanh nhất sạch nhất để làm điều này. Trước tiên, chúng ta xác định một hàm hợp nhất nếu có sự trùng lặp hoặc nếu hai đầu vào liền kề nhau, nếu không có sự chồng chéo hoặc kề nhau, chúng ta chỉ cần trả về daterange đầu tiên. Gợi ý +là một liên minh phạm vi trong bối cảnh của phạm vi.

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;

Sau đó, chúng tôi sử dụng nó như thế này,

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
Hàm cửa sổ chỉ xem xét hai giá trị liền kề tại một thời điểm và bỏ lỡ chuỗi. Hãy thử với ('2015-01-01', '2015-01-03'), ('2015-01-03', '2015-01-05'), ('2015-01-05', '2015-01-06').
Erwin Brandstetter
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.