ในโลกของ Database เรามักต้องมี unique id ที่ถูกใช้เป็น id ประจำแต่ละ record โดย id ที่ว่านี้ต้องไม่ซ้ำกัน เช่นใน database ที่เก็บข้อมูลโดยใช้แนวคิดแบบตาราง ก็มักใช้ id ดังกล่าวเป็น primary key ส่วนใน database แบบ documented อย่าง MongoDB ก็จะมี field ชื่อ _id
ทำหน้าที่เป็น primary key เช่นกัน โดยที่ MongoDB จะสร้าง unique id ให้ field _id
โดยอัตโนมัติ และค่าใน field นี้มี Data Type เป็น ObjectId ซึ่งแน่นอนว่ามัน (ควร) ไม่ซ้ำกัน
สำหรับใครที่ใช้ MongoDB มาแล้ว เมื่อมีการ insert เกิดขึ้นเราจะเห็น field _id
บรรจุค่าหนึ่งเอาไว้ ค่านั้นมี type เป็น ObjectId
ที่ถูก generate ขึ้นโดยอัตโนมัติโดย MongoDB มีลักษณะดังนี้
{ "_id" : ObjectId("5dcfb758ba596f3a9dbb6700") }
แล้วค่าที่อยู่ใน field _id
นี้มันคืออะไรกันแน่? มัน unique อย่างไร? ได้มาจากไหน? ก่อนอื่นเรามารู้จัก BSON ก่อน
Binary JSON (BSON)
MongoDB เก็บข้อมูลแต่ละ record ในรูปของ JSON โดยเก็บเป็น binary โดยอิงตาม BSON specification นี้ ซึ่งก็มีการนิยาม Data Type อะไรต่างๆ และ Data Type ที่ถูกใช้ใน field _id
ก็คือ ObjectId
นั่นเอง และถ้าเราดูใน specification ดังกล่าวจะพบว่า ObjectId
นั้นใช้พื้นที่ 12 bytes ในการเก็บข้อมูล
ตั้งแต่ MongoDB 3.4 เป็นต้นมา พื้นที่ 12 byte สำหรับใช้เก็บ ObjectId
ถูกแบ่งออกเป็น 3 ส่วนดังนี้
- 4-byte แรกเป็น UNIX Timestamp คือ วินาทีตั้งแต่ Unix epoch
- 5-byte ถัดมาเป็นค่าที่สุ่มขึ้นมา
- 3-byte สุดท้ายเป็นจำนวนนับ โดยนับเริ่มจากค่าที่สุ่มขึ้นมาอีกที
จากรูปด้านบน ObjectId นั้นเหมือนว่าจะ Global Unique เลยล่ะ และค่อนข้างรับประกันได้เลยว่าในแต่ละ collection นั้น ค่าที่ได้ต้อง unique แน่นอน ถึงแม้ว่า field _id
ใน collection หนึ่งๆ จะถูก generate ขึ้นมาก็ตาม แต่ MongoDB ก็ยังเปิดโอกาสให้เรากำหนดค่าเองได้เช่นกัน ตราบใดค่าที่เรากำหนดขึ้นมาเองยัง uniqe อยู่ ก็ไม่มีปัญหาอะไร
จากตัวอย่างที่ยกมา
{ "_id" : ObjectId("5dcfb758ba596f3a9dbb6700") }
เรามาลองแปลงเลข 4 byte แรกซึ่งก็คือ 5dcfb758
เป็นเวลา ได้ดังนี้
let hex = '5dcfb758'
let decimal = parseInt(hex, 16)
let date = Date(decimal)
// 'Sat Nov 16 2019 16:54:18 GMT+0700 (Indochina Time)' วันนี้!
ก่อนจะมาเป็น ObjectId ในวันนี้
รูปแบบในการสร้าง ObjectId นั้นเปลี่ยนไปเรื่อยๆ ก่อนหน้านี้รูปแบบจะแบ่งออกเป็น 4 ส่วนดังนี้
- 4-byte เป็นวินาทีตั้งแต่ Unix epoch
- 3-byte เป็น machine identifier
- 2-byte เป็น process id
- 3-byte เป็นจำนวนนับ เริ่มจากเลขสุ่ม
ตอนแรกดูเหมือนจะ unique และโอกาสซ้ำกันน่าจะน้อยมากแน่ๆ ซึ่งตอนหลังก็ถูกเปลี่ยนแปลงจาการใช้ machine identifier กับ process id มาเป็นเลขสุ่มจนได้ เนื่อจาากการที่ 5-byte ที่เป็น machine specific และ process id นั้นมีโอกาสซ้ำกันจากการเกิดขึ้นของ Virtual Machine (VMs) ที่มี MAC Address เดียวกันและ process ที่ถูก start มาในลำดับเดียวกัน ดังนั้น machine id กับ process id ถึงถูกนำออกไปแล้วเปลี่ยนเป็นค่าสุ่มแทน
Unique แค่ไหน?
ลองคำนวนเล่นๆ ดูว่า ค่าใน field _id
ที่ถูกสร้างขึ้นมานั้นมีโอกาสซ้ำกันมากแค่ไหน การสุ่มค่าให้กับ 8 byte สุดท้ายใน implementation ณ ปัจจุบันทำให้โอกาสสร้าง ObjectId
ที่ซ้ำกันเกิดขึ้นน้อยลงมาก แต่อย่างไรก็ตาม โอกาสที่จะซ้ำกันก็ยังขึ้นอยู่กับจำนวนการ insert ต่อวินาทีอยู่ดี ยิ่ง insert ต่อวินาทีที่เยอะมากขึ้นเท่าไหร่ โอกาสซ้ำกันก็ยิ่งมากขึ้นตามไปด้วย
ถ้าเรา insert 1 document ต่อ 1 วินาที จะพบว่า 4 byte แรกที่เป็น timestamp ไม่มีทางซ้ำกันแน่นอน ดังนั้น ObjectId
ที่ได้ ต้องไม่ซ้ำกันแน่นอนอยู่แล้วไม่ว่า byte ที่เหลือต่อมาจะมีค่าเป็นอะไรก็ตาม
แต่ถ้าเรา insert มากกว่า 1 document ต่อ 1 วินาที แปลว่าต้องมีอย่างน้อย 2 object ที่มี 4 byte แรกเหมือนกัน (เพราะเกิดขึ้นในวินาทีเดียวกัน) ดังนั้น จึงต้องมาดูกันต่อว่า โอกาสที่ 8 byte ที่เหลือนั้นมีโอกาสซ้ำกันแค่ไหน
ความเป็นไปได้ของ 8 byte ที่เหลือ (5 random + 3 random) จะเป็น 2^(8*8) ซึ่งมีค่าเป็น 1.84467441 x 10^19 หรือก็คือโอกาสที่จะสร้าง ObjectId
ซ้ำกันใน 1 วินาทีจะเป็น 1 ใน 18,446,744,100,000,000,000 นั่นเอง! ซึ่งต่อให้เป็นระบบที่มีอัตราการเขียนสูงๆ ที่ทำได้ 250,000,000 ครั้งต่อวินาที โอกาสซ้ำกันก็ยังอยู่ในอัตราส่วนที่น้อยมากๆ อยู่ดี
ถ้าเราเดินเล่นในอเมริกาโดยไม่กลัวว่าจะถูกฟ้าผ่าแล้วล่ะก็ เรายิ่งไม่ต้องกลัวว่าจะบังเอิญสุ่มแล้วได้ ObjectId
ซ้ำกัน เพราะโอกาสที่เราจะถูกฟ้าผ่าในอเมริกาคือ 1 ใน 700,000 (National Geographic) ซึ่งนั่นยังมากกว่าโอกาสที่ ObjectId
จะถูกสุ่มได้ซ้ำกันมากมายมหาศาลเลยล่ะ!
สรุป
ObjectId
เป็น data type ที่ถูกนิยามไว้ใน BSON Specification ที่ MongoDB ใช้สำหรับเก็บข้อมูล มันเป็น binary representation ของ JSON เช่นเดียวกับ data type อื่นๆ และมันคือ data type ที่นำมาใช้สร้างเป็น unique identifier ได้ดีเลยทีเดียว
- เราสามารถสร้าง
ObjectId
ขึ้นมาเองได้ กำหนดค่าเลขฐาน 16 ให้ก็ได้ หรือแม้แต่ดึงค่าตัวเลขต่างๆ ออกมาก็ได้ อ่านวิธีการได้ที่เอกสารอย่างเป็นทางการ - เราสามารถดึงเวลาที่ ObjectId นั้นถูกสร้างขึ้นมาได้จาก
getTimeStamp()
- การ sort by
_id
นั้นมีค่าแทบจะเทียบเท่าการเรียงตามเวลาสร้าง
Reference: