Asumsi / Klarifikasi
Tidak perlu membedakan antara infinity
dan membuka batas atas ( upper(range) IS NULL
). (Anda bisa mendapatkannya dengan cara lain, tetapi lebih sederhana dengan cara ini.)
Karena date
merupakan tipe diskrit, semua rentang memiliki [)
batas default .
Per dokumentasi:
Jenis rentang bawaan int4range
,, int8range
dan daterange
semua menggunakan bentuk kanonik yang mencakup batas bawah dan mengecualikan batas atas; itu adalah [)
,.
Untuk tipe lain (seperti tsrange
!) Saya akan menerapkan hal yang sama jika memungkinkan:
Solusi dengan SQL murni
Dengan CTE untuk kejelasan:
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;
Atau , sama dengan subqueries, lebih cepat tetapi juga tidak terlalu mudah dibaca:
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;
Atau dengan satu tingkat subquery yang lebih sedikit, tetapi membalik urutan:
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;
- Sortir jendela pada langkah kedua dengan
ORDER BY range DESC NULLS LAST
(dengan NULLS LAST
) untuk mendapatkan urutan sortir yang terbalik sempurna . Ini harus lebih murah (lebih mudah diproduksi, cocok dengan urutan indeks yang disarankan dengan sempurna) dan akurat untuk kasus sudut dengan rank IS NULL
.
Menjelaskan
a
: Saat memesan oleh range
, hitung maksimum berjalan dari batas atas ( enddate
) dengan fungsi jendela.
Ganti batas NULL (tanpa batas) dengan +/- infinity
hanya untuk menyederhanakan (tidak ada kasus NULL khusus).
b
: Dalam urutan sortir yang sama, jika sebelumnya enddate
lebih awal dari startdate
kami memiliki celah dan memulai rentang baru ( step
).
Ingat, batas atas selalu dikecualikan.
c
: Bentuk grup ( grp
) dengan menghitung langkah-langkah dengan fungsi jendela lain.
Di luar SELECT
membangun berkisar dari batas bawah ke batas atas di setiap kelompok. Voa.
Jawaban terkait erat pada SO dengan penjelasan lebih lanjut:
Solusi prosedural dengan plpgsql
Berfungsi untuk nama tabel / kolom apa saja, tetapi hanya untuk tipe daterange
.
Solusi prosedural dengan loop biasanya lebih lambat, tetapi dalam kasus khusus ini saya berharap fungsi menjadi jauh lebih cepat karena hanya membutuhkan pemindaian sekuensial tunggal :
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;
Panggilan:
SELECT * FROM f_range_agg('test', 'range'); -- table and column name
Logikanya mirip dengan solusi SQL, tetapi kita bisa puas dengan satu pass.
SQL Fiddle.
Terkait:
Bor biasa untuk menangani input pengguna dalam SQL dinamis:
Indeks
Untuk masing-masing solusi ini, indeks btree (default) polos range
akan sangat berperan untuk kinerja dalam tabel besar:
CREATE INDEX foo on test (range);
Indeks btree terbatas penggunaannya untuk tipe rentang , tetapi kita bisa mendapatkan data pra-disortir dan bahkan mungkin hanya pemindaian indeks.