13 คะแนน โดย GN⁺ 2024-07-25 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ใช้ฟีเจอร์กำหนดการของโคลน Google Calendar เป็นตัวอย่าง แสดงลำดับการออกแบบที่ไม่เริ่มจากการสร้างตารางทันที แต่ทำ โมเดลเชิงตรรกะ ให้สมบูรณ์ก่อน แล้วค่อยแปลงเป็น SQL schema
  • วิธีหลักคือหา anchor เช่น User, DayEvent, TimeEvent, Timezone ก่อน จากนั้นตรวจสอบ requirements และ cardinality ด้วย attribute และลิงก์แบบ 1:N·M:N
  • กำหนดการแบ่งเป็นอีเวนต์ทั้งวันและอีเวนต์แบบอิงเวลา โดยอีเวนต์แบบอิงเวลาจะบันทึก เวลา local ของเวลาเริ่มต้น·สิ้นสุด รวมถึง Timezone ของแต่ละรายการ เพื่อรองรับความเป็นไปได้ที่เขตเวลาจะเปลี่ยนในอนาคต
  • กำหนดการที่เกิดซ้ำแสดงด้วยความถี่ ช่วงห่าง รูปแบบการเกิดซ้ำรายเดือน เงื่อนไขสิ้นสุด และการเชื่อมโยงกับวันในสัปดาห์ พร้อมแยก DaySlot และ TimeSlot เป็น anchor ต่างหากเพื่อใช้ในการ render หน้าจอและแก้ไข instance รายตัว
  • การออกแบบเชิงกายภาพขั้นสุดท้ายยึดหลัก “หนึ่งตารางต่อหนึ่ง anchor” แต่ไม่สร้าง DayOfTheWeek เป็นตาราง จึงสรุปเป็น ตาราง SQL ทั้งหมด 8 ตาราง ได้แก่ตาราง anchor 6 ตารางและตารางลิงก์ M:N 2 ตาราง

แนวทางการออกแบบ: สร้างโมเดลเชิงตรรกะก่อน

  • เป้าหมายคือแสดงกระบวนการ ออกแบบตารางฐานข้อมูล ที่ใกล้เคียงโปรเจกต์จริง สำหรับฟีเจอร์ปฏิทินของโคลน Google Calendar
  • ลำดับโดยรวมคือสร้าง โมเดลเชิงตรรกะที่สมบูรณ์ ซึ่งอธิบายข้อมูลปฏิทินที่ต้องจัดเก็บก่อน แล้วจึงย้ายโมเดลนั้นไปเป็นโครงสร้างตารางเชิงกายภาพ
  • ส่วนแรกกล่าวถึงโมเดลเชิงตรรกะที่ไม่ผูกกับฐานข้อมูลใดฐานข้อมูลหนึ่ง
    • โมเดล business requirements อย่างเป็นอิสระจาก MySQL, Postgres, เซิร์ฟเวอร์ relational แบบดั้งเดิม, NoSQL และฐานข้อมูลบนคลาวด์
  • ส่วนหลังแสดงแนวทางหนึ่งในการแปลงโมเดลเชิงตรรกะเป็นตารางเชิงกายภาพ
    • สำหรับระบบที่ requirements ไม่ได้ซับซ้อนมากเป็นพิเศษ สามารถมองว่าเป็นวิธีออกแบบที่ใช้ได้

ขอบเขตปัญหา: ฟีเจอร์กำหนดการหลักของ Google Calendar

  • ระบบเป้าหมายเป็นระบบ ผู้ใช้หลายคน และโมเดลข้อมูลเกี่ยวกับผู้ใช้ไว้เพียงขั้นต่ำ
  • อีเวนต์อาจมี attribute เช่น ชื่อ คำอธิบาย และสถานที่ แต่ส่วนที่ซับซ้อนที่สุดคือ เวลาและวันที่
  • อีเวนต์ที่นำมาโมเดลครอบคลุมความแตกต่างต่อไปนี้
    • อีเวนต์ทั้งวันและอีเวนต์แบบอิงเวลา
    • อีเวนต์เกิดซ้ำและอีเวนต์ไม่เกิดซ้ำ
    • อีเวนต์ทั้งวันที่คร่อมหลายวัน
    • อีเวนต์แบบอิงเวลาที่เชื่อมกับเขตเวลา และเวลาเริ่มต้น·สิ้นสุดอาจอยู่คนละวันที่หรือคนละเขตเวลา
  • อีเวนต์เกิดซ้ำครอบคลุมรูปแบบต่อไปนี้
    • เกิดซ้ำทุกวันหรือทุก N วัน
    • เกิดซ้ำทุกสัปดาห์ในวันใดวันหนึ่ง โดยรองรับช่วงห่างตั้งแต่ 2 สัปดาห์ขึ้นไป
    • เกิดซ้ำทุกเดือนตามวันที่ระบุ หรือตามวันในสัปดาห์ของสัปดาห์ลำดับที่ระบุ
    • เกิดซ้ำทุกปี
    • เกิดซ้ำตลอดไป จนถึงวันที่กำหนด หรือซ้ำตามจำนวนครั้งที่กำหนด
  • instance เฉพาะของอีเวนต์เกิดซ้ำสามารถย้ายไปเป็นวัน·เวลาอื่น หรือลบได้
  • ต้องสามารถแก้ไขตัวกำหนดการเกิดซ้ำเองได้ แม้บางอีเวนต์จะผ่านไปแล้ว

