次のようなテーブル(PostgreSQL 9.4)があります。
CREATE TABLE dates_ranges (kind int, start_date date, end_date date);
INSERT INTO dates_ranges VALUES
(1, '2018-01-01', '2018-01-31'),
(1, '2018-01-01', '2018-01-05'),
(1, '2018-01-03', '2018-01-06'),
(2, '2018-01-01', '2018-01-01'),
(2, '2018-01-01', '2018-01-02'),
(3, '2018-01-02', '2018-01-08'),
(3, '2018-01-05', '2018-01-10');
ここで、指定された日付およびすべての種類について、各日付がdates_ranges
からの行数を計算する必要があります。ゼロはおそらく省略できます。
望ましい結果:
+-------+------------+----+
| kind | as_of_date | n |
+-------+------------+----+
| 1 | 2018-01-01 | 2 |
| 1 | 2018-01-02 | 2 |
| 1 | 2018-01-03 | 3 |
| 2 | 2018-01-01 | 2 |
| 2 | 2018-01-02 | 1 |
| 3 | 2018-01-02 | 1 |
| 3 | 2018-01-03 | 1 |
+-------+------------+----+
私は2つのソリューションを考え出しました。1つはLEFT JOIN
とGROUP BY
です。
SELECT
kind, as_of_date, COUNT(*) n
FROM
(SELECT d::date AS as_of_date FROM generate_series('2018-01-01'::timestamp, '2018-01-03'::timestamp, '1 day') d) dates
LEFT JOIN
dates_ranges ON dates.as_of_date BETWEEN start_date AND end_date
GROUP BY 1,2 ORDER BY 1,2
もう1つ、LATERAL
が付いています。
SELECT
kind, as_of_date, n
FROM
(SELECT d::date AS as_of_date FROM generate_series('2018-01-01'::timestamp, '2018-01-03'::timestamp, '1 day') d) dates,
LATERAL
(SELECT kind, COUNT(*) AS n FROM dates_ranges WHERE dates.as_of_date BETWEEN start_date AND end_date GROUP BY kind) ss
ORDER BY kind, as_of_date
このクエリを作成するためのより良い方法はあるのでしょうか?そして、カウント0の日付のペアを含める方法は?
実際には、いくつかの明確な種類があり、最大5年(1800日付)の期間、およびdates_ranges
テーブルに約3万行があります(ただし、大幅に拡大する可能性があります)。
インデックスはありません。私の場合、正確にはサブクエリの結果ですが、質問を1つの問題に限定したかったので、より一般的です。
次のクエリは、「ゼロの欠落」がOKの場合にも機能します。
select *
from (
select
kind,
generate_series(start_date, end_date, interval '1 day')::date as d,
count(*)
from dates_ranges
group by 1, 2
) x
where d between date '2018-01-01' and date '2018-01-03'
order by 1, 2;
ただし、データセットが小さいlateral
バージョンよりも高速ではありません。ただし、結合は必要ないため、拡張性が向上する可能性がありますが、上記のバージョンではすべての行が集計されるため、再び失われる可能性があります。
次のクエリは、重複しないシリーズを削除することで、不要な作業を回避しようとしています。
select
kind,
generate_series(greatest(start_date, date '2018-01-01'), least(end_date, date '2018-01-03'), interval '1 day')::date as d,
count(*)
from dates_ranges
where (start_date, end_date + interval '1 day') overlaps (date '2018-01-01', date '2018-01-03' + interval '1 day')
group by 1, 2
order by 1, 2;
-overlaps
演算子を使用する必要があります!オーバーラップ演算子は期間が右側で開いていると見なすため、右側にinterval '1 day'
を追加する必要があることに注意してください(日付は、多くの場合、時刻コンポーネントが午前0時のタイムスタンプと見なされるため、かなり論理的です)。
そして、カウント0の日付のペアを含める方法は?
次のように、すべての組み合わせのグリッドを作成し、次にLATERAL
テーブルに結合します。
_SELECT k.kind, d.as_of_date, c.n
FROM (SELECT DISTINCT kind FROM dates_ranges) k
CROSS JOIN (
SELECT d::date AS as_of_date
FROM generate_series(timestamp '2018-01-01', timestamp '2018-01-03', interval '1 day') d
) d
CROSS JOIN LATERAL (
SELECT count(*)::int AS n
FROM dates_ranges
WHERE kind = k.kind
AND d.as_of_date BETWEEN start_date AND end_date
) c
ORDER BY k.kind, d.as_of_date;
_
また、可能な限り高速でなければなりません。
最初は_LEFT JOIN LATERAL ... on true
_がありましたが、サブクエリc
に集約があるため、always行を取得し、 _CROSS JOIN
_も使用できます。パフォーマンスに違いはありません。
関連するすべてのkindsを保持するテーブルがある場合は、サブクエリk
を使用してリストを生成する代わりにそれを使用します。
integer
へのキャストはオプションです。それ以外の場合はbigint
を取得します。
インデックスは、特に_(kind, start_date, end_date)
_の複数列インデックスに役立ちます。サブクエリに基づいて構築しているため、これを実現できる場合とできない場合があります。
SELECT
リストでgenerate_series()
のようなセットを返す関数を使用することは、Postgres 10より前のバージョンでは一般にお勧めできませんあなたがあなたが何をしているかを正確に知らない限り)。見る:
行の数が少ないかまったくない組み合わせが多数ある場合は、この同等の形式の方が高速な場合があります。
_SELECT k.kind, d.as_of_date, count(dr.kind)::int AS n
FROM (SELECT DISTINCT kind FROM dates_ranges) k
CROSS JOIN (
SELECT d::date AS as_of_date
FROM generate_series(timestamp '2018-01-01', timestamp '2018-01-03', interval '1 day') d
) d
LEFT JOIN dates_ranges dr ON dr.kind = k.kind
AND d.as_of_date BETWEEN dr.start_date AND dr.end_date
GROUP BY 1, 2
ORDER BY 1, 2;
_
daterange
タイプの使用PostgreSQLには daterange
があります。使い方はとても簡単です。サンプルデータから始めて、テーブルのタイプを使用するように移動します。
BEGIN;
ALTER TABLE dates_ranges ADD COLUMN myrange daterange;
UPDATE dates_ranges
SET myrange = daterange(start_date, end_date, '[]');
ALTER TABLE dates_ranges
DROP COLUMN start_date,
DROP COLUMN end_date;
COMMIT;
-- Now you can create Gist index on it...
CREATE INDEX ON dates_ranges USING Gist (myrange);
TABLE dates_ranges;
kind | myrange
------+-------------------------
1 | [2018-01-01,2018-02-01)
1 | [2018-01-01,2018-01-06)
1 | [2018-01-03,2018-01-07)
2 | [2018-01-01,2018-01-02)
2 | [2018-01-01,2018-01-03)
3 | [2018-01-02,2018-01-09)
3 | [2018-01-05,2018-01-11)
(7 rows)
与えられた日付と種類ごとに、dates_rangesからの各行の行数を計算します。
次に、クエリを実行するために手順を逆にします generate a date series ですが、クエリ自体が包含(@>
)演算子を使用して日付が範囲内にあることを確認できるインデックスを使用します。
_timestamp without time zone
を使用することに注意してください(DSTの危険を停止するため)
SELECT d1.kind, day::date, count(d2.kind)
FROM dates_ranges AS d1
CROSS JOIN LATERAL generate_series(
lower(myrange)::timestamp without time zone,
upper(myrange)::timestamp without time zone,
'1 day'
) AS gs(day)
INNER JOIN dates_ranges AS d2
ON d2.myrange @> day::date
GROUP BY d1.kind, day;
これは、インデックスの項目別の日数の重複です。
副次的なボーナスとして、daterangeタイプで停止できます EXCLUDE CONSTRAINT
を使用して他と重複する範囲の挿入