top of page
ค้นหา
รูปภาพนักเขียนSathit Jittanupat

Art of query, How to tame your MongoDB



ผมใช้ MongoDB ผ่าน mLab และต่อมา Atlas นานกว่า 5 ปี ข้อดีที่สำคัญที่สุดคือ เป็นดาต้าเบสที่ต้องใช้คำว่า "forgive" มือใหม่ไม่โดนลงโทษ ยืดหยุ่นต่อความไม่สมบูรณ์ ไม่บังคับให้ต้องออกแบบ data model ก่อน ไม่จำเป็นต้องชำนาญตั้งแต่วันแรกที่เริ่ม เอื้อต่อการใช้ไปปรับปรุงไปแบบ agile ค่อยๆ เพิ่มรายละเอียดตามความซับซ้อนของงานในอนาคต โดยไม่จำเป็นต้องเผื่อหรือออกแบบไว้ล่วงหน้า ช่วยเริ่มงานได้เร็ว ทำไปเรียนรู้ไป กระบวนการ analyze กับ develop จะหลอมรวมเป็นเนื้อเดียวกัน


เมื่อมองย้อนกลับไป ผมสามารถเริ่มใช้งานจริงโดยไม่ต้องรู้หรือระวังอะไรมาก ยังมีเวลาที่จะเรียนรู้ทำความเข้าใจกลไกดาต้าเบส ประสิทธิภาพจะเริ่มสำแดงข้อแตกต่างชัดเจน เมื่อข้อมูลสะสมระดับหลายแสนไปแล้ว


เมื่อถึงตอนนั้น สัญญาณเตือนจาก Atlas แจ้งว่ามี Document Scan กับข้อมูลจำนวนมาก เริ่มทะยอยเข้ามา แต่คุณก็อยู่กับมันนานจนคุ้นเคย ได้เวลาทำความใจเรื่อง index และการออกแบบ query ที่ดี เพื่อให้ดาต้าเบสกลับไปมีประสิทธิภาพอีกครั้ง



ช่วงปีหลัง ผมอยู่ในห้วงเวลา serious optimization เนื่องจากคลัสเตอร์ดาต้าเบสที่ดูแลเริ่มทะยอยเข้าสู่ระดับที่ต้องปรับจูนอย่างละเอียด มีรายงานหลายตัวที่เคยใช้ได้เริ่มช้าไม่เหมือนเดิม Atlas มีแจ้งเตือนเกี่ยวกับ query ที่อาจก่อเกิดปัญหาเข้ามาถี่ขึ้น


ต่อไปนี้คือ เรื่องราวที่ได้เรียนรู้เกี่ยวกับ MongoDB


" Data in memory is fast "

ดาต้าเบสมีกลไกที่เรียกว่า cache ทำหน้าที่พักข้อมูลเอาไว้ เพื่อลดการอ่านข้อมูลจาก disk ที่ช้ากว่า จึงเป็นคำอธิบายว่า เมื่อคุณใช้ query เหมือนกัน 2 หน ทำไมครั้งแรกจึงช้า แต่ครั้งที่สองเร็ว เพราะครั้งแรกต้องอ่านข้อมูลจาก disk ที่ช้ากว่าก่อน แต่ครั้งที่สองข้อมูลนั้นยังคงอยู่ใน cache จึงไม่ต้องเสียเวลาอ่านจาก disk อีก


ที่จริงแล้วกลไก cache ของดาต้าเบสซับซ้อนเกินกว่าที่จะอธิบายได้ การแก้ปัญหาดาต้าเบสที่ทำงานช้าแบบง่ายที่สุดคือการเพิ่ม memory ให้ server ทำให้ดาต้าเบสสามารถเอามาทำ cache ที่ใหญ่ขึ้น แต่ราคาของ memory ก็สูงจนไม่อาจเพิ่มเผื่อไว้จนเกินพอดี


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


