วิธีที่เร็วที่สุดในการนับจำนวนช่วงวันที่ครอบคลุมแต่ละวันจากซีรี่ส์


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

ฉันคิดวิธีแก้ปัญหาสองข้อข้อหนึ่งด้วย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

และอีกข้อหนึ่ง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?

ในความเป็นจริงมีไม่กี่ชนิดที่แตกต่างกันระยะเวลาถึงห้าปี (วันที่ 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)ดัชนีจะช่วยโดยเฉพาะอย่างยิ่งดัชนีหลายคอลัมน์ใน เนื่องจากคุณกำลังสร้างข้อความค้นหาย่อยสิ่งนี้อาจเป็นไปได้หรืออาจเป็นไปไม่ได้ที่จะทำให้สำเร็จ

การใช้ฟังก์ชั่น set-return เช่นgenerate_series()ในSELECTรายการโดยทั่วไปไม่แนะนำให้ใช้ในรุ่น 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;

สำหรับฟังก์ชั่น set-return ในSELECTรายการ - ฉันอ่านมาแล้วว่ามันไม่แนะนำให้ใช้ แต่ดูเหมือนว่ามันใช้งานได้ดีถ้ามีฟังก์ชั่นดังกล่าวเพียงตัวเดียว หากฉันแน่ใจว่าจะมีเพียงคนเดียวมีอะไรผิดปกติหรือไม่
BartekCh

@BartekCh: SRF เดียวในSELECTรายการทำงานตามที่คาดไว้ อาจเพิ่มความคิดเห็นเพื่อเตือนไม่ให้เพิ่มอีก หรือย้ายไปที่FROMรายการเพื่อเริ่มต้นด้วย Postgres เวอร์ชันเก่า ทำไมต้องเสี่ยงกับภาวะแทรกซ้อน? (นั่นคือ SQL มาตรฐานและจะไม่สับสนว่าผู้คนมาจาก RDBMS อื่น ๆ )
Erwin 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)

ฉันต้องการคำนวณสำหรับวันที่ที่กำหนดและสำหรับทุกประเภทเป็นจำนวนแถวจาก dates_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;

สิ่งใดคือการทับซ้อนของวันที่แยกรายการบนดัชนี

ในฐานะโบนัสด้านข้างด้วยประเภท daterange คุณสามารถหยุดการแทรกช่วงที่ทับซ้อนกับผู้อื่นโดยใช้EXCLUDE CONSTRAINT


มีบางอย่างผิดปกติกับข้อความค้นหาของคุณดูเหมือนว่าจะนับแถวหลายครั้งJOINฉันเดามากเกินไป
BartekCh

@BartekCh ไม่มีคุณมีแถวที่ทับซ้อนกันคุณสามารถหลีกเลี่ยงได้โดยการลบช่วงที่ทับซ้อนกัน (แนะนำ) หรือใช้count(DISTINCT kind)
Evan Carroll

แต่ฉันต้องการแถวที่ทับซ้อนกัน ตัวอย่างเช่นสำหรับชนิด1วัน2018-01-01นี้เป็นหนึ่งในสองแถวแรกจากแต่การค้นหาของคุณจะช่วยให้dates_ranges 8
BartekCh

หรือcount(DISTINCT kind)คุณใช้เพิ่มDISTINCTคำหลักที่นั่นหรือไม่
Evan Carroll

น่าเสียดายที่มีDISTINCTคำหลักมันยังไม่ทำงานตามที่คาดไว้ มันนับประเภทที่แตกต่างกันสำหรับทุก ๆ วัน แต่ฉันต้องการนับแถวทั้งหมดของแต่ละประเภทสำหรับทุกวัน
BartekCh
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.