แสดง requirements ด้วย anchor, attribute และลิงก์

  • การโมเดลเริ่มจากการหา anchor ก่อน
    • anchor คือสิ่งที่นับได้และเพิ่มเข้าไปในฐานข้อมูลได้ เช่น User, Event
    • หากสามารถพูดได้อย่างเป็นธรรมชาติ เช่น “There are 200 Users in our database”, “When this button is clicked, a new DayEvent is created” ก็ถือเป็น anchor ได้
  • attribute ใช้จัดเก็บข้อมูลจริงเกี่ยวกับ anchor
    • User มี attribute อีเมล
    • DayEvent มีชื่อ วันที่เริ่มต้น และวันที่สิ้นสุด
    • แม้เป็นอีเวนต์วันเดียว ก็จัดเก็บวันที่เริ่มต้นและวันที่สิ้นสุดเป็นค่าเดียวกัน เพื่อให้จัดการด้วยโครงสร้างเดียวกับอีเวนต์หลายวัน
  • เมื่อ anchor สองตัวมีความสัมพันธ์กัน ให้ใช้ ลิงก์ ไม่ใช่ attribute
    • “User คนใดสร้าง DayEvent” ไม่ใช่ attribute เชิงตรรกะที่ใส่ User ID ลงใน attribute ของ DayEvent โดยตรง แต่เป็นลิงก์ระหว่าง User กับ DayEvent
    • ลิงก์ใช้ cardinality แบบ 1:N, M:N, 1:1 และตรวจสอบความหมายสองทิศทางด้วยประโยคสองประโยค
  • ระหว่าง User กับ DayEvent มีลิงก์แบบ 1:N
    • User สามารถสร้าง DayEvent ได้หลายรายการ
    • DayEvent ถูกสร้างโดย User หนึ่งคน

อีเวนต์แบบอิงเวลาและการจัดการ Timezone

  • เพิ่ม anchor Timezone และ TimeEvent สำหรับอีเวนต์แบบอิงเวลา
  • โมเดล Timezone ถูกทำให้น้อยที่สุดเพื่อวัตถุประสงค์ของบทเรียน โดยมีเพียงชื่อที่มนุษย์อ่านได้เป็น attribute
    • ค่าตัวอย่างคือ Europe/Kyiv
    • นิยามเขตเวลาจริงต้องมี UTC offset, การมีหรือไม่มี daylight saving time, จุดเริ่มต้น·สิ้นสุด DST, UTC offset เมื่อใช้ DST, นิยามในอดีต, สถานะใช้งาน·เลิกใช้ เป็นต้น แต่ยกไว้เป็นงานต่างหาก
  • TimeEvent มีชื่อ วันที่·เวลา local เริ่มต้น และวันที่·เวลา local สิ้นสุด
    • ค่าตัวอย่างคือ 2024-01-14 12:30, 2024-01-14 13:15
  • เวลาไม่ได้จัดเก็บเฉพาะ UTC แต่จัดเก็บเป็น เวลา local ที่ผู้ใช้ป้อน
    • ตัวอย่างเช่น เกมบิลเลียดวันที่ 6 กันยายน 2058 เวลา 09:30~11:00 ตามเวลา Cologne ยังไม่สามารถรู้ UTC offset ณ เวลานั้นได้ในปัจจุบัน จึงต้องจัดเก็บตามที่ผู้ใช้ป้อน
    • หากภายหลังกฎหมายท้องถิ่นเปลี่ยนไป ต้องสามารถปรับให้สอดคล้องได้
  • TimeEvent มีลิงก์ 1:N กับ Timezone แยกกันสำหรับเขตเวลาเริ่มต้นและเขตเวลาสิ้นสุด
    • เช่นตัวอย่างตั๋วเครื่องบิน เที่ยวบินจาก Amsterdam ไป London อาจมีเวลาออกเดินทางและเวลาถึงปลายทางอยู่คนละเขตเวลา
    • อีเวนต์แบบอิงเวลาส่วนใหญ่มีเขตเวลาเริ่มต้น·สิ้นสุดเหมือนกัน แต่กำหนดทั้งสองฝั่งเสมอเพื่อรองรับกรณีทั่วไป

การโมเดลอีเวนต์ทั้งวันที่เกิดซ้ำ

  • อีเวนต์ทั้งวันที่เกิดซ้ำแสดงด้วยการเพิ่ม attribute และลิงก์ที่เกี่ยวกับการเกิดซ้ำให้กับ DayEvent
  • ความถี่การเกิดซ้ำเป็น attribute แบบ either/or/or
    • ค่าที่เป็นไปได้คือ daily, weekly, monthly, annually
    • หากไม่ได้ตั้งค่า จะถือเป็นอีเวนต์ไม่เกิดซ้ำ
  • ช่วงห่างของการเกิดซ้ำเป็น attribute จำนวนเต็มที่มีความหมายเฉพาะ “กรณีเป็นอีเวนต์เกิดซ้ำ” เท่านั้น
    • ค่าตัวอย่างคือ 2
    • ใช้ในความหมาย เช่น ทุก 2 วัน ทุก 2 สัปดาห์ ทุก 2 เดือน
  • การเกิดซ้ำรายเดือนมี attribute สำหรับเลือกแยกต่างหาก
    • same_day คือเกิดซ้ำในวันที่เดียวกัน
    • same_weekday คือเกิดซ้ำในวันของสัปดาห์ที่มีลำดับเดียวกับวันที่เดิม
  • วันใดวันหนึ่งของสัปดาห์สำหรับการเกิดซ้ำรายสัปดาห์ไม่ได้จัดการเป็น attribute แบบอาร์เรย์สตริง แต่โมเดลด้วย anchor DayOfTheWeek และลิงก์ M:N
    • ตัวอย่าง ID ของ DayOfTheWeek คือ Mon, Tue, Wed, Thu, Fri, Sat, Sun
    • DayEvent สามารถเกิดได้ใน DayOfTheWeek หลายรายการ และ DayOfTheWeek สามารถมี DayEvent ได้หลายรายการ
  • เงื่อนไขสิ้นสุดการเกิดซ้ำก็จัดเป็น attribute
    • forever
    • until_date
    • N_repetitions
    • หากเป็น until_date ให้เพิ่ม attribute วันที่สิ้นสุด
    • หากเป็น N_repetitions ให้เพิ่ม attribute จำนวนครั้งที่เกิดซ้ำ

Slot สำหรับการ render และการแก้ไข instance รายตัว

  • หากจัดเก็บอีเวนต์เกิดซ้ำด้วยอีเวนต์ต้นฉบับเพียงรายการเดียว query สำหรับหาอีเวนต์ที่จะแสดงในหน้าสัปดาห์ใดสัปดาห์หนึ่งอาจซับซ้อน
  • เพื่อการ render หน้าจอและการแก้ไขราย instance ของอีเวนต์เกิดซ้ำ จึงนำแนวคิด Slot มาใช้
    • DaySlot แทน instance ของอีเวนต์ทั้งวันในวันที่ระบุ
    • TimeSlot แทน instance ของอีเวนต์แบบอิงเวลาในวันที่และเวลาที่ระบุ
  • DaySlot มีวันที่และสถานะว่าข้ามหรือไม่
    • วันที่ของ slot เฉพาะสามารถเปลี่ยนได้
    • หากยกเลิกการประชุมในสัปดาห์ใดสัปดาห์หนึ่ง สามารถทำเครื่องหมาย DaySlot นั้นเป็น skipped ได้
  • DayEvent กับ DaySlot มีลิงก์ 1:N
    • DayEvent หนึ่งรายการสามารถสร้าง DaySlot ได้หลายรายการ
    • DaySlot ตรงกับ DayEvent หนึ่งรายการ
    • แม้เป็น DayEvent ที่ไม่เกิดซ้ำ ก็สร้าง DaySlot เสมอเพื่อทำให้โค้ด render ง่ายขึ้น
  • อีเวนต์เกิดซ้ำไม่สิ้นสุดมีปัญหาเรื่องขอบเขตการสร้าง slot
    • อีเวนต์เช่นวันเกิดที่เกิดซ้ำทุกปีตลอดไป อาจสร้าง slot ไปจนถึงขอบเขตสมมติ เช่น 100 ปี
    • หรือสร้างแบบ on-demand เมื่อผู้ใช้ร้องขอหน้าปฏิทินในอนาคตไกลก็ได้
  • การคำนวณวันที่ต้องระวัง
    • วันเกิดวันที่ 29 กุมภาพันธ์
    • อีเวนต์รายเดือนที่เกิดทุกวันที่ 31
    • ในกรณีเหล่านี้ต้องตัดสินใจว่าจะห้ามใช้งาน หรือเลื่อนไปข้างหน้าหรือข้างหลังหนึ่งวัน

TimeSlot และเขตเวลา

  • สำหรับอีเวนต์แบบอิงเวลาที่เกิดซ้ำ ก็ใช้แนวทางเดียวกับอีเวนต์ทั้งวันโดยมี anchor TimeSlot
  • TimeSlot คือ instance ของอีเวนต์หนึ่งในวันที่·เวลาหนึ่ง และ TimeEvent ที่เกิดซ้ำจะสัมพันธ์กับ TimeSlot หลายรายการ
  • attribute ของ TimeSlot คือวันที่·เวลา local เริ่มต้น วันที่·เวลา local สิ้นสุด และสถานะว่าข้ามหรือไม่
  • TimeSlot เฉพาะสามารถย้ายไปวันอื่นได้ จึงต้องเก็บข้อมูลเวลาเริ่มต้น·สิ้นสุดแยกไว้
  • TimeSlot ก็มีลิงก์ 1:N กับ Timezone แยกกันสำหรับเขตเวลาเริ่มต้นและเขตเวลาสิ้นสุด
  • ระหว่าง TimeEvent กับ TimeSlot มีลิงก์ 1:N และสร้าง TimeSlot แม้สำหรับ TimeEvent ที่ไม่เกิดซ้ำ

โมเดลเชิงตรรกะที่สมบูรณ์

  • anchor ที่นิยามมาถึงตอนนี้มีทั้งหมด 7 รายการ
    • User
    • Timezone
    • DayEvent
    • TimeEvent
    • DayOfTheWeek
    • DaySlot
    • TimeSlot
  • attribute ประกอบด้วยอีเมลของ User, ชื่อของ Timezone, ชื่อ·วันที่เริ่มต้น·วันที่สิ้นสุด·ข้อมูลการเกิดซ้ำของ DayEvent, ชื่อ·เวลา local เริ่มต้น·สิ้นสุดของ TimeEvent, และเวลาที่เกิดขึ้นกับสถานะ skipped ของ DaySlot และ TimeSlot
  • ลิงก์สรุปได้ทั้งหมด 10 รายการ
    • User กับ DayEvent, User กับ TimeEvent
    • Timezone กับเขตเวลาเริ่มต้น·สิ้นสุดของ TimeEvent
    • DayEvent กับ DayOfTheWeek, TimeEvent กับ DayOfTheWeek
    • DayEvent กับ DaySlot, TimeEvent กับ TimeSlot
    • Timezone กับเขตเวลาเริ่มต้น·สิ้นสุดของ TimeSlot

ย้ายไปเป็นตาราง SQL

  • การออกแบบเชิงกายภาพใช้กลยุทธ์ หนึ่งตารางต่อหนึ่ง anchor เพื่อวัตถุประสงค์ด้านการศึกษา
  • โมเดลเชิงตรรกะมี anchor 7 รายการ, attribute 21 รายการ และลิงก์ 10 รายการ
  • โดยทั่วไปจะได้ 9 ตารางจากการนำจำนวน anchor มาบวกกับจำนวนลิงก์ M:N แต่ในการออกแบบขั้นสุดท้าย DayOfTheWeek ถูกจัดการเป็นกรณีพิเศษ จึงไม่สร้างตารางเชิงกายภาพ
  • ตาราง SQL ขั้นสุดท้ายมีทั้งหมด 8 ตาราง
    • users
    • timezones
    • day_events
    • time_events
    • day_slots
    • time_slots
    • day_event_dows
    • time_event_dows
  • ลิงก์ 1:N ถูกใส่เป็นคอลัมน์ลักษณะ foreign key ในตารางฝั่ง N
    • day_events.user_id
    • time_events.user_id
    • time_events.start_timezone_id
    • time_events.end_timezone_id
    • day_slots.day_event_id
    • time_slots.time_event_id
    • time_slots.start_timezone_id
    • time_slots.end_timezone_id
  • ลิงก์ M:N สร้างเป็นตารางแยก
    • day_event_dows เชื่อม DayEvent กับวันในสัปดาห์
    • time_event_dows เชื่อม TimeEvent กับวันในสัปดาห์
  • เลือก physical type ให้สอดคล้องกับ logical type
    • สตริงใช้ VARCHAR
    • วันที่ใช้ DATE
    • วันที่·เวลา local ใช้ DATETIME
    • yes/no ใช้ TINYINT UNSIGNED
    • attribute ที่เกี่ยวพันกันและมีความหมายเฉพาะตามเงื่อนไข ใช้ type ที่อนุญาต NULL
  • NULL และค่า sentinel ถือเป็นแนวคิดของ physical schema ไม่ใช่โมเดลเชิงตรรกะ

อินเด็กซ์และ attribute ที่ละไว้

  • ตัวอย่าง SQL schema ไม่มีอินเด็กซ์บางรายการที่นักพัฒนาฐานข้อมูลผู้มีประสบการณ์มองว่าจำเป็น
    • เช่น day_events.user_id อาจต้องมีอินเด็กซ์
  • การจะใส่อินเด็กซ์ที่คอลัมน์ใดหรือชุดคอลัมน์ใด ขึ้นอยู่กับว่าแอปพลิเคชัน query ตารางอย่างไร
  • สำหรับหัวข้ออินเด็กซ์ ดูได้ที่ Use The Index, Luke
  • ระหว่างอธิบาย logical schema มี attribute บางรายการที่ละไว้เพราะใช้แพตเทิร์นคล้ายกัน
    • ชื่อผู้ใช้
    • hash รหัสผ่านผู้ใช้
    • สถานที่ของอีเวนต์
    • รายชื่อผู้ได้รับเชิญ
  • องค์ประกอบข้อมูลที่ละไว้สามารถเพิ่มเข้าไปในตารางโมเดลเชิงตรรกะด้วยวิธีเดียวกัน แล้วสะท้อนใน SQL schema ได้

ขั้นตอนทั้งหมด

  • เริ่มจากข้อความรูปแบบอิสระที่อธิบายปัญหาทางธุรกิจ
  • จัดทำรายการ anchor
  • จัดทำรายการ attribute โดยเขียนความหมายของ attribute ให้ชัดเจนในรูปคำถาม
  • จัดทำรายการลิงก์ และตรวจสอบด้วยประโยคว่า cardinality ถูกต้องหรือไม่
  • หากจำเป็น ให้สร้าง schema แบบกราฟิกจากโมเดลเชิงตรรกะ
  • สร้างโมเดลเชิงกายภาพโดยเติมชื่อตาราง ชื่อคอลัมน์ และชนิดข้อมูลเชิงกายภาพ
  • ใช้ข้อมูลจากขั้นก่อนหน้าเขียน SQL schema เป็นคำสั่ง CREATE TABLE
  • ส่ง schema ไปยังเซิร์ฟเวอร์ฐานข้อมูล แก้ typo แล้วส่งอีกครั้ง
  • แชร์โมเดลเชิงตรรกะกับทีม

