15 พ.ย. 2568·อ่าน 2 นาที

ตารางเวลาที่เกิดซ้ำและโซนเวลาใน PostgreSQL: รูปแบบที่ควรรู้

เรียนรู้การจัดการตารางซ้ำและโซนเวลาใน PostgreSQL พร้อมรูปแบบการเก็บข้อมูลจริง กฎการเกิดซ้ำ ข้อยกเว้น และรูปแบบคิวรีที่ทำให้ปฏิทินถูกต้อง

ตารางเวลาที่เกิดซ้ำและโซนเวลาใน PostgreSQL: รูปแบบที่ควรรู้

ทำไมโซนเวลาและเหตุการณ์ที่เกิดซ้ำถึงพัง

ข้อผิดพลาดของปฏิทินส่วนใหญ่ไม่ใช่ข้อผิดพลาดทางคณิตศาสตร์ แต่เป็นข้อผิดพลาดด้านความหมาย คุณเก็บข้อมูลบางอย่างไว้ (ช่วงเวลาจริงหนึ่งจุด) แต่ผู้ใช้คาดหวังอีกอย่าง (เวลาในนาฬิกาท้องถิ่นในสถานที่เฉพาะ) ช่องว่างนี้คือสาเหตุที่ตารางเวลาซ้ำและโซนเวลาดูเหมือนถูกต้องในการทดสอบ แต่พอมีผู้ใช้จริงเข้ามาปัญหาก็เกิดขึ้น

Daylight Saving Time (DST) เป็นตัวกระตุ้นคลาสสิก การเปลี่ยนแปลงที่เป็น “ทุกวันอาทิตย์เวลา 09:00” ไม่เหมือนกับ “ทุก 7 วันจาก timestamp เริ่มต้น” เมื่อออฟเซ็ตเปลี่ยนสองแนวคิดนั้นจะเบี่ยงกันเป็นหนึ่งชั่วโมงและปฏิทินของคุณจะผิดโดยไม่รู้ตัว

การเดินทางและการผสมโซนเวลาเพิ่มชั้นปัญหาอีกชั้น การจองอาจผูกกับสถานที่ทางกายภาพ (เก้าอี้ร้านเสริมสวยใน Chicago) ขณะที่คนที่ดูอาจอยู่ London หากคุณปฏิบัติตารางที่ผูกกับสถานที่ราวกับว่าผูกกับบุคคล คุณจะแสดงเวลาท้องถิ่นผิดให้ฝ่ายใดฝ่ายหนึ่งอย่างแน่นอน

รูปแบบความล้มเหลวที่พบบ่อย:

  • คุณสร้าง recurrence โดยการบวก interval กับ timestamp ที่เก็บไว้ แล้ว DST เปลี่ยน
  • คุณเก็บ “เวลาท้องถิ่น” โดยไม่เก็บกฎโซน ทำให้ไม่สามารถสร้างช่วงเวลาที่ตั้งใจได้ในภายหลัง
  • คุณทดสอบแค่วันที่ที่ไม่ข้ามขอบเขต DST
  • คุณผสม “event time zone”, “user time zone” และ “server time zone” ในคิวรีเดียว

ก่อนเลือกสคีมา ให้ตัดสินใจก่อนว่า “ถูกรึเปล่า” หมายถึงอะไรสำหรับผลิตภัณฑ์ของคุณ

สำหรับการจอง คำว่า “ถูกต้อง” มักหมายถึง: การนัดหมายเกิดขึ้นตามเวลาในนาฬิกาที่ตั้งใจไว้ในโซนเวลาของสถานที่ และทุกคนที่ดูจะได้รับการแปลงเวลาที่ถูกต้อง

สำหรับกะงาน คำว่า “ถูกต้อง” มักหมายถึง: กะเริ่มตรงเวลาท้องถิ่นคงที่ของร้าน ถึงแม้พนักงานจะเดินทางไปที่อื่น

การตัดสินใจเดียวนี้ (ผูกตารางกับสถานที่ vs ผูกกับบุคคล) จะกำหนดทุกอย่าง: คุณจะเก็บอะไรอย่างไร วิธีสร้าง recurrence และวิธีคิวรีมุมมองปฏิทินโดยไม่มีเซอร์ไพรส์ชั่วโมงเดียว

