รู้สึกเหมือนไม่นาน เพิ่งต้นปีนี้เองที่ทะยอย update MongoDB ให้กับ cluster ของลูกค้าที่ใช้บริการโปรแกรมเป็น version 6 ถ้าจะกล่าวให้ถูกต้อง ตอนนี้ทุก cluster ใช้ version 6 ยกเว้น cluster ขนาดใหญ่แห่งหนึ่ง
เวลาล่วงเลยมาเรื่อยยังไม่ทันได้ update cluster นั้น MongoDB ก็ออก version 7 (สิงหาคม 2023)
หลังจากที่อ่านผ่าน ๆ พบว่ามีฟีเจอร์ใหม่ที่น่ามีประโยชน์กับโปรแกรมของเรา
Compound Wildcard Indexes
ดูจากคำอธิบาย เป็นการปรับปรุงเพิ่มจาก Wildcard Indexes ที่มีให้ใช้ตั้งแต่ version 4.2 ซึ่งตอนนั้นพิจารณาแล้วยังไม่สามารถใช้ประโยชน์กับโมเดลของข้อมูลในโปรแกรมของเราได้
WI เดิมถูกจำกัดให้ใช้กับ field ที่เป็น object ช่วยให้ Database Engine ใช้ประโยชน์จาก index นั้นค้นหาข้อมูลที่เป็น subfield ได้ โดยไม่จำเป็นต้องทำเป็นหลาย index สำหรับแต่ละฟิลด์ย่อย ลักษณะการใช้งานจึงเหมาะกับข้อมูลที่มีโครงสร้างแบบยืดหยุ่น แต่ยังไม่สะดวกเพราะไม่สามารถใช้ร่วมกับฟิลด์อื่นได้
Data segmentation
ก่อนที่จะต่อจิ๊กซอว์ 1,000 ชิ้น เราจะเริ่มจากสำรวจแล้วคัดแยกชิ้นส่วนออกเป็นกลุ่มใหญ่ ๆ ตามคุณสมบัติเด่นบางอย่าง เช่น ชิ้นส่วนที่เป็นขอบภาพ ชิ้นส่วนที่มีสีเด่นเหมือนกัน
หากชิ้นส่วนมีจำนวนน้อยเราไม่จำเป็นต้องเสียเวลาคัดแยก ขณะเดียวกันหากชิ้นส่วนมากเกินไป หากเลือกสีเด่นไม่เหมาะสม ก็ทำให้เสียเวลามัวแต่คัดแยก
วิธีช่วยให้ database ค้นหาข้อมูลได้รวดเร็วก็ใช้หลักคิดคล้ายกัน หาคุณสมบัติบางอย่างที่สามารถคัดแยกข้อมูลออกเป็นกลุ่มใหญ่ ๆ ได้ และสอดคล้องกับพฤติกรรมการใช้ข้อมูล
โปรแกรมของเราเป็น ERP ข้อมูลที่มีอัตราสะสมเพิ่มมากที่สุด เป็นข้อมูลเอกสาร
โมเดลของข้อมูลจึงมีฟิลด์ "ประเภท" สำหรับแบ่งกลุ่มเอกสารเป็น ซื้อ, ขาย, จ่าย, รับ ฯลฯ ธรรมชาติการใช้ข้อมูลเพื่อทำงานหรือรายงานต่าง ๆ มากกว่า 90% ก็มักจะอ้างอิงหรือวนเวียนอยู่กับประเภทเดียวกัน
สมมติว่ามีเอกสารทั้งหมดในระบบ 1 ล้านข้อมูล หากสามารถกรองด้วย "ประเภท" ได้ก่อน ก็จะทำให้เหลือเฉพาะข้อมูลของประเภทที่ต้องการนั้นในชั้นแรก หลังจากนั้นจึงเป็นการกรองข้อมูลชั้นละเอียด เช่น ประวัติการขายของลูกค้า A หากดาต้าเบสสามารถข้ามข้อมูลที่ไม่ใช่ประเภท "ขาย" ด้วย index จะทำให้เหลือปริมาณเอกสารที่ต้องหาชื่อลูกค้า A น้อยลง ได้คำตอบเร็วกว่าหาชื่อจากเอกสารทั้งหมด
Time awareness
ตอนที่ออกแบบโปรแกรมครั้งแรกผมตั้งใจให้เริ่มต้นด้วยช่องค้นหา ที่ผู้ใช้สามารถใส่คำค้นอะไรก็ได้ หมวดบิล เลขที่บิล ชื่อลูกค้า ชื่อสินค้า ฯลฯ ผลลัพธ์ที่ได้จากแสดงข้อมูล 20 อันดับที่เกิดขึ้นล่าสุด
เวลาผ่านไปหลายปี กลุ่มผู้ใช้ที่มี database ขนาดใหญ่เริ่มเกิดปัญหา บางครั้งใช้เวลาค้นหานาน หากคำค้นนั้นหาไม่พบหรือได้ผลลัพธ์ไม่ถึง 20 อันดับ สาเหตุเกิดจากต้องสแกนข้อมูลทั้งหมด
นำไปสู่การตั้งคำถามว่า อะไรคือสิ่งที่ผู้ใช้คาดหวัง ความเร็วในการหาคำตอบ หรือความครบถ้วนของคำตอบ
ขอบเขตของ "วันที่" หรือ active period เป็นอีกฟิลด์ที่ใช้กับ transactional data ช่วยจำกัดขอบเขตการค้นหาไม่ให้เป็นภาระหนักเกินไป
ยกตัวอย่างเช่น ผู้ใช้ต้องการค้นหาข้อมูล
"กรุงเทพ"
ถ้าคิดแบบตรงไปตรงมา เราก็ควรใช้คำค้นนี้ไปหาจาก database ทั้งหมด ซึ่งช่วงปีแรกของการใช้งานมักไม่มีใครสังเกตเห็นความแตกต่าง แต่เมื่อเวลาล่วงเลยไปผลลัพธ์ที่รวมจากข้อมูลทั้งหมดตั้งแต่ 10 ปีที่แล้วอาจไม่ใช่คำตอบที่ผู้ใช้คาดหวัง
สำหรับ database ขนาดใหญ่ ผมเลือกออกแบบเพื่อจำกัดขอบเขตการค้นหา หากไม่ระบุช่วงเวลา จะสะท้อนคำตอบที่เชื่อมโยงกับปัจจุบันก่อน ป้องกันปัญหาการค้นหาข้อมูลย้อนหลังที่ยาวนานเกินไปโดยไม่จำเป็น
"กรุงเทพ" + (ที่อยู่ในช่วง active period)
ซึ่งเราสามารถกำหนดค่า default สำหรับกิจการแต่ละแห่งไม่เหมือนกันได้ อาจจะเป็น 12 เดือนล่าสุด หรือ 3 ปีล่าสุด ขึ้นอยู่กับความเร็วของรอบวงจรธุรกรรม ธุรกิจที่เป็น B2C จะมีช่วงเวลาสั้น ขณะที่ธุรกิจ B2B ยิ่งให้เครดิตลูกค้ายาวเท่าไหร่ ก็จะมีช่วง active period ยาวนานตามไปด้วย
Flexible data model
โปรแกรม ERP ที่ออกแบบมาให้รองรับธุรกิจที่แตกต่างกัน ภายใต้โครงสร้างระบบบัญชีที่มีจุดร่วมบางส่วนเหมือนกัน แต่รายละเอียดนอกเหนือจากนั้นเป็นอะไรที่เพียงคล้ายกัน แต่ไม่เคยเหมือนกัน
ขึ้นอยู่กับโครงสร้างองค์กรที่แตกต่าง หรือกล่าวให้ชัด SWOT ไม่เหมือนกัน ทำให้แม้กระทั่งธุรกิจประเภทเดียวกัน ต่างก็ออกแบบระบบงานเพื่อเสริมจุดแข็งและปิดจุดอ่อนต่างกัน แบ่งแผนกฝ่ายบทบาทและหน้าที่รับผิดชอบไม่เหมือนกัน บางแห่งมีหลายสาขา บางแห่งมีพนักงานขาย บางแห่งใช้คลังสินค้ากลาง บางแห่งแยกคลังตามสาขา ฯลฯ
ความแตกต่างเหล่านี้ หลายครั้งนำไปสู่การเก็บรายละเอียดของข้อมูลไม่เท่ากัน ฟิลด์ "สาขา", "พนักงานขาย", "คลังสินค้า" ย่อมเป็นส่วนเกินสำหรับผู้ที่ไม่ได้ใช้ แต่เป็นสิ่งจำเป็นสำหรับผู้ที่ต้องการ เหล่านี้คือสิ่งที่ MongoDB ออกแบบมาแก้ปัญหา relational model ให้การปรับข้อมูลในดาต้าเบสภายหลังเป็นเรื่องง่าย และตรงไปตรงมา
ตัวอย่างกิจการแห่งหนึ่ง เมื่อเริ่มต้นไม่มีสาขา สามปีหลังจากนั้นได้ขยายสาขาเพิ่ม โปรแกรมก็เพียงเพิ่มฟิลด์สาขา เพิ่มหมวดเอกสาร แล้วใช้งานต่อเนื่องภายใต้ database เดิม
ข้อจำกัดอย่างหนึ่งของการมีฟิลด์ยืดหยุ่น คือการสร้าง index ที่สอดคล้องกับการใช้งานข้อมูล การสร้าง index เผื่อใช้ไว้จำนวนมาก ทำให้การบันทึกข้อมูลช้า เพราะทุกครั้งที่บันทึกข้อมูล ต้องปรับปรุง index ทั้งหมดด้วย
การเพิ่มฟิลด์สำคัญเข้ามาอาจหมายถึงตัวคูณทำให้ต้องมี index จำนวนมาก เช่น เดิมเราอาจมีรายงานยอดขาย ซึ่งแสดงยอดขายรวมทั้งหมด แต่พอมีสาขา ก็จะต้องมีรายงานยอดขายของสาขา แสดงว่า "สาขา" อาจเป็น data segmentation ที่ช่วยให้ database ค้นหาได้เร็วขึ้น
กลายเป็นสถานการณ์ที่ต้องชั่งน้ำหนักเลือก
หากต้องการค้นหาภายในสาขาเร็ว ต้องเพิ่ม index ฟิลด์สาขา แต่อาจทำให้การบันทึกข้อมูลช้า เพราะจำนวน index เพิ่มขึ้น และอาจทำให้ server ต้องใช้ memory เพิ่มขึ้น
บังเอิญว่าบรรดาฟิลด์พิเศษที่ customize ให้กับกิจการแต่ละแห่ง ถูกออกแบบให้เก็บอยู่ภายใต้ฟิลด์ชื่อ "info" อยู่แล้ว
นักพัฒนาสามารถออกแบบให้มีฟิลด์ย่อยเก็บข้อมูลอะไรก็ได้ ภายใต้ info เช่น ต้องการให้เก็บรายละเอียดการจัดส่งสินค้า ก็สามารถเพิ่มฟิลด์ ผู้จัดส่ง และ รถจัดส่ง info.sender, info.sendcar โดยไม่กระทบต่อโค้ดใน database abstraction layer
CWI (Compound Wildcard Indexes) รองรับ segmentation + wildcard จึงน่าจะตอบโจทย์กรณีนี้
ดังนั้นผมสามารถใช้ CWI ให้ประกอบด้วยฟิลด์ "ประเภท" สำหรับ segmentation แล้วมี "info" เป็น wildcard ที่ครอบคลุมฟิลด์ย่อยที่อาจแตกต่างกันได้ โดยไม่จำเป็นต้องคอยปรับเพิ่ม index ให้ฟิลด์ย่อยเหล่านั้นอีกต่อไป
key: { "_type": 1, "info.$**": 1 }
หรือ
key: { "_date": 1, "info.$**": 1 }
What's next
เรื่องที่จะต้องทำต่อไป คือการทดสอบ นอกจากความสะดวกในการดูแล index แล้ว ปัจจัยอื่นผมยังไม่แน่ใจ ว่าการใช้ CWI จะต้องแลกกับอะไรบ้าง
ประสิทธิภาพในการบันทึกข้อมูล
ประสิทธิภาพในการค้นหาข้อมูล
ขนาดของ index ใหญ่แค่ไหน
ส่วนที่คาดหวังเพิ่ม (ยังทำไม่ได้ใน version นี้)
อยากให้สามารถกำหนด options wildcardProjection
เลือกหรือยกเว้นฟิลด์ตาม datatype เช่น ไม่ต้องการให้สร้าง index ของ subfield ที่เป็น array หรือ ต้องการเฉพาะ subfield ที่เป็น primitive value
เลือกหรือยกเว้นฟิลด์ตาม glob patterns เช่น ต้องการให้สร้าง index เฉพาะ subfield ที่ชื่อ_* (ขึ้นด้วย underscore)
กำหนดจำนวนชั้นความลึกของ subfield ที่ใช้เป็น index ได้ (สามารถใช้เงื่อนไขนี้ กรองฟิลด์ที่ไม่ใช่ primitive value ได้ด้วย)
และอยากให้มี aggregation pipeline stage คล้ายกับ $search แต่ใช้กับ CWI ได้
ไม่รู้ว่าขอมากไปรึเปล่า เพราะ use case ของผู้ใช้ MongoDB มีหลากหลายมากมายเหลือเกิน
อ้างอิง
4 Big Reasons to Upgrade to MongoDB 7.0 https://www.mongodb.com/blog/post/4-big-reasons-upgrade-mongodb-7-0
MongoDB 7.0 Compound Wildcard Indexes https://www.mongodb.com/docs/v7.0/core/indexes/index-types/index-wildcard/index-wildcard-compound/
Comentários