ทำไมฐานข้อมูลจึงเขียนช้าแต่อ่านเร็ว และออกแบบตารางอย่างไรให้โตได้ดี
ฐานข้อมูลจำนวนมากถูกออกแบบให้ “อ่านเร็ว” เพราะพฤติกรรมการใช้งานจริงมักอ่านมากกว่าเขียนอย่างชัดเจน แต่ความเร็วในการอ่านนี้ต้องแลกมาด้วยต้นทุนฝั่งการเขียนที่สูงขึ้นจาก index, transaction, constraint และโครงสร้างข้อมูลภายใน

ทำไมฐานข้อมูลจึงชอบ “เขียนช้า อ่านเร็ว”
เวลาพูดถึงประสิทธิภาพของฐานข้อมูล หลายคนมักสงสัยว่าทำไมระบบจำนวนมากจึงถูกออกแบบให้ฝั่งการอ่านข้อมูลเร็วมาก แต่ฝั่งการเขียนกลับดูมีต้นทุนสูงกว่า คำตอบคือในโลกการใช้งานจริง ระบบส่วนใหญ่มักมีการอ่านมากกว่าการเขียนหลายเท่า และผู้ใช้มักรับรู้ความช้าจากการอ่านได้ชัดเจนกว่ามาก
ตัวอย่างเช่น หน้าโปรไฟล์ ฟีด รายงาน หรือ dashboard มักถูกเปิดดูซ้ำบ่อยมาก ในขณะที่การเขียน เช่น การสร้างข้อมูลใหม่หรือแก้ไขข้อมูล มักเกิดน้อยกว่า และบางครั้งยังสามารถซ่อนความหน่วงได้ด้วยคิวหรือการทำงานแบบ async
เหตุผลที่ระบบมักถูกปรับให้ “อ่านเร็ว”
สาเหตุสำคัญคือปริมาณงานอ่านในหลายระบบมากกว่างานเขียนประมาณ 10-100 เท่า เมื่อผู้ใช้กดเปิดหน้าแล้วต้องรอนาน ความรู้สึกแย่จะเกิดขึ้นทันที ต่างจากการเขียนข้อมูลที่หากช้าลงเล็กน้อย อาจจัดการได้ด้วยการประมวลผลเบื้องหลัง
ดังนั้นการทำให้ query อ่านทำงานได้เร็ว จึงมักเป็นเป้าหมายหลักของการออกแบบฐานข้อมูลตั้งแต่ต้น
ทำไมการอ่านเร็วถึงแลกมากับการเขียนที่แพงขึ้น
หัวใจสำคัญอยู่ที่ ดัชนี (Index) ซึ่งทำให้ฐานข้อมูลค้นหา กรอง เรียง และ join ข้อมูลได้เร็วขึ้นมาก แต่ทุกครั้งที่มีการ INSERT, UPDATE หรือ DELETE ฐานข้อมูลต้องอัปเดตดัชนีที่เกี่ยวข้องทั้งหมดตามไปด้วย
ยิ่งมี index มาก การเขียนก็ยิ่งช้าลง เพราะต้องทำงานเพิ่มหลายขั้นตอน ข้อดีคือ query ที่ออกแบบสอดคล้องกับ index จะเร็วขึ้นอย่างชัดเจน โดยเฉพาะ query ที่มีการกรองและเรียงข้อมูลบ่อยๆ
พูดง่ายๆ คือ
- อ่านเร็ว เพราะมีโครงสร้างช่วยค้นหาไว้ล่วงหน้า
- เขียนช้า เพราะต้องคอยอัปเดตโครงสร้างเหล่านั้นทุกครั้ง
ความถูกต้องของข้อมูลก็ทำให้การเขียนช้าลง
ฝั่งการเขียนไม่ได้มีแค่การบันทึกข้อมูลลงตารางเท่านั้น แต่ยังต้องผ่านขั้นตอนเพื่อรักษาความถูกต้องของระบบ เช่น
- การทำธุรกรรม (Transaction)
- การล็อกข้อมูล (Lock)
- การตรวจสอบ constraint
- การเขียน log เพื่อการกู้คืน เช่น Write-Ahead Log หรือ redo log
ตัวอย่าง constraint ที่พบบ่อย ได้แก่
unique(email)เพื่อกันอีเมลซ้ำforeign key(order_items.order_id -> orders.id)เพื่อกันข้อมูลลูกที่ไม่มีข้อมูลแม่check(price >= 0)เพื่อกันราคาติดลบ
สิ่งเหล่านี้ช่วยให้ข้อมูลเชื่อถือได้ในระยะยาว แต่ก็ทำให้ทุกการเขียนมีต้นทุนเพิ่มขึ้นตามไปด้วย
โครงสร้างข้อมูลภายในก็มีผลต่อความเร็ว
ฐานข้อมูลไม่ได้เก็บข้อมูลแบบแบนๆ เท่านั้น แต่มีการจัดรูปข้อมูลเพื่อให้ค้นหาได้เร็ว เช่น
- B-Tree
- LSM Tree
ในระบบแบบ B-Tree การแทรกข้อมูลใหม่อาจทำให้เกิด page split หรือการย้ายข้อมูลภายในโครงสร้าง ส่วนระบบแบบ LSM มักเขียนลง memtable ก่อน แล้วจึงค่อย compact ภายหลัง
แนวคิดสำคัญคือ โครงสร้างที่ช่วยให้ค้นหาเร็ว มักมีค่าใช้จ่ายในฝั่งเขียนหรือในช่วงจัดระเบียบข้อมูลเสมอ
แคชช่วยฝั่งอ่านได้มากกว่าฝั่งเขียน
อีกเหตุผลที่ทำให้การอ่านดูเร็วมากคือการมี cache หลายชั้น เช่น
- DB buffer pool
- OS page cache
- CDN
- แคชในแอปพลิเคชัน
ข้อมูลที่ถูกอ่านซ้ำมีโอกาสสูงที่จะถูกดึงจาก cache แทนการอ่านจาก disk จริง ทำให้ตอบสนองได้เร็วมาก แต่ฝั่งการเขียนยังต้องไปให้ถึง log หรือ disk เพื่อรักษาความทนทานของข้อมูล จึงเร่งได้ยากกว่า
มือใหม่ควรออกแบบตารางอย่างไรให้ไม่พังตอนข้อมูลโต
เมื่อเข้าใจแล้วว่าการอ่านเร็วต้องแลกกับต้นทุนบางอย่างในการเขียน คำถามถัดมาคือควรออกแบบฐานข้อมูลอย่างไรให้ระบบโตได้โดยไม่เจ็บตัวภายหลัง
1) เริ่มจากความถูกต้องก่อนความเร็ว
การออกแบบที่ดีควรเริ่มจากโครงสร้างที่ถูกต้องก่อน เพราะจะช่วยลดบั๊ก ทำ migration ง่าย และดูแลระบบในระยะยาวสะดวกกว่า จากนั้นค่อย optimize เพิ่มด้วย index หรือ denormalize ในจุดที่จำเป็นจริง
2) ทำ 3NF แบบพอดี แล้วค่อยเร่งเฉพาะจุด
ควรแยก entity ให้ชัดเจน เช่น
usersordersorder_items
หลีกเลี่ยงการเก็บข้อมูลซ้ำโดยไม่จำเป็น ถ้ายังไม่มีเหตุผลด้าน performance ที่ชัดเจน เพราะข้อมูลซ้ำทำให้ดูแลยากและเสี่ยงต่อความไม่สอดคล้อง
3) เลือก Primary Key ให้เหมาะกับการเติบโต
ถ้าใช้เลขที่เพิ่มขึ้นเรื่อยๆ เช่น auto-increment หรือ bigserial ในบางกรณีจะเขียนได้เร็วกว่า random UUID โดยเฉพาะในโครงสร้างแบบ B-Tree
แต่ถ้าระบบต้องสร้าง ID จากฝั่ง client หรือทำงานแบบกระจายหลายศูนย์ข้อมูล UUID หรือ ULID ก็อาจเหมาะกว่า โดยเฉพาะ ULID หรือ UUIDv7 ที่มีลักษณะเรียงตามเวลา ช่วยบาลานซ์ระหว่างความไม่ซ้ำกับการเขียนที่เป็นลำดับมากขึ้น
4) ใช้ datatype ให้พอดี
การเลือกชนิดข้อมูลให้เหมาะช่วยทั้งเรื่องพื้นที่ ความเร็ว และความชัดเจนของข้อมูล เช่น
- ใช้
INTเมื่อเพียงพอ - ใช้
BIGINTเมื่อคาดว่าปริมาณจะเกินขอบเขตของINT - ใช้
TIMESTAMP WITH TIME ZONEเมื่อต้องเก็บเวลาในโลกจริงที่เกี่ยวข้องกับหลายเขตเวลา
ไม่ควรใช้ TEXT เก็บทุกอย่างเพียงเพราะสะดวก เพราะการทำ index บนข้อความขนาดใหญ่มีต้นทุนสูง
5) ใส่ constraint เพื่อกันข้อมูลเน่า
หลายคนกลัวว่า constraint จะทำให้เขียนช้าลง แต่ในมุมการดูแลระบบระยะยาว constraint ช่วยลดความเสียหายได้มาก โดยเฉพาะเมื่อข้อมูลเริ่มมีขนาดใหญ่ ความผิดพลาดเล็กๆ จะกลายเป็นงานล้างข้อมูลที่ใช้เวลามหาศาล
ดังนั้นควรใช้ constraint ที่จำเป็นตั้งแต่ต้น เพื่อป้องกันปัญหามากกว่าค่อยมาแก้ทีหลัง
6) สร้าง index ตามวิธีค้นหา ไม่ใช่ตามชื่อคอลัมน์
หลักคิดที่สำคัญมากคือ อย่าสร้าง index เพราะเดาว่าน่าจะดี แต่ให้ดูจาก query ที่ใช้จริงก่อน
ตัวอย่างที่เหมาะสม เช่น
- ค้นหา order ของผู้ใช้ตามเวลา:
index(user_id, created_at) - กรองสถานะแล้วเรียงตามเวลา:
index(status, created_at)
โดยเฉพาะ composite index ลำดับคอลัมน์มีผลมาก คอลัมน์ที่ใช้กรองก่อนควรอยู่ด้านหน้า
7) ระวังการมี index มากเกินไป
ข้อผิดพลาดที่พบบ่อยในมือใหม่คือใส่ index ให้แทบทุกคอลัมน์ ผลคือ query อ่านบางส่วนอาจเร็วขึ้น แต่ระบบเขียนกลับช้าลงแบบหาสาเหตุไม่เจอ
ทางที่ดีควรเริ่มจาก index เท่าที่จำเป็นจริง อาจอยู่ในช่วง 0-3 ตัวแรก แล้วค่อยเพิ่มเมื่อมีหลักฐานจากการใช้งานจริง
8) แยกคอลัมน์ที่โตเร็วออกจากตารางหลัก
ตารางหลักที่ถูก join หรือค้นหาบ่อยควรมีขนาดกะทัดรัด เพื่อให้อ่านเร็วและแคชได้ง่าย ตัวอย่างเช่น
- แยก
usersออกจากuser_profiles - แยก
eventsหรือlogsไปอีกตาราง
การแยกข้อมูลลักษณะนี้ช่วยให้ตารางหลักไม่พองตัวเร็วเกินไป
9) โตแล้วต้องคิดเรื่อง partition และ archive
ข้อมูลประเภท log หรือ event มักโตเร็วมาก การทำ partition ตามวันหรือเดือนจะช่วยให้ query ช่วงเวลาสั้นๆ เร็วขึ้น และยังจัดการลบข้อมูลเก่าได้สะดวกกว่า
หากยังไม่พร้อมทำ partition ตั้งแต่วันแรก อย่างน้อยควรออกแบบให้มี created_at อย่างชัดเจน เพื่อรองรับการขยายในอนาคต
10) วัดก่อนเดา
หลายครั้งปัญหาความช้าไม่ได้มาจากตัวฐานข้อมูล แต่เกิดจาก query ที่เขียนไม่ดี ดังนั้นก่อนจะสรุปว่าต้องเปลี่ยนระบบ ควรตรวจสอบสิ่งต่อไปนี้ก่อน
- เปิด slow query log
- ดู execution plan
- ตรวจสอบสถิติการใช้งาน index
การวัดจากข้อมูลจริงช่วยให้แก้ปัญหาได้ตรงจุดกว่าการคาดเดา
ตัวอย่างภาพจำ: อ่านเร็วแลกเขียนช้า
สมมติว่าตาราง orders มี index (user_id, created_at)
ข้อดีคือเมื่อผู้ใช้เปิดหน้าประวัติการสั่งซื้อ ระบบจะค้นหาข้อมูลได้เร็วมาก เพราะ index ถูกสร้างมาให้รองรับรูปแบบ query นี้โดยตรง
แต่ข้อเสียคือทุกครั้งที่มี order ใหม่ ฐานข้อมูลต้องเพิ่มข้อมูลลงใน index ด้วย ทำให้ภาระฝั่งเขียนเพิ่มขึ้นเล็กน้อย
สำหรับเว็บหรือแอปทั่วไป การแลกแบบนี้มักคุ้มค่า เพราะประสบการณ์ฝั่งอ่านมีผลต่อผู้ใช้โดยตรงมากกว่า
สรุป
ฐานข้อมูลจำนวนมากถูกออกแบบให้ “อ่านเร็ว” เพราะงานจริงมักมีสัดส่วนการอ่านสูง และผู้ใช้สัมผัสความเร็วฝั่งอ่านได้ชัดที่สุด การเร่งการอ่านให้เร็วขึ้นมักอาศัย index, โครงสร้างข้อมูล, constraint และ cache ซึ่งล้วนเพิ่มต้นทุนให้กับการเขียน
สำหรับผู้เริ่มต้น หลักสำคัญคือออกแบบโครงสร้างให้ถูกต้องก่อน จากนั้นค่อยเพิ่ม index ตาม query จริง ควบคุมจำนวน index ไม่ให้มากเกินไป และเตรียมทางสำหรับการโตของข้อมูล เช่น การแยกตาราง การทำ partition และการวัดประสิทธิภาพจากของจริงเสมอ