シリーズから各日付をカバーする日付の範囲の数を数える最速の方法


12

次のようなテーブル(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 JOINGROUP 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つの問題に限定したかったので、より一般的です。


表の範囲が重なっていない、または接触していない場合はどうしますか。たとえば、あなたは(種類、終了を開始)の範囲を持っている=場合(1,2018-01-01,2018-01-15)(1,2018-01-20,2018-01-25)、あなたが持っているどのように多くの重複の日付を決定する際に考慮に入れていることをしたいですか?
エヴァンキャロル

テーブルが小さい理由もわかりません。なぜない2018-01-31か、2018-01-30または2018-01-29最初の範囲は、それらのすべてを持っているときには?
エヴァンキャロル

@EvanCarrollの日付generate_seriesは外部パラメーターdates_rangesです。表のすべての範囲をカバーしているとは限りません。最初の質問については、理解できないと思います-の行dates_rangesは独立しているので、重複を判別したくありません。
BartekCh 2018年

回答:


4

次のクエリは、「ゼロの欠落」が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時のタイムスタンプと見なされるため、かなり論理的です)。


いいですね、こんなgenerate_series風に使えるとは知りませんでした。いくつかのテストの後、私は以下の観察結果を得ました。クエリは実際に、選択した範囲の長さで非常に適切にスケーリングされます。実際、3年と10年の期間で違いはありません。ただし、期間が短い(1年)場合は、私のソリューションの方が速くなりdates_rangesます。理由は、(2010〜2100のように)非常に長い範囲があり、クエリの速度が低下しているためだと思います。ただし、内部クエリを制限start_dateしてend_date内部で使用すると、問題が解決するはずです。さらにいくつかのテストを行う必要があります。
BartekCh 2018年

6

そして、カウント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、我々はので、常に行を取得して使用することができるCROSS JOINだけでなく。パフォーマンスに違いはありません。

関連するすべての種類を保持するテーブルがある場合は、サブクエリでリストを生成する代わりにそれを使用してkください。

キャスト先integerはオプションです。そうでなければ、あなたは得るbigint

インデックスは特に役立ちます(kind, start_date, end_date)。サブクエリに基づいて構築しているため、これを実現できる場合とできない場合があります。

リストのようgenerate_series()にセットを返す関数を使用することは、Postgresの10より前のバージョンでSELECTは一般的にお勧めできません(何をしているのか正確にわかっていない限り)。見る:

行の数が少ないかまったくない組み合わせが多数ある場合は、この同等の形式の方が高速な場合があります。

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;

で集合を返す関数についてはSELECTリスト-私はそれが唯一のそのような機能があれば、それは、うまく動作しますように見えますが、賢明ではないことを読みました。1つだけになると確信している場合、何か問題が発生する可能性がありますか?
BartekCh 2018年

@BartekCh:SELECTリスト内の単一のSRFは期待どおりに機能します。コメントを追加して、別のコメントを追加しないようにしてください。または、それをFROMリストに移動して、古いバージョンのPostgresから始めます。なぜ合併症を危険にさらすのですか?(これも標準SQLであり、他のRDBMSから来る
ユーザーを

1

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からの各行の行数を計算します。

次に、クエリを実行して手順を逆にし、日付系列生成しますが、クエリ自体が包含(@>)演算子を使用して、インデックスを使用して日付が範囲内にあることを確認できるキャッチがあります

私たちが使用していることに注意してくださいtimestamp without time zone(夏時間の危険を防ぐため)

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;

これは、インデックスの項目化された1日の重複です。

副次的なボーナスとして、daterangeタイプを使用すると、他のものと重複する範囲の挿入をEXCLUDE CONSTRAINT


クエリに問題があります。行を複数回カウントしているようJOINです。1つ多すぎると思います。
BartekCh 2018年

@BartekCh行が重複していない場合、重複する範囲を削除するか(推奨)、または使用することでこれを回避できますcount(DISTINCT kind)
Evan Carroll

しかし、重複する行が必要です。たとえば、種類の1日付2018-01-01はからの最初の2行以内ですがdates_ranges、クエリはを返します8
BartekCh 2018年

または使用count(DISTINCT kind)してDISTINCTキーワードをそこに追加しましたか?
エヴァンキャロル

残念ながら、DISTINCTキーワードではまだ期待どおりに動作しません。日付ごとに異なる種類をカウントしますが、すべての日付の各種類のすべての行をカウントしたいと思います。
BartekCh 2018年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.