เลือกรูปแบบคิดที่ถูกต้อง: instant vs local time

บัคหลายอย่างมาจากการผสมสองแนวคิดของเวลา:

  • Instant: ช่วงเวลาสัมบูรณ์ที่เกิดขึ้นครั้งเดียว
  • Local time rule: เวลาบนผนังนาฬิกา เช่น “ทุกวันจันทร์เวลา 9:00 AM ใน Paris”

Instant เหมือนกันทุกที่ “2026-03-10 14:00 UTC” เป็น instant ปกติจะใช้กับการโทรวิดีโอ, การออกเดินทางของเที่ยวบิน และ “ส่งการแจ้งเตือนในเวลานี้ตรงๆ”

Local time คือสิ่งที่คนอ่านบนนาฬิกาในสถานที่หนึ่ง “9:00 AM ใน Europe/Paris ทุกวันทำงาน” เป็น local time ชั่วโมงเปิดร้าน, คลาสที่เกิดซ้ำ และกะพนักงานมักยึดติดกับโซนเวลาของสถานที่ เวลานั้นเป็นส่วนของความหมาย ไม่ใช่แค่การแสดงผล

กฎง่ายๆ ในการตัดสินใจ:

  • เก็บ start/end เป็น instant เมื่อเหตุการณ์ต้องเกิดตรงช่วงเวลาจริงเดียวทั่วโลก
  • เก็บ local date และ local time พร้อมกับ zone ID เมื่อเหตุการณ์จะต้องตามนาฬิกาในที่เดียว
  • ถ้าผู้ใช้เดินทาง ให้แสดงเวลาตามโซนของผู้ดู แต่เก็บตารางไว้ยึดกับโซนของสถานที่
  • อยา่ใส่โซนโดยเดาจากออฟเซ็ตเช่น "+02:00" ออฟเซ็ตไม่รวมกฎ DST

ตัวอย่าง: กะโรงพยาบาลคือ “จันทร์–ศุกร์ 09:00–17:00 America/New_York” ในสัปดาห์ที่เปลี่ยน DST กะยังเป็น 9 ถึง 5 ตามเวลาท้องถิ่น แม้ว่า instants ใน UTC จะเลื่อนไปหนึ่งชั่วโมง

ชนิดข้อมูลใน PostgreSQL ที่สำคัญ (และควรหลีกเลี่ยงอะไร)

บัคของปฏิทินส่วนใหญ่เริ่มจากชนิดคอลัมน์ผิด ข้อสำคัญคือแยกช่วงเวลาจริงออกจากความคาดหวังบนหน้าปัดนาฬิกา

ใช้ timestamptz สำหรับ instant จริง: การจอง, การเช็คอิน, การแจ้งเตือน และสิ่งที่คุณต้องเปรียบเทียบข้ามผู้ใช้หรือภูมิภาค PostgreSQL จะเก็บเป็นช่วงเวลาสัมบูรณ์และแปลงสำหรับการแสดงผล ดังนั้นการเรียงลำดับและตรวจสอบการทับซ้อนจะถูกต้องตามคาด

ใช้ timestamp without time zone สำหรับค่าเวลาท้องถิ่นที่ไม่ใช่ instant ด้วยตัวเอง เช่น “ทุกวันจันทร์เวลา 09:00” หรือ “ร้านเปิดเวลา 10:00” จับคู่กับตัวระบุโซน แล้วแปลงเป็น instant เมื่อสร้าง occurrence เท่านั้น

สำหรับรูปแบบการเกิดซ้ำ ชนิดพื้นฐานที่ช่วยได้:

  • date สำหรับข้อยกเว้นที่เป็นวัน (เช่นวันหยุด)
  • time สำหรับเวลาเริ่มต้นรายวัน
  • interval สำหรับระยะเวลา (เช่นกะ 6 ชั่วโมง)

เก็บโซนเวลาเป็นชื่อ IANA (เช่น America/New_York) ในคอลัมน์ text (หรือในตาราง lookup ขนาดเล็ก) ออฟเซ็ตเช่น -0500 ไม่พอเพราะไม่รวมกฎ DST