เมื่อข้อมูลมีขนาดใหญ่ขึ้นทุกวัน การใช้ดาต้าเบสตัวเดียว ที่ทำหน้าที่ทั้งงานประจำวัน (Operational Database) และงานวิเคราะห์ (Analytical Database) การบริหารจัดการให้ดีทั้งสองด้านนั้นยากมาก ขณะที่งานประจำวันมักใช้ข้อมูลปัจจุบัน แต่เพราะงานวิเคราะห์ข้อมูลมีการอ่านข้อมูลที่ย้อนหลังไปยาวนาน มีโอกาสที่ข้อมูลย้อนหลังเหล่านั้นไม่อยู่ใน cache ทำให้เกิดการ swap ข้อมูลใน cache ระหว่างปัจจุบันที่ใช้กับงานประจำ กับข้อมูลใช้วิเคราะห์ เข้าๆ ออกๆ จนทำไม่ได้ดีสักอย่าง ยิ่ง memory น้อยเมื่อเทียบกับขนาดข้อมูล ยิ่งมีโอกาสที่เกิดการ swap บ่อย


"Indexes are always in memory"

ส่วนที่ดาด้าเบสพยายามไม่ swap ออกไปจาก cache คือ index ดังนั้นการสร้าง index ให้ดาต้าเบส คือการบอกให้รู้ว่าฟิลด์เหล่านั้นมีโอกาสถูกใช้ค้นหาบ่อย


query ที่ค้นหาฟิลด์ตรงกับ index จึงเร็วกว่าแน่นอน เพราะจะได้คำตอบโดยไม่ต้องเสี่ยงวัดดวงกับ cache


ใน Atlas มีฟ้องเตือนความเสี่ยงของ query ที่เกิด "Document Scan" ต้องอ่านข้อมูลเต็ม (document) เพื่อใช้ฟิลด์ที่ไม่อยู่ใน index การทำงานจะเริ่มจากตรวจด้วย index เท่าที่ทำได้ ตัดให้เหลือข้อมูลน้อยที่สุดก่อน แต่ถ้าตัดแล้วก็ยังเหลือมากระดับพันหรือหมื่น การเอาข้อมูลเต็มมาใช้ตรวจ เป็นการวัดดวง หากอยู่ใน cache ก็เร็ว แต่ถ้าไม่อยู่ก็ช้า


" Complexity of 'sort' and 'skip' + 'limit' "

การค้นหาแบบ Document Scan จะหยุดเมื่อได้คำตอบครบตามเป้าหมาย 'skip' + 'limit' จะเป็นจริงเฉพาะเมื่อ 'sort' ตรงกับ index หรือถ้ายังไม่ครบก็ต้องไล่หาจนสุดข้อมูลในดาต้าเบส


ตอนที่ข้อมูลมีจำนวนไม่มาก โอกาสข้อมูลส่วนใหญ่พักอยู่ใน cache ทำให้รู้สึกว่าใช้เวลาไม่นาน แทบไม่เห็นความแตกต่างระหว่างการมีหรือไม่มี index แต่ถ้าดาต้าเบสใหญ่เกินกว่า cache แล้ว การหาไม่เจอ หรือหาได้ไม่ครบตาม 'limit' จะทำให้ช้าจนเห็นได้ชัด


ประเด็นที่ต้องคำนึงมีดังต่อไปนี้

- การใช้ 'sort' ทำให้ดาต้าเบสมีทางเลือกที่จะใช้ index ไม่มาก

- หาก index ไม่สามารถครอบคลุมฟิลด์ใน query ก็ต้องทำ Document Scan มีราคาต้องจ่าย ขึ้นอยู่กับ cache

- 'limit' ช่วยให้เร็วเฉพาะ query ที่ไม่ต้องทำ in memory sort


query ประเภทที่ใช้หาคำตอบ 100 อันดับแรก หรือ 20 อันดับสูงสุด จะต้องประกอบด้วย 'sort' + 'limit' หากคำสั่ง 'sort' นั้น ใช้ฟิลด์ที่ไม่มีอยู่ใน index ดาต้าเบสจำเป็นต้องหาข้อมูลให้ครบถ้วนผ่าน Document Scan ก่อน แล้วเอามาเรียงใหม่ด้วยวิธี "in memory sort" อีกที


