Technology

เมื่อ Index ไม่ใช่คำตอบสุดท้าย: แก้ปัญหา Database อืดด้วย CQRS

By Arnon Puitrakul - 14 มกราคม 2026

เมื่อ Index ไม่ใช่คำตอบสุดท้าย: แก้ปัญหา Database อืดด้วย CQRS

เชื่อว่า ถ้ามีใครมาปรึกษาว่า Database มัน Query ช้า ทางเลือกแรก ๆ ที่เราจะให้ลองคือการให้ทำ Index ผลที่ได้ออกมาแทบทุกครั้งทุกคนดู Happy เวอร์ ๆ แต่สุดท้าย พอต้องเขียนเยอะ ๆ กลายเป็นว่า แตกกันยับ ๆ การจะแก้ปัญหานี้ CQRS คือคำตอบที่ดี วันนี้เราจะมาเล่าให้อ่านกันว่า ทำไมการทำ Index ทำให้การเขียนช้าลง และถ้าเราจะต้องสร้างระบบที่อ่านเร็ว และเขียนเร็ว เราจะเอา CQRS มาใช้งานอย่างไร

ราคาที่ต้องจ่ายเมื่อใช้ Index

เมื่อเราใส่ Index เข้าไปใน Table แล้ว เรา เมื่อลอง Query ข้อมูลดู เราจะเห็นว่า มันทำได้เร็วกว่าเดิมเยอะมาก ๆ จากหลักวินาที อาจจะเหลือไม่ถึงวินาทีด้วยซ้ำ แต่ไม่มีอะไรที่ได้มาฟรี ๆ มันมีราคาที่ต้องจ่ายของมันอยู่ นั่นคือ ความเร็วในการเขียน ว่าแต่มันเกิดจากอะไร

หาก Table ของเรา ไม่มี Index ข้อมูล ก็จะวางอยู่บน Disk ของเราต่อกันไปเรื่อย ๆ สะเปะสะปะบ้างอะไร้บ้าง เวลาจะหาข้อมูลทีนึง เช่น มี WHERE Clause ตามหลังเกิดขึ้น มันจะต้องทำ Full Table Scan หรือการไล่อ่านตั้งแต่หัวตารางยันท้ายตาราง ถ้าใครจำได้ วิธีการนี้ เราจะได้ Big-O ของ N หรือก็คือ ยิ่งข้อมูลเยอะ การหาก็จะยิ่งช้าไปเรื่อย ๆ

แต่เมื่อเราใส่ Index เข้าไป แทนที่จะวางต่อกันเรื่อย ๆ มันจะถูกจัดเรียงโดยการใช้ Binary Tree เข้ามาช่วย เมื่อเวลาเราต้องการจะหาข้อมูล เราสามารถหาเป็นลำดับขั้นได้ เช่น เรามี 1 พันล้านแถว จากเดิมที่เราต้องไล่ 1 ถึง หนึ่งพันล้าน เราสามารถตัดทีละครึ่ง จาก 1 พันล้าน เหลือ 500 ล้าน เหลือ 250 ล้าน ไล่ลงไปเรื่อย ๆ ได้ ทำให้ Big O มันเหลือเพียงแค่ log(n) เท่านั้น

ปัญหามันจะอยู่ที่การเขียนทันที เพราะเวลามันเขียน มันจะไม่สามารถเขียนต่อท้าย หรือเขียนที่ไหนก็ได้เหมือนเมื่อก่อนแล้ว มันจะต้องเขียนลงไปในตำแหน่งที่ถูกต้อง เพื่อให้ Binary Tree มันยังคงความถูกต้องเหมือนเดิม มันจะต้องเริ่มจากการ Search หรือ Traverse เข้าไปใน Binary Tree ก่อน เพื่อหาตำแหน่งที่ถูกต้อง จากนั้น มันจะเขียนข้อมูลลงไป แต่ชิบหายแล้วละครับ ถ้าหน้านั้นมันดันเต็ม มันจะต้องฉีกหน้านั้นออกมา แล้วเอาข้อมูลส่วนนึงไปยัดไว้อีกหน้านึง อาจจะ งง

อารมณ์เหมือน เราจะเขียนข้อความ ไว้ระหว่างข้อความรอบ ๆ ดีหน่อย ถ้าพื้นที่เหลือมากพอ เราก็เขียนแทรกได้ตรง ๆ แต่ถ้าเกิดไม่พอขึ้นมา เราก็จะต้องเอาข้อความส่วนหนึ่งย้ายไปอยู่หน้าใหม่ แล้วค่อยเอาข้อความที่จะเขียนใส่แทนเข้าไปในตำแหน่งเดิม นั่นทำให้ ถึง Index มันจะทำให้การอ่านดีได้เท่าไหร่แต่ถ้าจะต้องเขียนหนัก ๆ อย่าง Transactioal Database ก็น่าจะไม่ไหวเหมือนกัน

