ในการเขียน SQL Query เพื่อจัดการข้อมูลช่วงเวลา (Date Range) มีสองแนวคิดที่นักพัฒนาและผู้ออกแบบระบบมักเข้าใจผิดหรือสับสนในการนำมาเทียบเคียงกัน นั่นคือ
1. การตรวจสอบช่วงเวลาแบบสองคอลัมน์ (Two-Column Enclosure)
SELECT a, b, c
FROM x
WHERE start_date >= '1982-03-31'
AND date_end <= '2082-01-01';
2. การใช้คำสั่ง BETWEEN บนคอลัมน์เดี่ยว (Single-Column Range)
SELECT a, b, c
FROM x
WHERE start_date BETWEEN '1982-03-31' AND '2082-01-01';
แม้ดูเผิน ๆ ทั้งสองคำสั่งจะมีจุดประสงค์เพื่อจำกัดกรอบข้อมูลระหว่างวันที่ 1982-03-31 ถึง 2082-01-01 เหมือนกัน แต่ในทางปฏิบัติ ทั้งคู่มีความหมายทางตรรกะ กลไกการทำงาน และผลข้างเคียงต่อระบบฐานข้อมูลที่ต่างกันอย่างสิ้นเชิง
ความหมายทางตรรกะ (Business Logic)
ความต่างสำคัญที่สุดอยู่ที่ โครงสร้างมิติของข้อมูล
- แบบสองคอลัมน์ (
start_date >= ... AND date_end <= ...):- คือการหาข้อมูลที่มี “วงรอบชีวิตทั้งหมด” บรรจุอยู่ภายในกรอบเวลาอย่างสมบูรณ์ (Contained / Fully Enclosed)
- เหตุการณ์ต้องเริ่ม ไม่ก่อน วันที่ 31 มีนาคม 1982 และต้องสิ้นสุด ไม่เกิน วันที่ 1 มกราคม 2082
- หากโครงการเริ่มตั้งแต่ปี 2023 แม้จะสิ้นสุดปลายปี 2025 หรือโครงการเริ่มปี 1982 แต่ไปจบปี 2027 รายการเหล่านี้จะไม่ติดเข้ามาในผลลัพธ์
- แบบใช้
BETWEENบนคอลัมน์เดียว:- ไวยากรณ์
col BETWEEN A AND Bถูกออกแบบมาสำหรับ คอลัมน์เดียว โดยแปลงเป็นcol >= A AND col <= B - หากนำมาใช้กับ
start_date BETWEEN ...จะหมายถึง: “สนใจแค่วันที่เริ่มต้นต้องอยู่ในกรอบนี้ ส่วนจะสิ้นสุดเมื่อไรไม่สนใจ (จะจบปี 2030 ก็ยังผ่านเงื่อนไข)” - ไม่สามารถนำ
BETWEENมาเขียนแทนเงื่อนไขที่ตรวจสอบ 2 คอลัมน์พร้อมกันได้โดยตรง
- ไวยากรณ์
| มิติการเปรียบเทียบ | แบบ start_date >= … AND date_end <= … | แบบ start_date BETWEEN … |
| จำนวนคอลัมน์ที่ตรวจ | 2 คอลัมน์ (start_date, date_end) | 1 คอลัมน์ (เช่น start_date) |
| เจตนาของผลลัพธ์ | หาช่วงเวลาที่เริ่มต้นและจบลงภายในกรอบ | หาจุดเวลาของคอลัมน์นั้นๆ ว่าตกอยู่ในช่วงหรือไม่ |
| งานที่เริ่มก่อนแต่จบในช่วง | ไม่แสดง (ถูกคัดทิ้ง) | ไม่แสดง (ถูกคัดทิ้ง) |
| งานที่เริ่มในช่วงแต่จบหลังกรอบ | ไม่แสดง (ถูกคัดทิ้ง) | แสดง (ติดเข้ามาในผลลัพธ์) |
ปัญหาเรื่อง Inclusive Range และข้อมูลประเภท DATETIME
คำสั่ง BETWEEN มีคุณสมบัติที่เป็น Inclusive (รวมหัวและท้ายเสมอ) ซึ่งสร้างปัญหาร้ายแรงเมื่อคอลัมน์เก็บข้อมูลชนิด DATETIME หรือ TIMESTAMP
WHERE created_at BETWEEN '1982-03-31' AND '2082-01-01'
ฐานข้อมูลจะแปลงค่าดังกล่าวเป็น
WHERE created_at >= '1982-03-31 00:00:00'
AND created_at <= '2082-01-01 00:00:00'
ผลลัพธ์: ข้อมูลที่เกิดขึ้นในวันที่ 2082-01-01 ตั้งแต่เวลา 00:00:01 เป็นต้นไป จะหลุดหายไปจากรายงานทั้งหมด
ทางแก้ในระดับ Production: ควรเลี่ยง BETWEEN แล้วใช้รูปแบบ Half-Open Interval [start, end) แทน
WHERE created_at >= '1982-03-31'
AND created_at < '2082-01-02'
สำหรับแบบสองคอลัมน์ จุดสิ้นสุด date_end <= '2082-01-01' ก็ต้องระวังปัญหานี้เช่นกัน หาก date_end มีเวลาติดอยู่ ควรปรับเป็น date_end < '2082-01-02'
ผลกระทบต่อ Index และการทำงานของ Optimizer
การกรองข้อมูลสองแบบนี้ส่งผลต่อการวาง Execution Plan ของ Query Engine แตกต่างกัน:
- แบบใช้
BETWEEN(หรือ>= AND <=บนคอลัมน์เดียว):- หากมี B-Tree Index บนคอลัมน์
start_dateตัว Optimizer จะทำ Index Range Scan เข้าหาจุดเริ่มต้นแล้วอ่านต่อเนื่องไปจนถึงจุดสิ้นสุดได้อย่างมีประสิทธิภาพสูงสุด
- หากมี B-Tree Index บนคอลัมน์
- แบบสองคอลัมน์ (
start_dateและdate_end):- การใช้ Inequality (
>=,<=) ข้าม 2 คอลัมน์ ทำให้ B-Tree Composite Index(start_date, date_end)ใช้งานได้เต็มประสิทธิภาพเพียงคอลัมน์แรกเท่านั้น - Database จะกระโดดหา
start_date >= '1982-03-31'จากนั้นต้องมาไล่สแกน Filter ค่าdate_end <= '2082-01-01'ทีละแถว - หากข้อมูลระหว่าง 2 วันที่นี้มีปริมาณมาก Optimizer อาจประเมินค่า Cost แล้วตัดสินใจเลือกทำ Full Table Scan แทนการใช้ Index
- การใช้ Inequality (
4. กรณีพิเศษ: ค่าว่าง (NULL Values)
ในระบบบันทึกสถานะงานหรือสัญญาทั่วไป รายการที่ยังดำเนินอยู่ (Active) มักเก็บ date_end เป็น NULL
- คิวรีแบบสองคอลัมน์ที่มีเงื่อนไข
date_end <= '2082-01-01'จะตีค่าการเปรียบเทียบกับNULLเป็นUNKNOWNและ คัดข้อมูลโครงการที่ยังดำเนินอยู่ทิ้งทั้งหมด - หากต้องการรวมรายการที่ยังเปิดอยู่ด้วย จะต้องจัดการค่า
NULLแยกต่างหาก
WHERE start_date >= '1982-03-31'
AND (date_end <= '2082-01-01' OR date_end IS NULL)
สรุปแล้ว BETWEEN เหมาะสำหรับการตรวจสอบพิกัดเวลาของ เหตุการณ์เดี่ยว (Point in Time) เท่านั้น ไม่สามารถนำมาใช้แทนการตรวจสอบความสัมพันธ์ระหว่าง จุดเริ่มต้นและจุดสิ้นสุด (Interval/Duration) ของสองคอลัมน์ได้