Cách nhanh nhất để đếm số lượng phạm vi ngày bao gồm mỗi ngày từ chuỗi


12

Tôi có một bảng (trong PostgreQuery 9.4) trông như thế này:

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');

Bây giờ tôi muốn tính toán cho các ngày đã cho và cho mọi loại, vào bao nhiêu hàng từ dates_rangesmỗi ngày rơi. Zeros có thể được bỏ qua.

Kết quả như ý:

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

Tôi đã đưa ra hai giải pháp, một với 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

và một với LATERAL, nhanh hơn một chút:

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

Tôi tự hỏi có cách nào tốt hơn để viết truy vấn này không? Và làm thế nào để bao gồm các cặp ngày loại với số đếm 0?

Trong thực tế, có một vài loại khác nhau, thời gian lên tới năm năm (1800 ngày) và ~ 30k hàng trong dates_rangesbảng (nhưng nó có thể tăng trưởng đáng kể).

Không có chỉ số. Nói chính xác trong trường hợp của tôi, đó là kết quả của việc truy vấn phụ, nhưng tôi muốn giới hạn câu hỏi trong một vấn đề, vì vậy nó mang tính tổng quát hơn.


Bạn sẽ làm gì nếu phạm vi trong bảng không chồng chéo hoặc chạm vào nhau. Chẳng hạn, nếu bạn có một phạm vi trong đó (loại, bắt đầu, kết thúc) = (1,2018-01-01,2018-01-15)(1,2018-01-20,2018-01-25)bạn có muốn tính đến điều đó khi xác định có bao nhiêu ngày chồng chéo bạn có không?
Evan Carroll

Tôi cũng bối rối tại sao bàn của bạn nhỏ? Tại sao không phải là 2018-01-31hay 2018-01-30hoặc 2018-01-29trong khi phạm vi đầu tiên có tất cả trong số họ?
Evan Carroll

@EvanCarroll ngày trong generate_serieslà các tham số bên ngoài - chúng không nhất thiết bao gồm tất cả các phạm vi trong dates_rangesbảng. Đối với câu hỏi đầu tiên tôi cho rằng tôi không hiểu nó - các hàng trong dates_rangesđộc lập, tôi không muốn xác định chồng chéo.
BartekCh

Câu trả lời:


4

Truy vấn sau đây cũng hoạt động nếu "thiếu số không" là 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;

nhưng nó không nhanh hơn lateralphiên bản với bộ dữ liệu nhỏ. Mặc dù vậy, nó có thể mở rộng tốt hơn, vì không cần tham gia, nhưng phiên bản trên tổng hợp trên tất cả các hàng, do đó, nó có thể bị mất ở đó một lần nữa.

Truy vấn sau đây cố gắng tránh các công việc không cần thiết bằng cách xóa bất kỳ chuỗi nào không trùng lặp:

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;

- và tôi đã sử dụng overlapstoán tử! Lưu ý rằng bạn phải thêm interval '1 day'vào bên phải vì toán tử chồng lấp coi các khoảng thời gian được mở ở bên phải (điều này khá hợp lý vì ngày thường được coi là dấu thời gian với thành phần thời gian là nửa đêm).


Thật tuyệt, tôi không biết generate_seriescó thể được sử dụng như thế. Sau một vài thử nghiệm tôi có những quan sát sau đây. Truy vấn của bạn thực sự có tỷ lệ rất tốt với độ dài phạm vi đã chọn - thực tế không có sự khác biệt giữa thời gian 3 năm và 10 năm. Tuy nhiên, trong khoảng thời gian ngắn hơn (1 năm), các giải pháp của tôi nhanh hơn - tôi đoán rằng lý do là có một số phạm vi thực sự dài trong dates_ranges(như 2010-2100), đang làm chậm truy vấn của bạn. Giới hạn start_dateend_datebên trong các truy vấn bên trong sẽ giúp mặc dù. Tôi cần phải làm thêm một vài bài kiểm tra.
BartekCh

6

Và làm thế nào để bao gồm các cặp ngày loại với số đếm 0?

Xây dựng một lưới tất cả các kết hợp sau đó LATERAL tham gia vào bảng của bạn, như thế này:

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;