ชุดปฏิบัติสำหรับหลายแอป:

  • timestamptz สำหรับ start/end instants ของการนัดหมายที่จองไว้
  • date สำหรับวันข้อยกเว้น
  • time สำหรับเวลาเริ่มต้นท้องถิ่นที่เกิดซ้ำ
  • interval สำหรับระยะเวลา
  • text สำหรับไอดีโซน IANA

ตัวเลือกแบบข้อมูลสำหรับแอปการจองและกะงาน

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

ตัวเลือก A: เก็บทุก occurrence

แทรกหนึ่งแถวสำหรับแต่ละกะหรือการจอง (ขยายแล้ว) คิวรีง่ายและเข้าใจง่าย แลกกับการเขียนหนักและการอัปเดตมากเมื่อกฎเปลี่ยน

เหมาะเมื่อเหตุการณ์ส่วนใหญ่เป็นครั้งเดียว หรือเมื่อคุณสร้าง occurrence เฉพาะล่วงหน้าไม่นาน (ตัวอย่างเช่น 30 วันถัดไป)

ตัวเลือก B: เก็บเป็นกฎและขยายเมื่ออ่าน

เก็บกฎตาราง (เช่น “สัปดาห์ละวันจันทร์และพุธ 09:00 ใน America/New_York”) และสร้าง occurrence สำหรับช่วงที่ร้องขอเมื่อจำเป็น

ยืดหยุ่นและประหยัดที่เก็บ แต่คิวรีซับซ้อนขึ้น มุมมองแบบเดือนอาจช้าลงถ้าไม่ทำ cache

ตัวเลือก C: กฎบวก occurrence แคช (ไฮบริด)

เก็บกฎเป็นแหล่งความจริง และเก็บ occurrence ที่สร้างไว้สำหรับหน้าต่างเลื่อน (เช่น 60–90 วัน) เมื่อกฎเปลี่ยน ให้สร้างแคชใหม่

นี่เป็นค่าดีจริงสำหรับแอปกะ: มุมมองเดือนเร็ว แต่ยังมีที่เดียวแก้แพทเทิร์น

ตารางที่ใช้งานได้จริง:

  • schedule: owner/resource, time zone, local start time, duration, recurrence rule
  • occurrence: instances ขยายแล้วพร้อม start_at timestamptz, end_at timestamptz, และสถานะ
  • exception: ตัวชี้ว่า “ข้ามวันนี้” หรือ “วันนี้ต่างไป”
  • override: แก้ไขต่อ-occurrence เช่น เปลี่ยนเวลเริ่ม, สลับพนักงาน, ติดธงยกเลิก
  • (optional) schedule_cache_state: ช่วงล่าสุดที่สร้างไว้เพื่อรู้ว่าจะเติมถัดไปอย่างไร

สำหรับการคิวรีช่วงปฏิทิน ให้ทำดัชนีเพื่อ “โชว์ทุกอย่างในหน้าต่างนี้”:

  • บน occurrence: btree (resource_id, start_at) และบ่อยครั้ง btree (resource_id, end_at)
  • ถ้าคุณคิวรี “overlaps range” บ่อย: สร้าง tstzrange(start_at, end_at) และเพิ่ม gist index

แทนกฎการเกิดซ้ำโดยไม่ทำให้เปราะบาง

จัดการกะตามสถานที่
สร้างกะพนักงานและตารางตามสถานที่ที่ยังคงยึดกับโซนของสถานที่
ลอง AppMaster

ตารางเวลาที่เกิดซ้ำพังเมื่อกฎซับซ้อนเกินไป ยืดหยุ่นเกินไป หรือเก็บเป็น blob ที่ไม่สามารถคิวรีได้ รูปแบบกฎที่ดีคือรูปแบบที่แอปคุณตรวจสอบได้และทีมอธิบายได้อย่างรวดเร็ว

สองแนวทางที่พบบ่อย:

  • ฟิลด์แบบกำหนดเองเรียบง่ายสำหรับแพทเทิร์นที่คุณรองรับจริง (กะรายสัปดาห์, วันที่เรียกเก็บรายเดือน)
  • กฎแบบ iCalendar (RRULE-style) เมื่อคุณต้อง import/export ปฏิทินหรือรองรับหลายการผสม

