web-dev-qa-db-ja.com

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

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

12
BartekCh

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

4
Colin 't Hart

そして、カウント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;
_
6

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を使用して他と重複する範囲の挿入

1
Evan Carroll