Cũng nên nhanh nhất có thể.

Tôi đã có LEFT JOIN LATERAL ... on truelúc đầu, nhưng có một tổng hợp trong truy vấn con c, vì vậy chúng tôi luôn nhận được một hàng và cũng có thể sử dụng CROSS JOIN. Không có sự khác biệt trong hiệu suất.

Nếu bạn có một bảng chứa tất cả các loại có liên quan , hãy sử dụng bảng đó thay vì tạo danh sách với truy vấn con k.

Các diễn viên integerlà tùy chọn. Khác bạn nhận được bigint.

Các chỉ mục sẽ giúp ích, đặc biệt là một chỉ số nhiều màu trên (kind, start_date, end_date). Vì bạn đang xây dựng trên một truy vấn con, điều này có thể hoặc không thể đạt được.

Việc sử dụng các hàm trả về tập hợp như generate_series()trong SELECTdanh sách thường không được khuyến khích trong các phiên bản Postgres trước 10 (trừ khi bạn biết chính xác những gì bạn đang làm). Xem:

Nếu bạn có nhiều kết hợp có ít hoặc không có hàng, hình thức tương đương này có thể nhanh hơn:

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;

Đối với các hàm trả về tập hợp trong SELECTdanh sách - Tôi đã đọc rằng không nên dùng, tuy nhiên có vẻ như nó hoạt động tốt, nếu chỉ có một chức năng như vậy. Nếu tôi chắc chắn rằng sẽ chỉ có một, có thể có điều gì đó không ổn?
BartekCh

@BartekCh: Một SRF duy nhất trong SELECTdanh sách hoạt động như mong đợi. Có thể thêm một bình luận để cảnh báo chống lại thêm một bình luận. Hoặc di chuyển nó vào FROMdanh sách để bắt đầu với các phiên bản cũ hơn của Postgres. Tại sao có nguy cơ biến chứng? (Đó cũng là SQL chuẩn và sẽ không gây nhầm lẫn cho những người đến từ RDBMS khác.)
Erwin Brandstetter

1

Sử dụng daterangeloại

PostgreSQL có một daterange. Sử dụng nó khá đơn giản. Bắt đầu với dữ liệu mẫu của bạn, chúng tôi chuyển sang sử dụng loại trên bảng.

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)

Tôi muốn tính toán cho các ngày đã cho và cho mọi loại, vào bao nhiêu hàng từ date_ranges mỗi ngày rơi.

Bây giờ để truy vấn nó, chúng tôi đảo ngược thủ tục và tạo một chuỗi ngày nhưng ở đây, chính truy vấn có thể sử dụng @>toán tử ngăn chặn ( ) để kiểm tra xem ngày có nằm trong phạm vi hay không, sử dụng chỉ mục.

Lưu ý chúng tôi sử dụng timestamp without time zone(để ngăn chặn các mối nguy 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;

Đó là sự chồng chéo ngày được ghi thành từng khoản trên chỉ mục.

Là một phần thưởng phụ, với loại daterange, bạn có thể dừng chèn các phạm vi trùng lặp với các phạm vi khác bằng cách sử dụng mộtEXCLUDE CONSTRAINT


Có gì đó không ổn với truy vấn của bạn, có vẻ như nó đang đếm hàng nhiều lần, JOINtôi đoán quá nhiều.
BartekCh

@BartekCh không có bạn có các hàng chồng chéo, bạn có thể khắc phục điều này bằng cách xóa các phạm vi chồng chéo (được đề xuất) hoặc sử dụngcount(DISTINCT kind)
Evan Carroll

nhưng tôi muốn các hàng chồng lên nhau. Ví dụ: 1ngày loại 2018-01-01nằm trong hai hàng đầu tiên từ dates_ranges, nhưng truy vấn của bạn đưa ra 8.
BartekCh

hoặc sử dụngcount(DISTINCT kind) bạn đã thêm DISTINCTtừ khóa ở đó?
Evan Carroll

Thật không may với DISTINCTtừ khóa nó vẫn không hoạt động như mong đợi. Nó đếm các loại khác nhau cho mỗi ngày, nhưng tôi muốn đếm tất cả các hàng của mỗi loại cho mỗi ngày.
BartekCh
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.