ผลรวมกลิ้ง / นับ / เฉลี่ยในช่วงวันที่


20

ในฐานข้อมูลของธุรกรรมที่ครอบคลุม 1,000 กิจการในระยะเวลา 18 เดือนฉันต้องการเรียกใช้แบบสอบถามเพื่อจัดกลุ่มทุกช่วงเวลา 30 วันที่เป็นไปได้โดยentity_idใช้ SUM ของจำนวนธุรกรรมและ COUNT ของธุรกรรมในช่วง 30 วันนั้นและ คืนค่าข้อมูลในวิธีที่ฉันสามารถสอบถามได้ หลังจากการทดสอบจำนวนมากรหัสนี้บรรลุสิ่งที่ฉันต้องการ:

SELECT id, trans_ref_no, amount, trans_date, entity_id,
    SUM(amount) OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_total,
    COUNT(id)   OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_count
  FROM transactiondb;

และฉันจะใช้ในแบบสอบถามที่มีโครงสร้างขนาดใหญ่กว่าเช่น:

SELECT * FROM (
  SELECT id, trans_ref_no, amount, trans_date, entity_id,
      SUM(amount) OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_total,
      COUNT(id)   OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_count
    FROM transactiondb ) q
WHERE trans_count >= 4
AND trans_total >= 50000;

กรณีที่แบบสอบถามนี้ไม่ครอบคลุมคือเมื่อการนับธุรกรรมจะครอบคลุมหลายเดือน แต่ยังคงอยู่ภายใน 30 วันนับจากวันอื่น แบบสอบถามชนิดนี้เป็นไปได้กับ Postgres หรือไม่ ถ้าเป็นเช่นนั้นฉันยินดีต้อนรับการป้อนข้อมูลใด ๆ หลายหัวข้ออื่น ๆ พูดคุยกัน " ทำงาน " มวลรวมไม่กลิ้ง

ปรับปรุง

CREATE TABLEสคริปต์:

CREATE TABLE transactiondb (
    id integer NOT NULL,
    trans_ref_no character varying(255),
    amount numeric(18,2),
    trans_date date,
    entity_id integer
);

ข้อมูลตัวอย่างสามารถพบได้ที่นี่ ฉันใช้ PostgreSQL 9.1.16

ผลลัพธ์ในอุดมคติจะรวมSUM(amount)และCOUNT()ธุรกรรมทั้งหมดในช่วงเวลา 30 วัน ดูภาพนี้เช่น:

ตัวอย่างของแถวที่จะรวมอยู่ใน "set" แต่ไม่ใช่เพราะชุดของฉันคงที่ทุกเดือน

การเน้นวันที่สีเขียวแสดงว่าข้อความค้นหาของฉันถูกรวมไว้ในสิ่งใด การเน้นแถวสีเหลืองหมายถึงบันทึกสิ่งที่ฉันต้องการเป็นส่วนหนึ่งของชุด

อ่านก่อนหน้า:


1
โดยevery possible 30-day period by entity_idที่คุณหมายถึงระยะเวลาที่สามารถเริ่มต้นใด ๆวันดังนั้น 365 ระยะเวลาที่เป็นไปได้ในปี (ยังไม่ได้ก้าวกระโดด)? หรือคุณต้องการที่จะพิจารณาวันที่มีการทำธุรกรรมจริงเป็นจุดเริ่มต้นของช่วงเวลาสำหรับแต่ละคนentity_id ? โปรดระบุคำนิยามตารางของคุณรุ่น Postgres ข้อมูลตัวอย่างบางส่วนและผลลัพธ์ที่คาดหวังสำหรับตัวอย่าง
Erwin Brandstetter

ในทางทฤษฎีฉันหมายถึงทุกวัน แต่ในทางปฏิบัติไม่จำเป็นต้องพิจารณาวันที่ไม่มีธุรกรรม ฉันโพสต์ตัวอย่างข้อมูลและคำจำกัดความของตาราง
tufelkinder

