ในการออกแบบระบบฐานข้อมูลที่ต้องจัดการกับมิติของเวลา เช่น ระบบจองห้องพัก, สัญญาจ้าง, แผนงานโครงการ หรือแคมเปญโปรโมชัน หนึ่งในเงื่อนไขที่พบบ่อยแต่สร้างความสับสนได้ง่ายที่สุดคือรูปแบบ
SELECT *
FROM projects
WHERE start_date >= '1982-08-05'
AND date_end <= '3000-12-31';
เงื่อนไขนี้ไม่ได้มีไว้สำหรับการค้นหาข้อมูลตามช่วงเวลาทั่วไป แต่มีเจตนาเฉพาะเจาะจงทางตรรกะ รวมถึงมีพฤติกรรมการทำงานบน Database Engine ที่นักพัฒนาจำเป็นต้องเข้าใจลึกซึ้ง
1. ทำไมถึงต้องใช้เงื่อนไขนี้? (The Core Intent)
เหตุผลเดียวในการใช้เงื่อนไขนี้คือ คุณต้องการข้อมูลที่มี “วงจรชีวิตทั้งหมดบรรจุอยู่ภายในกรอบเวลาอย่างสมบูรณ์” (Fully Contained / Enclosed)
หากกำหนดกรอบการค้นหาเป็นปี 1982 (1982-01-01 ถึง 1982-12-31):
- รายการที่เริ่มและจบในปี 1982
ดึงมาแสดง - รายการที่เริ่มปี 1982 แต่ลากยาวมาจบปี 2003: ถูกตัดทิ้ง (เพราะ
start_date < '1982-01-01') - รายการที่เริ่มปลายปี 1982 แต่ไปจบต้นปี 2007: ถูกตัดทิ้ง (เพราะ
date_end > '1982-12-31')
เคสการใช้งานจริงที่เหมาะสม
- โครงการประเภท Short-term
เช่น การตรวจสอบโปรเจกต์พิเศษที่ต้องเริ่มและส่งมอบภายในปีงบประมาณเดียวกันเท่านั้น - สัญญาแบบปิด
2. จุดตัดสำคัญ: ระวังจำสับสนกับ “ช่วงเวลาคาบเกี่ยว” (Overlap)
ข้อผิดพลาดที่พบได้บ่อยที่สุดในโปรดักชัน คือ นักพัฒนาต้องการดึงงานที่ active ในช่วงเวลานั้น แต่กลับไปเขียนแบบ Contained
ถ้าโจทย์ธุรกิจคือ “อยากรู้ว่าในปี 1982 มีงานไหนบ้างที่กำลังดำเนินการอยู่” การใช้ start_date >= ... AND date_end <= ... จะทำให้ข้อมูลงานระยะยาว (เช่น งานที่เริ่มตั้งแต่ปี 1982 และจะเสร็จปี 2082) หายไปจากระบบทันที
สำหรับโจทย์ที่ต้องการดึงงานที่มีความคาบเกี่ยว (Overlap) ทั้งหมด สูตรที่ถูกต้องต้องเป็น
-- ดึงทุกงานที่มีชีวิตอยู่หรือคาบเกี่ยวในปี 1982
WHERE start_date <= '1982-12-31'
AND date_end >= '1982-01-01';
| ความต้องการทางธุรกิจ | รูปแบบ SQL ที่ต้องใช้ |
| ต้องเริ่มและจบ ภายใน กรอบเท่านั้น (Contained) | start_date >= 'min' AND date_end <= 'max' |
| มีความคาบเกี่ยวหรือ แตะ โดนช่วงเวลานี้ (Overlap) | start_date <= 'max' AND date_end >= 'min' |
3. กับดักเรื่องค่าว่าง (The NULL Trap)
ในระบบงานจริง ฟิลด์ date_end มักอนุญาตให้เป็น NULL สำหรับรายการที่ยังเปิดอยู่ (Ongoing / Active Tasks) หรือสัญญายังไม่มีกำหนดสิ้นสุด
เมื่อคุณรันคำสั่ง
WHERE start_date >= '1982-01-01'
AND date_end <= '1982-12-31'
การนำ NULL ไปเปรียบเทียบกับ <= '1982-12-31' จะได้ผลลัพธ์เป็น UNKNOWN (ไม่ใช่ TRUE) เสมอ ส่งผลให้รายการที่เริ่มในปี 1982 แต่ยังไม่กำหนดวันสิ้นสุด หรือยังทำไม่เสร็จ หลุดหายไปจากคิวรีทันที
หากเป้าหมายธุรกิจมองว่ารายการที่ยังไม่จบ ถือว่าอยู่ในขอบเขตการตรวจสอบด้วย ต้องเขียนดักทางเลือกไว้เสมอ
WHERE start_date >= '1982-01-01'
AND (date_end <= '1982-12-31' OR date_end IS NULL)
4. พฤติกรรมด้าน Performance และการทำงานของ Index
เมื่อดูในเชิงการประมวลผล (Query Execution Plan) รูปแบบ start_date >= ... AND date_end <= ... เป็นการใช้ Range Comparison (>=, <=) กับ 2 คอลัมน์ที่แยกจากกัน ซึ่งสร้างความท้าทายให้กับ B-Tree Index:
- ขีดจำกัดของ Composite Index
- แม้คุณจะสร้าง Index
CREATE INDEX idx_dates ON projects (start_date, date_end) - ตัว Optimizer จะสามารถใช้ Index เพื่อกระโดดหาและสแกนช่วงของคอลัมน์แรก (
start_date >= '1982-01-01') ได้อย่างมีประสิทธิภาพ - แต่สำหรับคอลัมน์ที่สอง (
date_end <= '1982-12-31') ฐานข้อมูลจะไม่สามารถทำ Range Seek ซ้อนกันได้ จะต้องนำค่านั้นมาไล่ตรวจ (Filter) ทีละแถวจากชุดข้อมูลที่ผ่านเกณฑ์แรกมาแล้ว
- แม้คุณจะสร้าง Index
- การเลือกระหว่าง Index vs Table Scan:
- หากช่วงเวลาที่กำหนดมีข้อมูลสัดส่วนมากเกินไป (Low Selectivity) Database Engine มักจะข้ามการใช้ Index แล้วหันไปทำ Full Table Scan แทน เพื่อลดภาระการอ่านข้อมูลแบบ Random Access
5. แนวทางปฏิบัติที่ดี (Best Practices)
- ระบุชนิดข้อมูลให้ชัดเจน
หากคอลัมน์เป็นDATETIMEหรือTIMESTAMPหลีกเลี่ยงการเขียน<= '1982-12-31'เพราะระบบจะแปลงเป็น00:00:00อัตโนมัติ ทำให้ข้อมูลระหว่างวันตกหล่น ควรใช้แบบ Half-Open Interval เช่นdate_end < '2082-01-01' - สร้าง Check Constraint ป้องกันข้อมูลย้อนศร
เพื่อป้องกัน Data Inconsistency ควรมี Database Constraint กักไว้เสมอว่าCONSTRAINT chk_dates CHECK (date_end >= start_date) - ตรวจสอบเจตนาของโจทย์ให้แน่ใจ
ก่อนเคาะคีย์บอร์ดพิมพ์start_date >= ... AND date_end <= ...ให้ทบทวนเสมอว่าคุณกำลังต้องการงานที่ “เริ่มและจบในกรอบเท่านั้น” จริงหรือไม่