หากผลลัพธ์จาก query ตาม index ยังมีจำนวนมาก ก็พลอยทำให้ขั้นตอน Document Scan อาจใช้เวลานาน พึงระลึกไว้เสมอว่า ไม่ควรใช้ 'sort' โดยไม่จำเป็น และพยายามหลีกเลี่ยงหากไม่แน่ใจว่าฟิลด์ที่ต้องการ 'sort' นั้นมี index หรือไม่


" Pagination inconsistent "

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


ใช้ 'sort' + 'skip' + 'limit' เป็นวิธีที่ตรงไปตรงมา นิยมใช้กันมาก แต่ข้อเสียดังที่กล่าวมาแล้ว หาก query นั้นทำให้เกิด Document Scan และหากค่าของ 'skip' + 'limit' รวมกันแล้วเป็นเลขที่สูงมาก ก็จะทำให้ช้า นอกจากนี้หากข้อมูลที่ query นั้นเปลี่ยนแปลงตลอดเวลา ผลลัพธ์ของ 'skip' + 'limit' ในแต่ละรอบ มีโอกาสคลาดเคลื่อนซ้อนซ้ำกัน ทำให้ข้อมูลไม่เรียงต่อกันสมบูรณ์ จึงเหมาะสำหรับงานบางประเภทที่ไม่ซีเรียสการแสดงข้อมูลซ้ำซ้อน แต่ไม่สามารถใช้กับงานที่เป็นรายงานใช้ตรวจสอบ


อีกวิธีหนึ่งคือ read more ใช้ 'sort' ด้วย _id ของ MongoDB ซึ่งเป็น unique key ใช้เรียงก่อนหลังตามลำดับตอน insert แล้วใช้ query "$gt" หรือ "$lt" เพื่อหาข้อมูลถัดไปขึ้นอยู่ว่าเป็นการหาจากมากไปน้อย หรือน้อยไปมาก วิธีนี้แก้ปัญหาข้อมูลซ้ำซ้อนได้ แต่มีข้อเสียที่สำคัญ เพราะไม่สามารถเลือกใช้ index อื่นนอกจาก _id หาก query ต้องตรวจฟิลด์อื่นด้วย จะทำให้เกิด Document Scan ไล่ค้นข้อมูลไปทีละ _id เสมอ


ดังนั้นการเลือกใช้ pagination query จึงขึ้นอยู่กับบริบท การแสดงบนหน้าจอ เช่น catalog สินค้า ที่ไม่มีปัญหาหากแสดงข้อมูลเกิดซ้ำกันช่วงรอยต่อขึ้น page ใหม่ สามารถใช้ 'sort' + 'skip' + 'limit' ได้ แต่ถ้าเป็นรายงานตรวจสอบ จำเป็นต้องคำนวณตัวเลขรวมยอด จะต้องชั่งน้ำหนักให้ดีระหว่างการใช้ _id + 'limit' หรือกลั้นใจบังคับให้อ่านข้อมูลทั้งหมดมาในคราวเดียว โดยไม่ต้องใช้ 'sort' เพราะข้อดีของการไม่ใช้ 'sort' ทำให้ดาต้าเบสมีโอกาสเลือก optimize ด้วย index ที่ดีที่สุดได้


" Transforming aggregation should read all at once "

Aggregation pipeline stage ที่ใช้เปลี่ยนโครงสร้างข้อมูลเช่น '$group' เป็นอีกจุดหนึ่งที่ควรระวัง เพราะจำเป็นต้องใช้ข้อมูลเพื่อทำการ group ข้อมูลจนจบสิ้นทั้งหมดก่อน จึงทำงาน stage ต่อไปได้


ดังนั้นไม่มีประโยชน์ที่จะทะยอยอ่านข้อมูลทีละก้อนมาต่อกันด้วย 'skip' หรือ 'limit' ยกเว้นแต่ว่าต้องการคำตอบประเภท 10 อันดับสูงสุด