ทางสายกลางที่ใช้ได้: อนุญาตชุดตัวเลือกจำกัด เก็บเป็นคอลัมน์ และถือว่า RRULE เป็นเพียงรูปแบบแลกเปลี่ยนเท่านั้น

ตัวอย่าง กฎสัปดาห์สามารถแสดงด้วยฟิลด์เช่น:

  • freq (daily/weekly/monthly) และ interval (ทุก N)
  • byweekday (อาร์เรย์ของ 0-6 หรือ bitmask)
  • bymonthday (1-31) ตัวเลือกสำหรับกฎรายเดือน
  • starts_at_local (local date+time ที่ผู้ใช้เลือก) และ tzid
  • until_date หรือ count (หลีกเลี่ยงการรองรับทั้งสองถ้าไม่จำเป็น)

สำหรับขอบเขต ชอบเก็บ duration (เช่น 8 ชั่วโมง) แทนการเก็บ end timestamp สำหรับทุก occurrence ระยะเวลาคงที่เมื่อชั่วโมงเปลี่ยน คุณยังคำนวณ end ต่อ occurrence ได้: occurrence start + duration

เมื่อขยายกฎ ให้ปลอดภัยและจำกัด:

  • ขยายเฉพาะภายใน window_start และ window_end
  • เพิ่มบัฟเฟอร์เล็กๆ (เช่น 1 วัน) สำหรับเหตุการณ์ข้ามคืน
  • หยุดหลังจำนวน instance สูงสุด (เช่น 500)
  • กรองผู้สมัครก่อน (โดย tzid, freq, และ start date) ก่อนสร้างจริง

ขั้นตอนทีละขั้น: สร้างตารางเวลาที่ปลอดภัยจาก DST

สร้าง API สำหรับเหตุการณ์ที่เกิดซ้ำ
เปลี่ยนกฎการเกิดซ้ำให้เป็น endpoints จริงโดยไม่ต้องเขียนคิวรีทั้งหมดด้วยมือ
เริ่มสร้าง

รูปแบบที่เชื่อถือได้คือ: ถือแต่ละ occurrence เป็นแนวคิดปฏิทินท้องถิ่นก่อน (วันที่ + เวลาในท้องถิ่น + โซนของสถานที่), แล้วแปลงเป็น instant เมื่อต้องเรียงลำดับ, ตรวจสอบข้อขัดแย้ง, หรือแสดงผลเท่านั้น

1) เก็บความตั้งใจเป็นท้องถิ่น ไม่ใช่เดาการเป็น UTC

บันทึกโซนเวลาของตาราง (ชื่อ IANA เช่น America/New_York) พร้อมเวลาเริ่มต้นท้องถิ่น (เช่น 09:00) เวลาท้องถิ่นนี้คือสิ่งที่ธุรกิจต้องการ แม้ DST จะเปลี่ยน

ยังเก็บระยะเวลาและขอบเขตที่ชัดเจนสำหรับกฎ: วันที่เริ่ม และทั้ง end date หรือ repeat count ขอบเขตป้องกันบัคการขยายไม่รู้จบ

2) แยกข้อยกเว้นและ overrides ออกมาต่างหาก

ใช้สองตารางเล็ก ๆ: หนึ่งสำหรับวันที่ข้าม, อีกหนึ่งสำหรับ occurrence ที่เปลี่ยนแปลง จับคีย์ด้วย schedule_id + local_date เพื่อจับคู่ recurrence ต้นฉบับอย่างสะอาด

รูปร่างที่ใช้งานได้จริงดูเหมือน:

-- core schedule
-- tz is the location time zone
-- start_time is local wall-clock time
schedule(id, tz text, start_date date, end_date date, start_time time, duration_mins int, by_dow int[])

schedule_skip(schedule_id, local_date date)

schedule_override(schedule_id, local_date date, new_start_time time, new_duration_mins int)

3) ขยายเฉพาะในหน้าต่างที่ร้องขอ

