私は複数の日付範囲(私の負荷は最大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 | ==
--+---------------------------
| ====== == ===============>
infinity
とオープン上限(upper(range) IS NULL
)を区別する必要はありません。 (どちらの方法でもかまいませんが、こちらの方が簡単です。)
date
は離散型であるため、すべての範囲にはデフォルトの[)
境界があります。 ドキュメントごと:
組み込みの範囲型
int4range
、int8range
、およびdaterange
はすべて、下限を含み、上限を除外する正規形式を使用します。つまり、[)
です。
他のタイプ(tsrange
!など)については、可能であれば同じように強制します。
明確にするためにCTEを使用:
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;
Orサブクエリレベルが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
を使用)で並べ替えて、 perfectly 逆の並べ替え順序を取得します。これはより安く(作成が簡単で、提案されたインデックスのソート順を完全に一致させます)、rank IS NULL
。のコーナーケースでは accurate a
:range
で並べ替えるときに、実行最大値ウィンドウ関数での上限(enddate
)の。
単純化するために、NULL境界(無制限)を+/- infinity
に置き換えます(特別なNULLケースはありません)。
b
:同じソート順で、前のenddate
がstartdate
よりも前の場合ギャップがあり、新しい範囲(step
)を開始します。
上限は always 除外されることを覚えておいてください。
c
:別のウィンドウ関数で歩数をカウントすることにより、グループ(grp
)を形成します。
外側のSELECT
ビルドでは、各グループの下限から上限までの範囲です。ボイラ。
SOの詳細な説明付きの密接に関連する回答:
任意のテーブル/列名で機能しますが、タイプdaterange
でのみ機能します。
ループを使用した手続き型のソリューションは通常遅いですbutこの特殊なケースでは、関数は実質的に =)速い単一の順次スキャンのみが必要なため:
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ソリューションに似ていますが、1つのパスで対処できます。
関連:
動的SQLでユーザー入力を処理するための通常のドリル:
これらのソリューションのそれぞれについて、range
のプレーン(デフォルト)btreeインデックスは、大きなテーブルでのパフォーマンスに役立ちます。
CREATE INDEX foo on test (range);
btreeインデックスは範囲タイプの使用が制限されています ですが、事前にソートされたデータや、おそらくインデックスのみのスキャンも取得できます。
私はこれを思いつきました:
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;
$$;
まだ少しホーニングが必要ですが、アイデアは次のとおりです。
+
)は失敗し、既に構築された範囲を返し、再初期化します数年前、Teradataシステムで重複する期間をマージするために(@ErwinBrandstetterのソリューションと同様のいくつかのソリューションの中で)さまざまなソリューションをテストし、次の最も効率的なソリューションを見つけました(分析関数を使用して、Teradataの新しいバージョンには組み込み関数があります)そのタスク)。
maxEnddate
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;
これはTeradataで最速だったので、それがPostgreSQLでも同じであるかどうかはわかりません。実際のパフォーマンスの数値を取得するといいでしょう。