시리즈에서 각 날짜를 다루는 날짜 범위 수를 계산하는 가장 빠른 방법


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대해 각 날짜의 행 수를 계산하려고합니다 . 0은 생략 될 수 있습니다.

원하는 결과 :

+-------+------------+----+
|  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 |
+-------+------------+----+

나는 두 가지 솔루션, 하나 마련했습니다 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

와 하나 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 일), ~ 30k 개의 행이 있습니다 dates_ranges(그러나 크게 늘어날 수 있음).

인덱스가 없습니다. 내 경우에는 정확하게는 하위 쿼리의 결과이지만 질문을 하나의 문제로 제한하고 싶기 때문에 더 일반적입니다.


표의 범위가 겹치거나 닿지 않으면 어떻게해야합니까? 예를 들어 당신은 범위 (종류, 시작, 끝)이 = 경우 (1,2018-01-01,2018-01-15)그리고 (1,2018-01-20,2018-01-25)당신은 얼마나 많은 중복 날짜를 결정할 때 당신이 고려하는 것이 먹고 싶어합니까?
Evan Carroll

나는 왜 당신의 테이블이 작은 지 혼란 스럽습니까? 왜 아닙니다 2018-01-31또는 2018-01-30또는 2018-01-29최초의 범위는 그들 모두를 가질 때 거기에?
Evan Carroll

@EvanCarroll 날짜 generate_series는 외부 매개 변수이므로 반드시 dates_ranges표의 모든 범위를 다룰 필요는 없습니다 . 첫 번째 질문에 대해서는 이해하지 못한다고 가정합니다. 행 dates_ranges은 독립적이므로 중복을 결정하고 싶지 않습니다.
BartekCh

답변:


4

다음 쿼리는 "제로 누락"이 정상인 경우에도 작동합니다.

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'오버랩 연산자가 기간을 오른쪽에서 열린 것으로 간주 할 때 오른쪽 에 추가 해야합니다 (날짜가 자정의 시간 구성 요소가있는 시간 소인으로 간주되기 때문에 상당히 논리적 임).


좋아, 나는 그런 식 generate_series으로 사용될 수 있다는 것을 몰랐다 . 몇 가지 테스트 후 나는 다음과 같은 관찰을했다. 선택한 범위 길이에 따라 쿼리가 실제로 확장됩니다. 실제로 3 년에서 10 년 사이의 차이는 없습니다. 그러나 짧은 기간 (1 년) 동안 내 솔루션이 더 빠릅니다.이 이유는 dates_ranges(2010-2100과 같이) 실제로 장거리가 있기 때문에 쿼리 속도가 느려지기 때문입니다. 내부 쿼리를 제한 start_date하고 end_date내부에서 도움이 될 것입니다. 몇 가지 테스트를 더해야합니다.
BartekCh

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). 하위 쿼리를 작성하고 있기 때문에이 작업을 수행하거나 수행하지 못할 수 있습니다.

Postgres 버전 10 이전 generate_series()에는 SELECT목록 에서 와 같이 set-returning 함수를 사용하는 것이 바람직하지 않습니다 (무엇을하고 있는지 정확히 알지 않는 한). 보다:

행이 적거나없는 조합이 많이있는 경우이 형식이 더 빠를 수 있습니다.

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목록의 set-returning 함수에 관해서는 -바람직하지 않다는 것을 읽었지만 그러한 함수가 하나만 있으면 제대로 작동하는 것처럼 보입니다. 하나만있을 것이라고 확신한다면 문제가 발생할 수 있습니까?
BartekCh

@BartekCh : SELECT목록 의 단일 SRF가 예상대로 작동합니다. 다른 의견을 추가하지 않도록 경고하는 의견을 추가하십시오. 또는 FROM이전 버전의 Postgres에서 시작 하도록 목록으로 이동하십시오 . 왜 합병증이 위험합니까? (즉 또한 표준 SQL의와 혼동 사람들은 다른 RDBMS에서 오는되지 않습니다.)
어윈 Brandstetter

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)

주어진 날짜와 모든 종류에 대해 date_ranges에서 각 날짜가 몇 줄인지 계산하고 싶습니다.

이제 쿼리하기 위해 프로 시저를 뒤집고 날짜 시리즈를 생성 하지만 쿼리 자체가 포함 ( @>) 연산자를 사용하여 인덱스를 사용하여 날짜가 범위 내에 있는지 확인할 수 있습니다.

우리는 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;

인덱스에서 항목 별 하루 오버랩입니다.

부수 보너스로 날짜 범위 유형을 사용하면을 사용하여 다른 범위와 겹치는 범위의 삽입을 중지 할 수 있습니다EXCLUDE CONSTRAINT


검색어에 문제가 있습니다. 행이 여러 번 계산되는 것처럼 보입니다 JOIN. 너무 많이 생각합니다.
BartekCh

@BartekCh 겹치는 행이 없습니다. 겹치는 범위를 제거하거나 (권장)count(DISTINCT kind)
Evan Carroll

그러나 겹치는 행을 원합니다. 예를 들어 친절한 1날짜 2018-01-01는에서 처음 두 행 내에 dates_ranges있지만 쿼리는을 제공합니다 8.
BartekCh 2016 년

또는 사용count(DISTINCT kind) 하여 DISTINCT키워드 를 추가 했 습니까?
Evan Carroll

불행히도 DISTINCT키워드의 경우 여전히 예상대로 작동하지 않습니다. 모든 날짜에 대해 고유 한 종류를 계산하지만 모든 날짜에 대해 각 종류의 모든 행을 계산하고 싶습니다.
BartekCh
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.