สร้างวันที่ท้องถิ่นผู้สมัครสำหรับช่วงที่คุณกำลังเรนเดอร์ (สัปดาห์, เดือน) กรองตามวันในสัปดาห์ แล้วใช้ skips และ overrides

WITH days AS (
  SELECT d::date AS local_date
  FROM generate_series($1::date, $2::date, interval '1 day') d
), base AS (
  SELECT s.id, s.tz, days.local_date,
         make_timestamp(extract(year from days.local_date)::int,
                        extract(month from days.local_date)::int,
                        extract(day from days.local_date)::int,
                        extract(hour from s.start_time)::int,
                        extract(minute from s.start_time)::int, 0) AS local_start
  FROM schedule s
  JOIN days ON days.local_date BETWEEN s.start_date AND s.end_date
  WHERE extract(dow from days.local_date)::int = ANY (s.by_dow)
)
SELECT b.id,
       (b.local_start AT TIME ZONE b.tz) AS start_utc
FROM base b
LEFT JOIN schedule_skip sk
  ON sk.schedule_id = b.id AND sk.local_date = b.local_date
WHERE sk.schedule_id IS NULL;

4) แปลงสำหรับผู้ดูเมื่อที่สุดท้ายเท่านั้น

เก็บ start_utc เป็น timestamptz สำหรับการเรียงลำดับ, ตรวจสอบข้อขัดแย้ง, และการจอง แปลงเป็นโซนของผู้ดูตอนแสดงผลเท่านั้น วิธีนี้หลีกเลี่ยงความประหลาดใจจาก DST และทำให้มุมมองปฏิทินสม่ำเสมอ

รูปแบบคิวรีเพื่อสร้างมุมมองปฏิทินที่ถูกต้อง

หน้าจอปฏิทินโดยทั่วไปเป็นคิวรีช่วง: “โชว์ทุกอย่างระหว่าง from_ts และ to_ts” รูปแบบที่ปลอดภัยคือ:

  1. ขยายเฉพาะผู้สมัครในหน้าต่างนั้น
  2. ใช้ข้อยกเว้น/overrides
  3. เอาแถวสุดท้ายออกมาเป็น start_at และ end_at ในรูป timestamptz

การขยายรายวันหรือรายสัปดาห์ด้วย generate_series

สำหรับกฎสัปดาห์ง่ายๆ (เช่น “ทุกจันทร์–ศุกร์ 09:00 ท้องถิ่น”) ให้สร้างวันที่ท้องถิ่นในโซนของตาราง แล้วแปลงแต่ละวันที่ท้องถิ่น + เวลาเป็น instant

-- Inputs: :from_ts, :to_ts are timestamptz
-- rule.tz is an IANA zone like 'America/New_York'
WITH bounds AS (
  SELECT
    (:from_ts AT TIME ZONE rule.tz)::date AS from_local_date,
    (:to_ts   AT TIME ZONE rule.tz)::date AS to_local_date
  FROM rule
  WHERE rule.id = :rule_id
), days AS (
  SELECT d::date AS local_date
  FROM bounds, generate_series(from_local_date, to_local_date, interval '1 day') AS g(d)
)
SELECT
  (local_date + rule.start_local_time) AT TIME ZONE rule.tz AS start_at,
  (local_date + rule.end_local_time)   AT TIME ZONE rule.tz AS end_at
FROM rule
JOIN days ON true
WHERE EXTRACT(ISODOW FROM local_date) = ANY(rule.by_isodow);

วิธีนี้ดีเพราะการแปลงเป็น timestamptz เกิดขึ้นต่อ occurrence ดังนั้นการเปลี่ยน DST จะถูกใช้ในวันที่ถูกต้อง

กฎซับซ้อนขึ้นด้วย recursive CTE

เมื่อกฎขึ้นกับ “nth weekday”, ช่องว่าง, หรือ interval แบบกำหนดเอง, recursive CTE สามารถสร้าง occurrence ถัดไปซ้ำ ๆ จนกว่าจะผ่าน to_ts ให้ยึดการเรียกซ้ำไว้ในหน้าต่างเพราะจะไม่รันไม่รู้จบ

