PlusMagi's Blog By Pitt Phunsanit data engineering,Data Visualization,Programming เจาะลึกความต่างใน SQL: ทำไม start_date >= … AND date_end <= … ถึงแทนที่ด้วย BETWEEN ไม่ได้

เจาะลึกความต่างใน SQL: ทำไม start_date >= … AND date_end <= … ถึงแทนที่ด้วย BETWEEN ไม่ได้

ในการเขียน 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 เข้าหาจุดเริ่มต้นแล้วอ่านต่อเนื่องไปจนถึงจุดสิ้นสุดได้อย่างมีประสิทธิภาพสูงสุด
  • แบบสองคอลัมน์ (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

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) ของสองคอลัมน์ได้