1 ความคิดเห็น

 
GN⁺ 2024-07-25
ความคิดเห็นจาก Hacker News
  • ถ้าซีเรียลไลซ์เหตุการณ์แบบสุ่มหนึ่งรายการในปฏิทินของผมเป็น iCalendar จะมีขนาดราว 740 ไบต์ และแม้แต่ปฏิทินที่ยุ่งสุดขั้วซึ่งมีเหตุการณ์ทุก ๆ 15 นาทีระหว่าง 9 โมงเช้าถึง 5 โมงเย็น ตลอดทั้งปีก็มีแค่ 11,680 รายการ หรือราว 8MB เท่านั้น
    ดังนั้นแทนที่จะออกแบบสคีมา ปัญหานี้ดูเหมือนปฏิทินอาจเหมาะกับการทำเป็น การสแกนตามลำดับ มากกว่ามาก ถ้าปรับ iCalendar parser ให้กวาดช่วงเหตุการณ์ที่ dump ออกมาได้ด้วย throughput ระดับ GB/s กรณีเลวร้ายข้างต้นก็สแกนได้ภายในไม่กี่มิลลิวินาที
    การปรับ parser ให้เหมาะสมเป็นปัญหาที่ง่ายกว่ามากและแก้ครั้งเดียวจบ เมื่อเทียบกับการเปลี่ยนหรือขยายโมเดลข้อมูลที่ไม่ดีหลังจากแพร่ไปยังผู้ใช้จำนวนมากแล้ว และยังไงงานแรกของโมเดลข้อมูลใหม่ก็น่าจะเป็นการ import/export iCalendar อยู่ดี จึงดูเป็นการประนีประนอมที่ค่อนข้างดี

    • ถ้าอย่างนั้นก็ต้องสร้าง การค้นหาแบบช่วง อย่าง BETWEEN ... AND ... ของ SQL ขึ้นมาใหม่ไม่ใช่หรือ? การหาเหตุการณ์ของผู้ใช้ 1, 2, 3 ก็เหมือนกัน
      ถ้าเป็นแอปพลิเคชันจริง ก็น่าจะมีสิ่งอย่างบัญชีผู้ใช้อยู่ในฐานข้อมูลเชิงสัมพันธ์อยู่แล้ว แต่จู่ ๆ ก็ต้องสแกนเหตุการณ์ในไดเรกทอรีแล้วเอาผลลัพธ์นั้นไปเชื่อมกับเรคคอร์ดในฐานข้อมูล
      สำหรับบางแอปพลิเคชันอาจถูกต้อง แต่มีงานบางอย่างที่ฐานข้อมูลทำได้ดีจริง ๆ และนั่นทำให้มันเป็นตัวเลือกที่ดี หากมีดัชนีที่เหมาะสม ก็น่าจะได้ throughput ระดับเดียวกันหรือดีกว่าด้วยซ้ำ เว้นแต่จะสร้างโครงสร้างไดเรกทอรีอัจฉริยะสำหรับเหตุการณ์ ซึ่งจริง ๆ แล้วก็เหมือนดัชนีมิติเดียว และในฐานข้อมูลสามารถสร้างดัชนีได้หลายมิติรวมถึงการผสมมิติต่าง ๆ
      สุดท้ายก็พูดถูก และเป็นเรื่องของ การแลกเปลี่ยนข้อดีข้อเสีย
    • iCalendar ถูกออกแบบมาเป็น รูปแบบสำหรับแลกเปลี่ยนข้อมูล ไม่ใช่รูปแบบจัดเก็บเพื่อเข้าถึงข้อมูลปฏิทินอย่างรวดเร็ว
      ตัวรูปแบบเผยให้เห็นอายุของมัน คือดูออกว่าออกแบบมาก่อนที่ XML/JSON จะ “ฮิต”
      อ้างอิง: https://en.wikipedia.org/wiki/ICalendar
    • อาจทำได้โดยไม่ใช้ DBMS แต่ข้อมูลก็ยังต้องมีสคีมาที่ดี และไม่ควรเป็นแค่ก้อนเรคคอร์ด iCalendar ตัวอย่างเช่น มีเพียง ที่อยู่อีเมลที่เปลี่ยนได้และซ้ำกันได้ แทนที่จะเป็น ID ผู้ใช้ที่เสถียร
    • แนวทางแบบนั้นก็น่าจะให้วิธีแก้ปัญหา สล็อตเวลาไม่จำกัด ที่บทความพูดถึงได้สะอาดกว่าด้วย
  • เป็นบทความเกี่ยวกับการจัดการ กำหนดการซ้ำ ในแอปพลิเคชัน ซึ่งส่วนตัวแล้วเปิดหูเปิดตามาก แนะนำให้อ่านจริง ๆ: https://github.com/bmoeskau/Extensible/blob/master/recurrenc...

    • น่าสนใจที่คำแนะนำในบทความต้นฉบับกับบทความนี้ตรงข้ามกัน
      บทความต้นฉบับบอกให้เก็บเขตเวลาไว้กับทุกวันที่เสมอ ส่วนบทความที่ลิงก์บอกให้แปลงทั้งหมดเป็น UTC ตรงส่วนนี้ผมเห็นด้วยกับบทความต้นฉบับ
      บทความต้นฉบับบอกให้สร้างแถวในฐานข้อมูลสำหรับแต่ละเหตุการณ์ ส่วนบทความที่ลิงก์บอกว่าอย่าทำ ตรงส่วนนี้ผมเห็นด้วยกับบทความที่ลิงก์
  • ครั้งที่ผมพลาดหนักที่สุดในการสัมภาษณ์คือตอนต้องทำระบบจองแบบง่าย ๆ แต่ต้องรองรับ การจองซ้ำ ด้วย
    ตั้งแต่ช่วงปีแรก ๆ ในมหาวิทยาลัย ผมไม่เคยรู้สึกหลงทางและสับสนขนาดนั้นเวลาพยายามทำอะไรให้สำเร็จด้วยโค้ดเลย ทุกวันนี้ถ้าต้องทำงานเกี่ยวกับวัน/เวลา ผมก็กลัวขึ้นมาก่อน และไม่รู้ทำไมมันถึงไม่เข้าที่เข้าทางในหัว

    • คำถามออกแบบว่า “ออกแบบระบบจอง โดยรวมการจองซ้ำด้วย” คล้ายกับคำถามว่า “เขียนฟังก์ชันสำหรับเรียงลำดับลิสต์สตริง แต่ต้องรองรับสตริง UTF-8 ใด ๆ และยึดตามธรรมเนียมการเรียงตามตัวอักษรของทุก locale”
      ดูเผิน ๆ เหมือนง่าย แต่ถ้าพยายามให้ทำงานได้ทั่วไปในโลกจริง ปัญหาจะ ซับซ้อนจนแทบเป็นไปไม่ได้
    • ไม่ต้องโทษตัวเองมากก็ได้ ผมเคยได้รับมอบหมายให้ปรับปรุงปฏิทินของ CMS ให้รองรับฟีเจอร์ได้มากขึ้นเยอะ แล้วเกือบโดนมันกลืนกิน ตอนนั้นผมมีประสบการณ์ประมาณ 8 ปีแล้ว
      นี่เป็น ปัญหาที่ยากแบบหลอกตา และต้องใช้ทักษะการทำ data modeling ที่ยอดเยี่ยม
  • ความสามารถในการสร้างแบบจำลองระบบถูกประเมินต่ำเกินไป ในโดเมนใหม่ ๆ ควรเริ่มจาก การวิเคราะห์ปัญหาทั้งหมด เพื่อจับให้ได้ทั้งโครงสร้างเชิงสถิตของระบบ คือโมเดลคลาส และพฤติกรรมเชิงไดนามิก คือกรณีการใช้งาน
    ถ้ากระโดดเข้าไปที่โมเดลฐานข้อมูลเชิงสถิตทันที ก็มีโอกาสพลาดพฤติกรรมเชิงไดนามิกได้ง่าย ในแอป CRUD ง่าย ๆ แบบนี้อาจไม่เป็นไร แต่ในระบบที่ซับซ้อนกว่านั้นอาจเป็นความผิดพลาดครั้งใหญ่

    • ในฐานะ data scientist ผมเคยเจอคำถามสัมภาษณ์ที่ดูเหมือนง่ายว่า “คุณจะสร้างสคีมายังไง” แล้วต้องลำบากพอสมควรแบบสด ๆ อยู่หลายครั้ง
      คำถามล่าสุดที่จำได้คือจะสร้างตารางสินค้าที่มีคูปองอย่างไร ตอนแรกคิดว่าสองตารางก็น่าจะพอ แต่ต้องเก็บประวัติด้วยไม่ใช่หรือ? ถ้าอย่างนั้นก็ต้องใส่วัน/เวลาตามสินค้าและคูปอง แล้วคอยอัปเดต ทีนี้ก็ต้องคิดถึงดัชนีของตารางอีก และต้องพิจารณาด้วยว่าการ join เพื่อหาราคาหลังส่วนลดเป็นวิธีที่ดีไหม คูปองส่วนใหญ่ใช้ได้คนละครั้ง แล้วจะ implement ยังไงกันแน่?
      เขาน่าจะต้องการแค่ ตารางสินค้า + คูปอง ง่าย ๆ แต่ผมกลับขุดลึกแบบบ้าคลั่งอยู่คนเดียวไปพักใหญ่
    • ผลลัพธ์ของการวิเคราะห์แบบนี้หน้าตาเป็นอย่างไร? ถ้ามีลิงก์อ้างอิงเป็นเอกสารตัวอย่างหรือบทในหนังสือก็อยากดู
  • ผมคิดว่าเป็นโดเมนที่เลือกมาได้ดีสำหรับแสดงการ modeling และเป็นบทความปูพื้นฐานที่ดี
    คำว่า “anchor” รู้สึกแปลกอยู่นิดหน่อย แต่คำอธิบายก็เป็นรูปธรรมและติดดินเหมือนสมอจริง ๆ จึงพอเข้าใจได้
    แนวคิดในการนิยามคุณลักษณะด้วยคำถามนั้นแข็งแรงดี เรามักกระโดดไปตั้งชื่อคอลัมน์/คุณลักษณะขั้นต่ำทันทีโดยยังไม่ได้นิยามว่าเรากำลังพยายามตอบคำถามอะไร ซึ่งทำให้ไม่สามารถขจัดความคลุมเครือในหัวลูกค้าได้ การนิยามคุณลักษณะโดยอิงคำถาม เป็นวิธีที่ดีในการทำให้ชัดเจนได้อย่างรวดเร็ว

    • คำว่า “anchor” รับมาจาก Anchor Modeling(https://en.wikipedia.org/wiki/Anchor_modeling) คำว่า “Entity” ใช้กันกว้างขวาง แต่ผมไม่อยากใช้เพราะอาจพ่วงภาระและสมมติฐานที่ไม่จำเป็นมาด้วย
      อีกอย่าง คำนี้ถูกใช้ทับซ้อนกันหนักมาก ในวิทยาการคอมพิวเตอร์ แทบทุกอย่างไม่เป็น object ก็เป็น entity
      ปฏิกิริยาที่ว่าแนวคิดนิยามคุณลักษณะด้วยคำถามนั้นแข็งแรงดี เป็นการยืนยันสำคัญว่าแนวทางนี้สมเหตุสมผล
  • เขตเวลา ทำให้ปวดหัวเป็นพิเศษ โดยเฉพาะช่วงใกล้จุดเปลี่ยนเวลา
    ลองสมมติว่าเขตเวลาขยับไปข้างหน้าหนึ่งชั่วโมงเมื่อเข้าสู่เวลาออมแสง และถอยกลับหนึ่งชั่วโมงเมื่อกลับสู่เวลามาตรฐาน
    เมื่อเวลาข้ามไปข้างหน้าหนึ่งชั่วโมง อีเวนต์ความยาว 1 ชั่วโมงอาจดูบนหน้าจอเหมือนกินเวลา 2 ชั่วโมง และชั่วโมงที่สองนั้นอาจเข้าถึงไม่ได้หรือไม่มีอยู่จริง
    เมื่อเวลาถอยกลับหนึ่งชั่วโมง อีเวนต์ความยาว 1 ชั่วโมงอาจดูเหมือน 2 ชั่วโมง หรือดูเหมือน 0 ชั่วโมงก็ได้
    เขตเวลาเป็นสิ่งที่มนุษย์สร้างขึ้น จึงไม่ควรฮาร์ดโค้ดค่าไว้ สักวันมันก็เปลี่ยน

    • ถ้าติดตามเวลาโดยคำนึงถึงการเดินทางด้วย เรื่องจะยิ่งน่าสนใจขึ้นอีก ครั้งแรกที่เจอคือการติดตามเวลาบนเรือที่อยู่กลางทะเลหลายวัน
      คุณอาจข้ามเขตเวลาไปมาได้ทั้งสองทิศทาง ข้ามเส้นแบ่งวันสากลได้ และอาจเกิดกรณีที่วันที่/เวลาเริ่มต้นตามเวลาท้องถิ่นอยู่หลังวันที่/เวลาสิ้นสุดตามเวลาท้องถิ่นด้วย
    • หมายความว่าจะสร้างปฏิทินทั้งหมดเป็น UTC โดยไม่มีเขตเวลาหรือ? ผมเห็นด้วยว่าเขตเวลาเป็นสิ่งที่เราสร้างขึ้น แต่ก็สร้างขึ้นเพื่อการจัดระเบียบในแต่ละภูมิภาคที่ดีกว่า ผมทำงานกับลูกค้าทั่วโลก และทุกคนก็ใช้เขตเวลา โดยส่วนตัวแล้วผมว่าความต้องการนี้ค่อนข้างไร้สาระ
      สู้ไปโฟกัสที่การเลิก แนวคิดเวลาออมแสง ในบางพื้นที่ที่ยังใช้อยู่จะดีกว่า โดยเฉพาะตอนวางแผนกำหนดการหลังการเปลี่ยนเวลาที่กำลังจะมาถึง มันเป็นตัวก่อความสับสนระหว่างเขตเวลามากที่สุด
  • ไม่อยากเก็บวันที่สองค่าไว้ในอีเวนต์ เก็บ เวลาเริ่มต้นและระยะเวลา ของอีเวนต์จะง่ายกว่า
    วิธีนี้ทำให้ลอจิกการอัปเดตอีเวนต์เรียบง่ายขึ้น เวลาสิ้นสุดสามารถคำนวณจากเวลาเริ่มต้นและระยะเวลาได้ทุกเมื่อ

    • พูดตรง ๆ ก็คือเก็บทั้งสามค่าไปเลย ได้แก่ เวลาเริ่มต้น เวลาสิ้นสุด และระยะเวลา แล้วเลือกใช้ค่าที่ต้องการตามวัตถุประสงค์ ขอแค่มี จุดเข้า/อัปเดตจุดเดียว ที่คอยซิงก์การแทนค่าทางเลือกเหล่านี้ก็พอ
    • วิธีนั้นอาจมีปัญหาด้านประสิทธิภาพของฐานข้อมูล ต้องคำนวณระยะเวลาแบบทันทีทุกครั้งที่คิวรี
    • กลับกันก็เหมือนกันไม่ใช่หรือ? ถ้ามีเวลาเริ่มต้นและเวลาสิ้นสุด ก็แค่คำนวณระยะเวลาได้เลย
  • เคยทำแอปปฏิทินที่มีตารางเวลาซ้ำ ๆ มาก่อน หลังจากค้นคว้ามาเยอะ เราตัดสินใจใช้ RRules เพื่อแทนสิ่งนี้ และพอใจกับมันมาก งานช่วงแรกนั้นทำตอนที่ผมอยู่เอเจนซี
    ต่อมาผมเข้าร่วมบริษัทนั้นแบบพนักงานประจำ แล้วก็ถึงกับตกใจเมื่อเห็นว่าผู้รับเหมาจากบริษัทอื่นลบ RRules ออก และเปลี่ยนไปเป็นวิธีสร้างและลบอินสแตนซ์ของอีเวนต์แบบทันทีแทน มันแทบไม่มีความทนทานต่อความล้มเหลวเลย สคริปต์ที่บางครั้งก็ล้มเหลวระหว่างทำงานอื่น ๆ ไปด้วย มักจะสร้างอีเวนต์ใหม่ไม่สำเร็จ ผลก็คืออีเวนต์รายเดือนที่ควรเกิดซ้ำมีบางเดือนหายไป
    มันน่าหงุดหงิดมากที่มีคนลบสิ่งที่ผ่านการคิดและค้นคว้ามาอย่างมากออกไป โดยไม่ได้ลงแรงเท่า ๆ กันเลย อยู่บริษัทนั้นไม่กี่สัปดาห์ก็รู้ได้แล้วว่า CEO คาดหวังให้ทีมวิศวกรรมผลิตฟีเจอร์ตามใจเขาที่ไม่มีใครใช้ต่อไปเรื่อย ๆ และเพราะความไม่แน่นอนของตลาดงาน ผมจึงต้องอยู่ต่ออย่างเศร้า ๆ ถึง 2 ปี
    แถมภายหลังพอค้นดู ก็รู้สึกขมขื่นมากเมื่อเห็นรีวิวปลอมบน Glassdoor ที่เห็นชัดว่า CEO เป็นคนเขียน ทั้งหมดใช้สำนวนเดียวกันและไม่มีคำวิจารณ์แย่ ๆ เลย ผมกับคนรู้จักหลายคนที่เคยทำงานที่นั่นเกลียดเขา แต่ก็ยังพอปลอบใจได้ว่าผมได้เขียนเอสเซย์ที่ดีที่สุดบางชิ้นตอนอยู่บริษัทนั้น ส่วน CTO ก็หมดคำจะพูดเหมือนกัน

  • ครั้งหนึ่งเคยทำแบ็กเอนด์สำหรับควบคุมปฏิทินและทรัพยากรให้แพลตฟอร์ม low-code
    คอนโทรลนั้นปรับแต่งได้สูงมาก เลือกได้ทั้งมุมมองหลายแบบ เช่น รายวัน รายเดือน รายปี รวมถึงมุมมองทรัพยากรด้วย สามารถจองทรัพยากรด้วยกลุ่มแบบกำหนดเอง และจัดกลุ่มตามปลั๊กอิน, ตาม resource ID ฯลฯ ได้ มีการกำหนด “plugins” ในแหล่งข้อมูล รวมถึงระบุคอลัมน์เริ่มต้น/สิ้นสุด คอลัมน์ชื่อเรื่อง และคอลัมน์ทรัพยากร ทรัพยากรอาจมาจาก foreign key หรือความสัมพันธ์ 1:1 ก็ได้ หรือมาจากความสัมพันธ์ 1:N ของแหล่งข้อมูล “ลูก” หรือจากแหล่งข้อมูล/ตารางเดียวกันก็ได้
    นอกจากนี้ยังทำ ชุดนัดหมาย หลากหลายแบบ เช่น รายเดือน รายสัปดาห์ เฉพาะวันในสัปดาห์ และรายวัน พร้อมให้เลือกได้ว่าจะคัดลอกค่าคอลัมน์ใดบ้าง เรายังจัดการการชนกันของนัดหมายด้วย โดยสามารถกำหนดให้ถือว่าชนกันเฉพาะเมื่อจองทรัพยากรเดียวกันได้ ก่อนหรือหลังนัดหมายยังตั้ง buffer ที่ห้ามใส่นัดหมายอื่นได้ด้วย
    แม้จะท้าทายเป็นครั้งคราวเพราะเขตเวลาในยุโรปและเวลาแบบฤดูร้อน/ฤดูหนาว แต่ก็เป็นงานที่ค่อนข้างสนุก

  • สิ่งที่อยากได้จาก Google Calendar มีแค่ change log ของตัวปฏิทินเองเท่านั้น ขอเถอะ ช่วยเพิ่มสิ่งนี้ลงในฐานข้อมูลที