หลังจากได้แถวผู้สมัคร ใช้ overrides และ cancellations โดยการ join ตารางข้อยกเว้นบน (rule_id, start_at) หรือคีย์ท้องถิ่นเช่น (rule_id, local_date) ถ้ามีบันทึกยกเลิก ให้ตัดแถวออก ถ้ามี override ให้แทนที่ start_at/end_at ด้วยค่าจาก override

รูปแบบประสิทธิภาพที่สำคัญที่สุด:

  • จำกัดช่วงแต่เนิ่นๆ: กรองกฎก่อน แล้วค่อยขยายเฉพาะภายใน [from_ts, to_ts)
  • ทำดัชนีตาราง exception/override บน (rule_id, start_at) หรือ (rule_id, local_date)
  • หลีกเลี่ยงการขยายหลายปีสำหรับมุมมองเดือน
  • แคช occurrence ที่ขยายเฉพาะถ้าคุณสามารถ invalidate ได้อย่างสะอาดเมื่อกฎเปลี่ยน

จัดการข้อยกเว้นและ overrides อย่างสะอาด

ออกแบบตารางปฏิทินของคุณ
ออกแบบตารางเวลา, occurrence, และ overrides ในสคีมาที่สะอาดด้วย Data Designer
สร้างโปรเจกต์

ตารางซ้ำมีประโยชน์ก็ต่อเมื่อคุณสามารถทำลายมันได้อย่างปลอดภัย ในแอปการจองและกะงาน “สัปดาห์ปกติ” เป็นกฎฐาน และทุกอย่างอื่นคือข้อยกเว้น: วันหยุด, ยกเลิก, การย้ายการนัดหมาย, หรือการสลับพนักงาน ถ้าข้อยกเว้นถูกเพิ่มทีหลัง มุมมองปฏิทินจะเบี้ยวและเกิดข้อมูลซ้ำ

เก็บสามแนวคิดแยกกัน:

  • สคีมาฐาน (กฎการเกิดซ้ำและโซนเวลา)
  • Skips (วันที่หรือตัวอย่างที่ต้องไม่เกิด)
  • Overrides (occurrence ที่มีรายละเอียดเปลี่ยน)

ใช้ลำดับความสำคัญคงที่

เลือกลำดับหนึ่งและรักษาให้สม่ำเสมอ ตัวเลือกที่พบบ่อย:

  1. สร้างผู้สมัครจาก recurrence พื้นฐาน
  2. ใช้ overrides (แทนที่รายการที่สร้าง)
  3. ใช้ skips (ซ่อนมัน)

ทำให้กฎอธิบายง่ายในประโยคเดียวสำหรับผู้ใช้

หลีกเลี่ยงข้อมูลซ้ำเมื่อ override แทนที่ instance

ข้อมูลซ้ำมักเกิดเมื่อคิวรีส่งทั้ง occurrence ที่สร้างและแถว override ป้องกันโดยใช้คีย์คงที่:

  • ให้แต่ละ instance ที่สร้างมีคีย์คงที่ เช่น (schedule_id, local_date, start_time, tzid)
  • เก็บคีย์นั้นในแถว override เป็น “original occurrence key”
  • เพิ่ม unique constraint เพื่อให้มี override ต่อ occurrence ต้นฉบับแค่หนึ่งรายการ

จากนั้นในคิวรี ให้ยกเว้น occurrence ที่สร้างแล้วที่มี override ตรงกัน และ union กับแถว override แทน

เก็บการตรวจสอบย้อนหลังโดยไม่สะดุด

ข้อยกเว้นคือที่ที่มีข้อพิพาทเกิดขึ้น (“ใครแก้กะฉัน?”) ใส่ฟิลด์ audit พื้นฐานบน skips และ overrides: created_by, created_at, updated_by, updated_at, และเหตุผลโดยย่อได้

ความผิดพลาดที่พบบ่อยซึ่งทำให้บัคเลื่อนหนึ่งชั่วโมง

บัคหนึ่งชั่วโมงมักเกิดจากการผสมความหมายของเวลา: instant (จุดบนเส้นเวลา UTC) กับการอ่านนาฬิกาท้องถิ่น (เช่น 09:00 ทุกวันจันทร์ใน New York)

ความผิดพลาดคลาสสิกคือเก็บกฎเวลาท้องถิ่นเป็น timestamptz ถ้าคุณบันทึก “จันทร์เวลา 09:00 America/New_York” เป็น timestamptz หนึ่งค่า คุณได้เลือกวันที่เฉพาะ (และสถานะ DST) แล้ว ต่อมาพอจะสร้างจันทร์ถัดไป ความตั้งใจเดิมว่า “เสมอ 09:00 ท้องถิ่น” หายไป

สาเหตุอีกประการคือพึ่งพาออฟเซ็ตคงที่เช่น -05:00 แทนชื่อโซน IANA ออฟเซ็ตไม่รวมกฎ DST เก็บไอดีโซน (เช่น America/New_York) และให้ PostgreSQL ใช้กฎที่ถูกต้องในแต่ละวัน

ระวังช่วงเวลาที่แปลง หากคุณแปลงเป็น UTC เร็วเกินไปในขณะสร้าง recurrence คุณอาจตรึงออฟเซ็ต DST และนำไปใช้กับทุก occurrence รูปแบบที่ปลอดภัยคือ: สร้าง occurrence ในเงื่อนไขท้องถิ่น (date + local time + zone) แล้วแปลงแต่ละ occurrence เป็น instant

ข้อผิดพลาดที่ปรากฏบ่อย:

  • ใช้ timestamptz เพื่อเก็บเวลาในวันท้องถิ่นที่เกิดซ้ำ (ควรใช้ time + tzid + กฎ)
  • เก็บแต่ออฟเซ็ต ไม่ใช่ชื่อโซน IANA
  • แปลงขณะที่กำลังสร้าง recurrence แทนที่จะทำทีหลัง
  • ขยาย recurrence “ตลอดไป” โดยไม่มีหน้าต่างเวลาจริง
  • ไม่ทดสอบสัปดาห์เริ่ม/สิ้นสุด DST

การทดสอบง่าย ๆ ที่จับปัญหาส่วนใหญ่: เลือกโซนที่มี DST, สร้างกะรายสัปดาห์เวลา 09:00, และเรนเดอร์ปฏิทินสองเดือนที่ข้ามการเปลี่ยน DST ยืนยันว่าแต่ละ instance แสดงเป็น 09:00 ท้องถิ่น แม้ instants ในพื้นฐานจะแตกต่างกัน

รายการตรวจสอบด่วนก่อนปล่อย

ทำให้การจัดตารางเป็นแบบ end to end
เชื่อมต่อการยืนยันตัวตน, การแจ้งเตือน และเวิร์กโฟลว์การจัดตารางในแอปพร้อมผลิต
เริ่มเลย

ก่อนปล่อย ตรวจสอบพื้นฐาน:

  • ทุกสคีมาผูกกับสถานที่ (หรือหน่วยธุรกิจ) ที่มีชื่อโซนเวลา เก็บไว้ในสคีมาเอง
  • เก็บไอดีโซน IANA (เช่น America/New_York) ไม่ใช่ออฟเซ็ตดิบ
  • การขยาย recurrence สร้าง occurrence เฉพาะภายในช่วงที่ร้องขอ
  • ข้อยกเว้นและ overrides มีลำดับความสำคัญที่อธิบายได้ชัดเจน
  • ทดสอบสัปดาห์เปลี่ยน DST และผู้ดูในโซนเวลาต่างจากตาราง

ทำ dry run ที่เป็นจริง: ร้านใน Europe/Berlin มีกะรายสัปดาห์เวลา 09:00 ท้องถิ่น ผู้จัดการดูจาก America/Los_Angeles ยืนยันว่ากะยังคงเป็น 09:00 Berlin ทุกสัปดาห์ แม้แต่ละภูมิภาคจะเปลี่ยน DST ในวันต่างกัน

ตัวอย่าง: กะพนักงานรายสัปดาห์มีวันหยุดและการเปลี่ยน DST

ทำซ้ำกฎเวลาอย่างปลอดภัย
สร้างสัญญาเวลาแบบต้นแบบอย่างรวดเร็ว แล้วทำซ้ำโดยไม่ก่อหนี้ทางเทคนิคที่ยุ่งยาก
เริ่มใช้งาน