Monolithic Dilemma: ตารางเดียวทำทุกอย่าง

ในยุคแรก ๆ เวลาเราออกแบบ Database เรามักจะทำแบบ Monolithic คือใช้ Model เดียว ทำทั้งสองหน้าที่คือ การเขียน ที่เราต้องการ Data Consistency สูงสุด เลยออกแบบตามหลัก Normalisation เพื่อลดความซ้ำซ้อน ในขณะที่เราต้องการแสดงผลข้อมูลที่หลากหลาย เราเลยต้องเขียน Query ให้ Join เยอะ ๆ เพื่อเอามาแสดง

เราจะเห็นว่า ทั้งสองส่วนนี้มันขัดแย้งกันโดยสิ้นเชิง ฝั่งอ่าน เราต้อง Denormalisation เพื่อให้เราไม่ต้อง Join และใส่ Index เยอะ ๆ อ่านเร็วจริง แต่เขียนช้า กลับกัน ถ้าเราอยากเขียนเร็ว เราจะต้อง Normalisation และเอา Index ออก ทำให้ อ่านช้า แต่ Join หนัก

เมื่อระบบเราใหญ่ขึ้น มี Read/Write Activity เยอะขึ้น การ Lock Row เพื่อเขียนก็จะไปขวางการอ่าน และการอ่านที่ซับซ้อนก็จะแย่ง Resource สำหรับเขียน สุดท้ายระบบก็จะเกิดคอขวดขึ้นทั้งระบบ จนแตกในที่สุดนั่นเอง

แยกมันออกด้วย CQRS

ถ้ามันจะขัดแย้งในตัวมันเองขนาดนี้ งั้นเราก็แยกกัน อย่าฝืนใช้ตารางเดียวกันแค่นั้น นี่คือที่มาของ CQRS (Command Query Responsibility Segregation) เราจะแยกมันออกมาเป็นสองฝั่ง

ฝั่งแรกรับหน้าที่ในการเขียนเป็นหลัก (Write Optimised) เน้นการจัดการกับ Business Logic และการเปลี่ยนแปลงข้อมูล (Create, Update, Delete) โดยการใช้ RDBMS ปกติ อย่าง MySQL และ PostgreSQL เน้นออกแบบโดยใช้ Normalisation เยอะ ๆ เพื่อความถูกต้องของข้อมูล และที่สำคัญ ใส่ Index ให้น้อยที่สุด แค่เอา Primary และพวก Unique Constraint พอแล้ว เพื่อให้การ Insert/Update เร็วที่สุดเท่าที่จะเป็นไปได้ ไม่ต้องเสียเวลา Rebalance Tree

และส่วนของการอ่านเป็นหลัง (Read Optimised) เน้นเสิร์ฟข้อมูลให้ผู้ใช้ดูอย่างเดียวไปเลย พยายามเลือกใช้ฐานข้อมูลที่อ่านเร็วมาก ๆ อย่าง Elasticsearch และ Redis หรือ ๆๆๆ อาจจะใช้ SQL Database แต่เป็น Read Replica ก็ได้ แต่ข้อมูลเราจะพยายามทำให้มัน Flat มากที่สุด กล่าวคือ เมื่อเราจะใช้งาน เราแทบไม่จำเป็นจะต้อง Join Table อะไรเลย ทีนี้แหละ เรายากจะใส่ Index ตรงไหน กดเข้าไปเต็มที่ ไม่ต้องกลัวเขียนช้าแล้ว

อาจจะนึกภาพไม่ออกว่า เราจะเชื่อมต่อส่วนของ Write Optimised และ Read Optimised หากันได้อย่างไร เราจะเอา Event-Driven Architecture เข้ามาช่วย โดยเมื่อ User Trigger คำสั่งบางอย่าง เช่น Insert หรือ Update เราจะเขียนลงไปใน Write Optimised หรือ DB ปกติเลย ซึ่งมันจะเขียนได้โคตรเร็ว เพราะไม่มี Index มาให้รกหูรกตา จากนั้น เราจะส่ง Event ไป เช่น OrderReceived เข้าไปที่ Message Bus อย่าง Kafka หรือ RabbitMQ