ดังนั้นคุณต้องการสะสมแถวของเดียวกันentity_idในหน้าต่าง 30 วันโดยเริ่มจากแต่ละธุรกรรมจริง สามารถมีธุรกรรมหลายรายการในชุดเดียวกัน(trans_date, entity_id)หรือชุดค่าผสมนั้นมีลักษณะเฉพาะได้หรือไม่? คำจำกัดความของตารางของคุณไม่มีUNIQUEข้อ จำกัด หรือ PK แต่ดูเหมือนว่าข้อ จำกัด จะหายไป ...
Erwin Brandstetter

ข้อ จำกัด เพียงอย่างเดียวคือidคีย์หลัก สามารถมีธุรกรรมได้หลายรายการต่อเอนทิตีต่อวัน
tufelkinder

เกี่ยวกับการกระจายข้อมูล: มีรายการ (ต่อ entity_id) เป็นเวลาเกือบทุกวันหรือไม่
Erwin Brandstetter

คำตอบ:


26

แบบสอบถามที่คุณมี

คุณสามารถทำให้การสืบค้นของคุณง่ายขึ้นโดยใช้WINDOWประโยค แต่นั่นก็แค่ทำให้ไวยากรณ์สั้นลงไม่ใช่การเปลี่ยนแผนคิวรี

SELECT id, trans_ref_no, amount, trans_date, entity_id
     , SUM(amount) OVER w AS trans_total
     , COUNT(*)    OVER w AS trans_count
FROM   transactiondb
WINDOW w AS (PARTITION BY entity_id, date_trunc('month',trans_date)
             ORDER BY trans_date
             ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING);
  • ยังใช้ความเร็วที่เร็วขึ้นเล็กน้อยcount(*)เนื่องจากidมีการกำหนดไว้แน่นอนNOT NULLหรือไม่
  • และคุณไม่จำเป็นต้องทำORDER BY entity_idตั้งแต่ตอนนี้PARTITION BY entity_id

คุณสามารถทำให้ง่ายขึ้นต่อไปแม้ว่า:
อย่าเพิ่มORDER BYคำจำกัดความหน้าต่างเลยไม่เกี่ยวข้องกับคำค้นหาของคุณ จากนั้นคุณไม่จำเป็นต้องกำหนดกรอบหน้าต่างแบบกำหนดเองเช่น:

SELECT id, trans_ref_no, amount, trans_date, entity_id
     , SUM(amount) OVER w AS trans_total
     , COUNT(*)    OVER w AS trans_count