คลินิกขนาดเล็กมีหนึ่งกะซ้ำ: ทุกวันจันทร์ 09:00–17:00 ในโซนท้องถิ่นของคลินิก (America/New_York) คลินิกปิดวันหยุดหนึ่งวันจันทร์ พนักงานคนหนึ่งเดินทางในยุโรปสองสัปดาห์ แต่ตารางของคลินิกต้องยึดกับเวลาบนผนังของคลินิก ไม่ใช่ตำแหน่งของพนักงานขณะนั้น

ทำให้สิ่งนี้ทำงานถูกต้องได้โดย:

  • เก็บกฎ recurrence ยึดกับวันที่ท้องถิ่น (weekday = Monday, เวลาในท้องถิ่น = 09:00–17:00)
  • เก็บโซนเวลาของสคีมา (America/New_York)
  • เก็บวันที่เริ่มใช้งานเพื่อให้กฎมีจุดยึดชัดเจน
  • เก็บข้อยกเว้นเพื่อยกเลิกวันหยุดจันทร์นั้น (และ overrides สำหรับการเปลี่ยนแปลงครั้งเดียว)

ตอนนี้เรนเดอร์ช่วงปฏิทินสองสัปดาห์ที่ครอบคลุมการเปลี่ยน DST ใน New York คิวรีจะสร้างวันจันทร์ในช่วงวันที่ท้องถิ่นนั้น แนบเวลาท้องถิ่นของคลินิก แล้วแปลงแต่ละ occurrence เป็น instant (timestamptz) เพราะการแปลงเกิดขึ้นต่อ occurrence DST จึงถูกจัดการถูกต้องในวันนั้น

ผู้ดูต่างคนต่างเห็นเวลาในนาฬิกาที่ต่างกันสำหรับ instant เดียวกัน:

  • ผู้จัดการใน Los Angeles จะเห็นเวลาเร็วกว่าบนหน้าปัด
  • พนักงานที่เดินทางใน Berlin จะเห็นเวลาช้ากว่า

แต่คลินิกได้สิ่งที่ต้องการ: 09:00–17:00 New York ทุกวันจันทร์ที่ไม่ได้ยกเลิก

ขั้นตอนถัดไป: นำไปใช้, ทดสอบ, และรักษาความเรียบร้อย

นิยามแนวทางเรื่องเวลาให้ชัดเจนตั้งแต่ต้น: คุณจะเก็บเฉพาะกฎ, เก็บเฉพาะ occurrence, หรือไฮบริด? สำหรับหลายผลิตภัณฑ์การจองและกะงาน ไฮบริดใช้งานได้ดี: เก็บกฎเป็นแหล่งความจริง, เก็บแคชเลื่อนถ้าจำเป็น, และเก็บข้อยกเว้น/overrides เป็นแถวจริง

เขียน "สัญญาเวลา" ของคุณไว้ในที่เดียว: อะไรนับเป็น instant, อะไรนับเป็น local wall time, และคอลัมน์ไหนเก็บแต่ละอย่าง นี่จะป้องกันเมื่อจุดหนึ่งส่งคืน local time ขณะที่อีกจุดส่งคืน UTC

เก็บการสร้าง recurrence ในโมดูลเดียว ไม่กระจัดกระจายเป็นชิ้น SQL ถ้าต้องเปลี่ยนการตีความ "9:00 AM local" คุณจะมีที่เดียวที่ต้องแก้

ถ้าคุณกำลังสร้างเครื่องมือการจัดตารางโดยไม่เขียนทุกอย่างด้วยมือ AppMaster (appmaster.io) เป็นทางเลือกที่ใช้งานได้สำหรับงานประเภทนี้: คุณสามารถออกแบบฐานข้อมูลใน Data Designer, สร้างตรรกะ recurrence และข้อยกเว้นใน business processes, และยังได้ backend และโค้ดแอปจริง

ง่ายต่อการเริ่มต้น
สร้างบางสิ่งที่ น่าทึ่ง

ทดลองกับ AppMaster ด้วยแผนฟรี
เมื่อคุณพร้อม คุณสามารถเลือกการสมัครที่เหมาะสมได้

เริ่ม