เขียน WHERE ให้ Sargable เพื่อให้ Database ใช้ Index ได้เต็มประสิทธิภาพ
แม้ WHERE จะดูเหมือนมีหน้าที่แค่กรองข้อมูล แต่รูปแบบการเขียนมีผลโดยตรงว่าฐานข้อมูลจะใช้ index ได้หรือไม่ บทความนี้สรุปแนวคิดเรื่อง sargable พร้อมตัวอย่าง rewrite query ที่ช่วยให้ query เร็วขึ้นอย่างมีนัยสำคัญ

การเขียน WHERE ที่ดูเหมือนทำงานเหมือนกันในเชิงผลลัพธ์ อาจให้ประสิทธิภาพต่างกันมากในเชิง execution plan เพราะสิ่งสำคัญไม่ได้มีแค่ว่าเงื่อนไขนั้น “กรองแถวได้ถูกต้อง” หรือไม่ แต่ยังรวมถึงว่าเงื่อนไขนั้น เอื้อให้ฐานข้อมูลใช้ index ได้ตรง ๆ หรือเปล่า ด้วย
แนวคิดสำคัญที่อยู่เบื้องหลังเรื่องนี้คือคำว่า sargable ซึ่งเป็นหนึ่งในพื้นฐานที่มีผลต่อความเร็วของ query อย่างมาก โดยเฉพาะในระบบ backend, report API หรือ endpoint ที่มีทราฟฟิกสูง
Sargable คืออะไร
คำว่า sargable อธิบายแบบง่าย ๆ คือ เงื่อนไขที่ฐานข้อมูลสามารถใช้ค้นหาใน index ได้โดยตรง เช่น
column = valuecolumn >= valuecolumn < value
เงื่อนไขลักษณะนี้เหมือนการเปิดสารบัญแล้วกระโดดไปยังช่วงข้อมูลที่ต้องการได้ทันที แทนที่จะต้องไล่อ่านข้อมูลทีละแถวทั้งตาราง
ตัวอย่างเช่น
WHERE created_at >= '2026-07-01'
AND created_at < '2026-07-02'
หากมี index ที่ created_at ฐานข้อมูลมีโอกาสใช้ range scan เพื่ออ่านเฉพาะช่วงเวลาที่สนใจได้ ทำให้ลดจำนวนแถวที่ต้องอ่านลงอย่างมาก
ปัญหาของการครอบ Column ด้วย Function
รูปแบบที่พบบ่อยและทำให้ query ช้าลงโดยไม่รู้ตัวคือการใช้ function ครอบ column เช่น
WHERE DATE(created_at) = '2026-07-01'
แม้ผลลัพธ์จะดูตรงกับความต้องการ แต่ปัญหาคือฐานข้อมูลต้องคำนวณ DATE(created_at) ให้แต่ละแถวก่อน แล้วจึงค่อยนำไปเปรียบเทียบกับค่าที่ต้องการ ส่งผลให้ index เดิมบน created_at อาจช่วยได้ไม่เต็มที่ หรือบาง engine อาจต้อง scan ข้อมูลจำนวนมาก
แนวทางที่ควรฝึกคือ
เปลี่ยนจาก “แปลง column” เป็น “แปลงเงื่อนไข”
เช่น rewrite เป็น
WHERE created_at >= '2026-07-01 00:00:00'
AND created_at < '2026-07-02 00:00:00'
ข้อดีของรูปแบบนี้คือ
- อ่าน index ได้เป็นช่วง
- ครอบคลุมข้อมูลที่มีเวลา เช่น
10:15,18:30,23:59 - ลด bug ที่พบบ่อยใน report API จากการเทียบวันที่แบบตัดเวลาทิ้ง
การค้นหา Email และผลกระทบจาก LOWER()
อีกกรณีคลาสสิกคือการค้นหา email แบบไม่สนตัวพิมพ์เล็กพิมพ์ใหญ่ หลายคนมักเขียนว่า
WHERE LOWER(email) = LOWER($1)
แม้จะใช้งานได้ถูกต้องเชิงตรรกะ แต่ถ้าไม่มี functional index หรือ expression index บน LOWER(email) แล้ว index ปกติบน email อาจไม่ช่วย เพราะฐานข้อมูลต้องคำนวณ LOWER(email) ก่อนเปรียบเทียบ
ทางเลือกที่เหมาะสมมีหลายแบบ เช่น
- เก็บค่า
normalized_emailตั้งแต่ตอนเขียนข้อมูล - ใช้ case-insensitive type หรือ collation ให้ตรงกับ requirement
- สร้าง expression index เช่น
LOWER(email) - normalize input ตั้งแต่ใน layer ของ Node.js
ประเด็นสำคัญคือไม่ควรแก้ปัญหาแบบโยน LOWER() ลงใน query ทุกจุด แล้วคาดหวังว่า index จะยังทำงานได้ดีเหมือนเดิม
LIKE แบบ Prefix กับ Contains ต่างกันมาก
การใช้ LIKE ก็มีผลต่อการใช้ index อย่างชัดเจน โดยเฉพาะความต่างระหว่าง
WHERE name LIKE 'john%'
กับ
WHERE name LIKE '%john%'
กรณีแรกอย่าง 'john%' ยังมีโอกาสใช้ btree index ได้ในหลายระบบ เพราะฐานข้อมูลรู้ว่าต้องเริ่มค้นหาจาก prefix ที่ชัดเจน
แต่กรณี '%john%' มี wildcard นำหน้า ทำให้ฐานข้อมูลไม่รู้ว่าจะเริ่มจากจุดไหนใน index จึงมักลงเอยด้วยการ scan หรือจำเป็นต้องใช้ index ประเภทอื่นแทน
หากโจทย์คือการค้นหาคำในประโยคหรือค้นหาแบบ contains จริง ๆ ควรใช้เครื่องมือที่เหมาะกับงาน เช่น
- PostgreSQL:
pg_trgm - MySQL:
FULLTEXT index - ระบบค้นหาภายนอก เช่น Elasticsearch หรือ OpenSearch เมื่อระบบมีขนาดใหญ่ขึ้น
หลักคิดคือ อย่าฝืนให้ btree ทำงานที่ไม่ถนัด
Type Mismatch ก็ทำให้ Index ใช้ไม่ได้
อีกกับดักที่เจอบ่อยคือชนิดข้อมูลไม่ตรงกัน เช่น column user_id เป็น integer แต่ query ส่งค่าเป็น string แล้วต้อง cast column เพื่อให้เทียบกันได้
ตัวอย่างที่เสี่ยงคือ
WHERE CAST(user_id AS TEXT) = '123'
เมื่อมีการ cast column แบบนี้ index บน user_id อาจถูกลดประสิทธิภาพหรือใช้ไม่ได้เลย ทางที่ถูกต้องกว่าคือส่ง parameter ให้ตรง type ตั้งแต่ฝั่งแอปพลิเคชัน เช่น
- ใช้
numberสำหรับ id - ใช้
Dateสำหรับ timestamp - หรือใช้ string ที่ format ชัดเจนตามที่ driver รองรับ
ในระบบ backend จริง ปัญหานี้มักเริ่มจาก request query ที่ทุกค่าเข้ามาเป็น string เช่น req.query.userId ได้ค่า '123' แล้วถูกส่งต่อเข้า query builder ทันทีโดยไม่ normalize
workflow ที่ปลอดภัยกว่าคือ
รับ input → validate → coerce type → build query
ตัวอย่างเช่นใช้ zod เพื่อแปลง userId ให้เป็น number ก่อน แล้วค่อยส่งเข้า SQL parameter
OR Condition อาจทำให้ Optimizer เลือกแผนไม่ดี
เงื่อนไข OR โดยเฉพาะเมื่อครอบหลาย column มักทำให้ execution plan แย่ลงได้ เช่น
WHERE status = 'paid'
OR customer_email = $1
บางครั้ง optimizer อาจใช้ index ได้ แต่บางครั้งก็อาจเลือก scan เพราะมองว่าการอ่านหลายเส้นทางไม่คุ้มต้นทุน
ถ้า query นี้สำคัญและช้า ทางเลือกหนึ่งที่ควรลองคือแยกเป็น UNION ALL เพื่อเปิดทางให้ฐานข้อมูลเลือกใช้ index แยกกันในแต่ละส่วน เช่น
SELECT ... WHERE status = 'paid'
UNION ALL
SELECT ... WHERE customer_email = $1
AND status <> 'paid'
วิธีนี้ไม่ใช่สูตรสำเร็จ แต่เป็นแนวทางที่ช่วยให้ optimizer มองเส้นทางการเข้าถึงข้อมูลได้ชัดขึ้น สิ่งที่ต้องทำเสมอคือวัดผลด้วย EXPLAIN
Optional Filter ที่เขียนง่าย แต่อาจช้ากว่าที่คิด
อีกแพตเทิร์นที่สะดวกในการทำ API เดียวรองรับหลายกรณีคือ
WHERE ($1 IS NULL OR status = $1)
แม้จะเขียนง่าย แต่ในบางฐานข้อมูล optimizer อาจสร้าง plan แบบกลาง ๆ ที่ไม่เหมาะกับทุกกรณี โดยเฉพาะ endpoint ที่มีทราฟฟิกสูง
แนวทางที่มักดีกว่าคือให้ Node.js สร้าง WHERE เฉพาะ filter ที่มีจริง เช่น
- ถ้าไม่มี
statusก็ไม่ใส่ condition นี้เลย - ถ้ามี
statusค่อยใส่WHERE status = $1
query ที่ชัดเจนมักช่วยให้ optimizer ตัดสินใจได้แม่นยำกว่า และเพิ่มโอกาสในการใช้ index อย่างเหมาะสม
หลักจำสำหรับ WHERE ที่เป็นมิตรกับ Index
หากต้องการเขียน WHERE ให้มีโอกาสใช้ index ได้ดี ควรจำหลักต่อไปนี้ให้ขึ้นใจ
- อย่าครอบ column ด้วย function ถ้าไม่มี index รองรับ
- อย่า cast column เพื่อให้เข้ากับ input
- อย่าใช้ wildcard นำหน้ากับ btree แล้วหวังความเร็ว
- อย่าใช้
ORกว้าง ๆ โดยไม่ดู execution plan - อย่าเดาว่า index ถูกใช้งานแล้ว จนกว่าจะตรวจด้วย
EXPLAIN
4 คำถามที่ควรถามเมื่อเจอ Query ช้า
เวลา debug query ช้า ลองเริ่มจากคำถาม 4 ข้อนี้
- condition แตะ column ตรง ๆ หรือมีการแปลง column ก่อน?
- pattern search เป็นแบบ prefix หรือ contains?
- parameter type ตรงกับ column หรือไม่?
- execution plan อ่านแถวจริงกี่แถว เทียบกับจำนวนแถวที่ return?
หากพบว่า query อ่านข้อมูลเป็นล้านแถว แต่คืนผลลัพธ์แค่ไม่กี่แถว ปัญหาอาจไม่ได้อยู่ที่ server หรือ Node.js แต่อยู่ที่รูปแบบ WHERE ที่ทำให้ index ไม่สามารถช่วยงานได้
สรุป
index ไม่ได้ไร้ประโยชน์เพราะฐานข้อมูลทำงานไม่ฉลาด แต่บ่อยครั้งเกิดจากการเขียนเงื่อนไขที่ทำให้ฐานข้อมูล “เริ่มค้นหา” จาก index ได้ไม่ดีพอ
ดังนั้นก่อนจะรีบเพิ่ม index ใหม่หรือโทษว่าระบบช้าเพราะ application layer ควรเริ่มจากการตรวจสอบว่า WHERE ของเรานั้น sargable หรือไม่ หากเขียนเงื่อนไขให้เป็นมิตรกับ index ได้ตั้งแต่ต้น ก็จะช่วยให้ query เร็วขึ้น มีเสถียรภาพขึ้น และลดปัญหาคอขวดในระบบ production ได้อย่างมาก