FROM   transactiondb
WINDOW w AS (PARTITION BY entity_id, date_trunc('month',trans_date);

เรียบง่ายเร็วขึ้น แต่ก็ยังเป็นรุ่นที่ดีกว่าของสิ่งที่คุณมีกับแบบคงที่เดือน

แบบสอบถามที่คุณอาจต้องการ

... ยังไม่ได้กำหนดไว้อย่างชัดเจนดังนั้นฉันจะสร้างสมมติฐานเหล่านี้:

นับการทำธุรกรรมและจำนวนเงินสำหรับทุกระยะเวลา 30 entity_idวันในการทำธุรกรรมครั้งแรกและครั้งสุดท้ายของการใด ๆ ยกเว้นช่วงเวลานำหน้าและตามหลังโดยไม่มีกิจกรรม แต่รวมช่วงเวลา 30 วันที่เป็นไปได้ทั้งหมดภายในขอบเขตด้านนอกเหล่านั้น

SELECT entity_id, trans_date
     , COALESCE(sum(daily_amount) OVER w, 0) AS trans_total
     , COALESCE(sum(daily_count)  OVER w, 0) AS trans_count
FROM  (
   SELECT entity_id
        , generate_series (min(trans_date)::timestamp
                         , GREATEST(min(trans_date), max(trans_date) - 29)::timestamp
                         , interval '1 day')::date AS trans_date
   FROM   transactiondb 
   GROUP  BY 1
   ) x
LEFT JOIN (
   SELECT entity_id, trans_date
        , sum(amount) AS daily_amount, count(*) AS daily_count
   FROM   transactiondb
   GROUP  BY 1, 2
   ) t USING (entity_id, trans_date)
WINDOW w AS (PARTITION BY entity_id ORDER BY trans_date
             ROWS BETWEEN CURRENT ROW AND 29 FOLLOWING);

รายการนี้มีระยะเวลา 30 วันทั้งหมดสำหรับแต่ละรายการ entity_idโดยมีผลรวมของคุณและtrans_dateเป็นวันแรก (รวม) ของช่วงเวลานั้น หากต้องการรับค่าสำหรับแต่ละแถวแต่ละแถวเข้าร่วมในตารางฐานอีกครั้ง ...

ปัญหาพื้นฐานเหมือนกับที่กล่าวไว้ที่นี่:

การกำหนดเฟรมของหน้าต่างไม่สามารถขึ้นอยู่กับค่าของแถวปัจจุบัน

และค่อนข้างโทรgenerate_series()ด้วยtimestampอินพุต:

แบบสอบถามที่คุณต้องการจริง

หลังจากอัปเดตคำถามและการสนทนา:
สะสมแถวเดียวกันentity_idในหน้าต่าง 30 วันโดยเริ่มจากแต่ละธุรกรรมจริง

เนื่องจากข้อมูลของคุณมีการกระจายอย่างเบาบางจึงควรมีประสิทธิภาพมากขึ้นในการรันการเข้าร่วมด้วยตัวเองกับเงื่อนไขช่วงดังนั้นยิ่ง Postgres 9.1 ยังไม่ได้LATERALเข้าร่วม:

SELECT t0.id, t0.amount, t0.trans_date, t0.entity_id
     , sum(t1.amount) AS trans_total, count(*) AS trans_count
FROM   transactiondb t0
JOIN   transactiondb t1 USING (entity_id)
WHERE  t1.trans_date >= t0.trans_date
AND    t1.trans_date <  t0.trans_date + 30  -- exclude upper bound
-- AND    t0.entity_id = 114284  -- or pick a single entity ...
GROUP  BY t0.id  -- is PK!
ORDER  BY t0.trans_date, t0.id

ซอ Fiddle

หน้าต่างการหมุนสามารถใช้งานได้ (กับประสิทธิภาพ) ด้วยข้อมูลเกือบทุกวัน

สิ่งนี้ไม่ได้รวมซ้ำกันใน(trans_date, entity_id)วัน แต่แถวทั้งหมดของวันเดียวกันจะรวมอยู่ในหน้าต่าง 30 วันเสมอ

สำหรับตารางขนาดใหญ่ดัชนีครอบคลุมเช่นนี้อาจช่วยได้ค่อนข้าง:

CREATE INDEX transactiondb_foo_idx
ON transactiondb (entity_id, trans_date, amount);

คอลัมน์สุดท้ายamountมีประโยชน์เฉพาะเมื่อคุณสแกนดัชนีอย่างเดียวเท่านั้น อื่นลดลง

แต่มันจะไม่ถูกนำไปใช้ในขณะที่คุณเลือกทั้งตารางต่อไป มันจะสนับสนุนการค้นหาสำหรับชุดย่อยขนาดเล็ก


ลักษณะนี้ดีจริงๆการทดสอบบนข้อมูลในขณะนี้และพยายามที่จะเข้าใจทุกอย่างแบบสอบถามของคุณจะทำจริง ...
tufelkinder

@tufelkinder: เพิ่มโซลูชันสำหรับคำถามที่อัปเดต
Erwin Brandstetter

ตรวจสอบทันที ฉันรู้สึกทึ่งที่มันทำงานใน SQL Fiddle ... เมื่อฉันพยายามเรียกใช้โดยตรงกับ transactiondb ของฉันมันมีข้อผิดพลาดเกิดขึ้นด้วยcolumn "t0.amount" must appear in the GROUP BY clause...
tufelkinder

@tufelkinder: ฉันตัดกรณีทดสอบลงเหลือ 100 แถว sqlfiddle จำกัด ขนาดของข้อมูลทดสอบ Jake (ผู้เขียน) ลดขีด จำกัด ลงเมื่อสองสามเดือนที่ผ่านมาดังนั้นไซต์จึงหยุดทำงานได้ง่ายกว่า
Erwin Brandstetter

1
ขออภัยสำหรับความล่าช้าที่จำเป็นในการทดสอบในฐานข้อมูลเต็มรูปแบบ คำตอบของคุณคือการศึกษาเชิงลึกและยอดเยี่ยมเช่นเคย ขอขอบคุณ!
tufelkinder
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.