"Carefully use 'count' and 'distinct' "

'count' และ 'distinct' เป็นคำสั่งที่จำเป็นต้องอ่านข้อมูลไปจนสุดตามเงื่อนไข query เพื่อให้ได้คำตอบ ดังนั้นจึงควรระมัดระวังในการใช้ หาก query ประกอบด้วยฟิลด์ที่ไม่อยู่ใน index หรือ distinct ฟิลด์ที่ไม่อยู่ใน index อาจทำให้เกิด Document Scan ได้เช่นกัน


" Size of result matter, '$unwind' is costly "

พยายามใช้ `$project` เพื่อกำหนดฟิลด์ที่ต้องการใช้เท่าที่จำเป็น การทำงานผ่านอินเตอร์เน็ต ขนาดของข้อมูลที่แตกต่างกันมีผลต่อเวลาที่ใช้ในการรับส่งข้อมูล บางครั้งมีผลให้รู้สึกว่าช้าหรือเร็วมากกว่าเวลาที่ใช้ในการประมวลผล query ในดาต้าเบสเสียอีก


การใช้ '$unwind' เพื่อกระจาย subdocument ใน array ออกมาเป็น document เดี่ยว ขณะที่ฟิลด์อื่นที่อยู่นอก array จะกลายเป็นฟิลด์ซ้ำ ทวีคูณตามจำนวนสมาชิกใน array หมายความว่าขนาดของผลลัพธ์ที่ได้หลังจาก '$unwind' จะมีขนาดใหญ่ขึ้นจากฟิลด์ซ้ำ จนมีผลต่อความเร็วในการส่งข้อมูลได้


" Use 'maxTimeMS' for circuit breaker "

เป็นสุดยอดกระบวนท่าไม้ตายเพื่อเอาชีวิตรอด ที่ควรใช้กับทุก query อย่าปล่อยให้ดาต้าเบสทำงานไปเรื่อยๆ โดยไม่กำหนดเวลาสิ้นสุด หากเจอเพียง query เดียวที่ใช้เวลานานหลายนาที นอกจากจะบล็อกไม่ให้ query ที่ตามหลังมาทำงานแล้ว อาจทำให้เซิร์ฟเวอร์ล่มได้


ทั้งหมดที่เล่ามา เป็นประสบการณ์ตรงจากการสังเกตและค้นคว้า ลองผิดลองถูกด้วยตัวเอง บางเรื่องอาจไม่สามารถหาแหล่งอ้างอิงได้ หลายกรณีขึ้นอยู่กับเงื่อนไขแวดล้อมอื่นด้วย การออกแบบ query รวมทั้ง index จึงควบคู่ไปกับการทำความเข้าใช้ลักษณะการใช้งานของผู้ใช้ ธรรมชาติการกระจายตัวของข้อมูลในดาต้าเบสนั้นๆ ด้วย การวัดสถิติความถี่ของการเรียกใช้ แต่ละ query ในโลกจริงของผู้ใช้ บางครั้ง query เมื่อทดสอบเดี่ยวก็ไม่ช้า แต่เมื่อเอาไปใช้ มีปัจจัยเรื่องการทำงานหลากหลายเวลาเดียวให้พิจารณาด้วย หากคนหนึ่งต้องการดูรายงานวิเคราะห์ยอดขายของปีที่แล้ว อีกคนหนึ่งต้องการดูยอดค้างชำระของเดือนนี้ ก็อาจเกิดการสลับ cache เพราะข้อมูลที่ต่างขอบเขตกันมาก ก็อาจทำให้ช้าได้แล้ว


หวังว่าข้อสังเกตที่เล่ามาจะเป็นประโยชน์สำหรับชาว Mongorian ทั้งหลายไม่มากก็น้อย หากมีข้อผิดพลาดประการใดขอน้อมรับทุกกรณี


July 2022 / Sathit J.



ดู 27 ครั้ง0 ความคิดเห็น

โพสต์ล่าสุด

ดูทั้งหมด

Comentarios


Post: Blog2_Post
bottom of page