ฝั่ง Query Service ก็จะคอยฟัง Event เหล่านี้ แล้วหยิบข้อมูลไป แปะ ลงใน Read DB Format ที่เว็บต้องการ (เราเรียก กระบวนการแปลงข้อมูลเพื่อให้อยู่ในรูปแบบที่หน้าบ้านต้องใช้ว่า Projection) สุดท้าย เวลา User Query หน้าเว็บ มันก็จะไปดึงจาก Read DB อย่าง ElasticSearch มาแสดงผลได้ทันทีแบบ O(1) เลยทีเดียว เพราะไม่ต้อง Join อะไรทั้งนั้น

CQRS ไม่ใช่ Silver Bullet

อ่านถึงตรงนี้ เซี้ยน อยาก Implement CQRS มาใช้งานเลยใช่มั้ยครับ แต่เดี๋ยวก่อน ของฟรีไม่มีบนโลกอีกแล้วละ การทำมันก็มีราคาของมันที่ต้องแลกมาอยู่

อย่างแรกคือ ความซับซ้อน จากเดิมที่เลี้ยงดูแล Database ลูกเดียว จบ ตอนนี้เราต้องดู Database 2 ก้อน + Message Broker + Sync Logic ความซับซ้อนเยอะขึ้นแบบพันล้าน !!! เพราะระบบไม่ได้ Flow ข้อมูลจบใน Request เดียว

อย่างที่สองคือ ความหน่วง เพราะข้อมูลมันต้องวิ่งผ่าน Message Bus ที่มันจะมี Delay อยู่บ้าง เมื่อ User กด Save จะระบบจะตอบ Success และเด้ง User ไปที่หน้า Listing ทันที แต่ปัญหาคือ ข้อมูลยังไม่มี เพราะ Worker ยัง Sync ข้อมูลไม่เสร็จ ดังนั้นพอกลับมาแล้วแทนที่จะมีข้อมูลใหม่อยู่ มันก็จะไม่มี ไม่ขึ้น ทำให้ User เข้าใจว่า มันทำไม่สำเร็จ

ส่วนวิธีการแก้ไขคือ เราอาจจะต้องใช้ Optimistic UI หรือคือ การหลอกไปก่อนว่าเสร็จแล้ว โดยการเอาข้อมูลอาจจะส่งเป็นค่ากลับมาให้ Update Item ที่แสดงบน Frontend อีกทีไปก่อน ระหว่างที่หน้าโหลดจริง ๆ มันฝั่ง Worker มันก็น่าจะทำงานเสร็จแล้วละ

สรุป : เราควรใช้ CQRS เมื่อไหร่

อย่างที่ได้ยกตัวอย่างมา เราจะเห็นข้อดี และ ข้อเสียของการเอา CQRS มาใช้งานแล้ว ถามว่า Best Practice เราควรจะใช้งานมันเมื่อไหร่ เราว่า แบ่งออกเป็น 3 เคสที่เจอบ่อย ๆ

อย่างแรกคือ กลุ่มระบบที่มี Traffic สูงมาก ๆ คือ เราเริ่มมองเห็นแล้วว่า ระบบเริ่มรับ Load ที่เข้ามาไม่ไหวแล้ว CPU Utilisation บน Database Process เด้งไป 100% บ่อย ๆ อันนี้เตรียมคิดเรื่องการ Scale ไว้เลย CQRS เป็นหนึ่งในนั้น

หรือกลุ่มที่มี Read/Write Ratio ต่างกันมาก ๆ เช่น ข้อมูลชุดนี้อ่าน 1,000 ครั้ง ต่อการเขียน 1 ครั้ง ไม่ต้องคิดเลย ชัดเจนมาก ๆ ทำเถอะ โดยเฉพาะถ้าเข้าเคสแรกด้วย

และสุดท้ายคือ กลุ่มที่มีการแสดงผลหน้าเว็บที่อาจจะเป็น Dashboard หรือ Report ที่มีการ Join Table แบบฉ่ำ ๆ หลัก 10 Table และใช้เวลา Query นานหลายวินาที หรือถ้าตอนนี้อาจจะยังไม่รอนานหลายวินาที แต่ถ้าเราเห็นแนวโน้มของการเติบโตของข้อมูล และคาดว่ามันน่าจะทำให้ Query หลักหลายวินาทีได้ ก็รีบทำไปก่อนดีกว่า

แต่เคสที่ไม่เหมาะมาก ๆๆ คือ CURD ทั่ว ๆ ไป คนใช้หลักร้อย จูน Index กับเขียน SQL ดี ๆ ก็เอาอยู่แล้ว ก็อย่า หา ทำ หวังว่าบทความนี้น่าจะทำให้เห็นภาพของการ Scale Database ขึ้นในอีกมิตินึงนะ ถ้าอยากรู้เรื่องอะไรอีก ก็ Comment มาบอกกันได้