UUIDv4 vs Auto-Increment: ความลับของ B-Tree ที่ถูกสุ่มพังจนดิสก์ระเบิด
⏱️️ ใช้เวลาอ่านประมาณ: 4 นาที 25 วินาที
ยุคเริ่มเขียนเว็บ ทุกคนใช้ id BIGSERIAL PRIMARY KEY ตัวเลขวิ่งเรียง 1, 2, 3... ชีวิตง่ายดาย
วันหนึ่ง มีซีเนียร์เดินมาทักด้วยสีหน้าจริงจัง: "อย่าใช้ Auto-increment นะเว้ย เดี๋ยวคู่แข่งรู้ยอดขายว่ามีกี่ออเดอร์ แถมแฮกเกอร์แค่เปลี่ยนเลขใน URL ก็ไล่ดูข้อมูลคนอื่นได้หมด ให้เปลี่ยนไปใช้ UUID (v4) ซะ!"
คุณฟังดูเข้าท่า เลยเปลี่ยน Primary Key เป็น UUID.randomUUID() ทันที... สองเดือนผ่านไป ข้อมูลแตะ 10 ล้านแถว จากเดิมที่บันทึกข้อมูลใช้เวลา 2 มิลลิวินาที ตอนนี้พุ่งไป 800 มิลลิวินาที หน้าเว็บเริ่มช้าจนหมุนติ้ว เกิดอะไรขึ้นใต้ฝากระโปรงของ Database?
📚 ตู้เก็บหนังสือที่เป็นระเบียบ: โลกของ B-Tree Index
ฐานข้อมูลความสัมพันธ์ส่วนใหญ่ (MySQL InnoDB, PostgreSQL) จัดเก็บข้อมูลบนดิสก์ตาม Clustered Index ซึ่งใช้โครงสร้างแบบ B+ Tree
กฎเหล็กของ B-Tree คือ "ข้อมูลต้องเรียงลำดับเสมอ" ข้อมูลจะถูกบรรจุอยู่ในหน้ากระดาษที่เรียกว่า Page (ขนาดมาตรฐานราว 8KB หรือ 16KB):
- เมื่อใช้ Auto-Increment (1, 2, 3...): แถวใหม่มีค่ามากกว่าแถวเดิมเสมอ Database แค่หยิบข้อมูลใหม่ไป แปะต่อท้ายหน้ากระดาษแผ่นล่าสุด พอหน้าเต็มก็เปิดหน้าใหม่แบบสบายๆ ทำงานเร็วเป็นเส้นตรง (Sequential Write)
- เมื่อใช้ UUID v4 สุ่มมั่ว (e.g.,
f47ac10b...): ค่าที่สุ่มมามีโอกาสตกอยู่ตรงไหนก็ได้ในจักรวาล! ข้อมูลใหม่อาจต้องแทรกเข้าไปตรงกลาง ระหว่างหน้ากระดาษที่เต็มแน่นเอี้ยดไปแล้วตั้งแต่เดือนก่อน
💥 ปรากฏการณ์ "หน้ากระดาษฉีก" (Page Split)
เมื่อ Database ต้องยัดข้อมูลแถวใหม่ลงไปในหน้าที่ ไม่มีที่ว่างเหลือแล้ว สิ่งที่มันต้องจำใจทำคือ:
- สั่งหยุดทุกอย่าง แล้วทำการ Page Split คือฉีกหน้าเดิมออกเป็น 2 หน้า
- ย้ายข้อมูลครึ่งหนึ่งไปหน้าใหม่ เพื่อเปิดช่องว่างให้ค่าที่สุ่มมาแทรกตัวลงไปตรงกลาง
- จัดระเบียบ Pointer ของโหนดแม่ข้างบนใหม่ทั้งหมด
- เขียนข้อมูลทั้งสองหน้าลง Disk ซ้ำอีกรอบ!
ผลลัพธ์คือเกิด Disk Fragmentation มหาศาล, Cache ใน RAM (Buffer Pool) ที่เคยโหลดหน้าเดิมไว้กลายเป็นของเสียทันที แถมขนาดตารางบนดิสก์บวมขึ้นเป็น 2 เท่าตัว เพราะแต่ละหน้ามีช่องว่างแหว่งๆ อยู่เต็มไปหมด
🛡️ ท่าแก้เผ็ด: รู้จัก UUIDv7 และ TSID
เรายังอยากได้ความลับและความไม่ซ้ำข้ามเครื่องแบบ UUID แต่ไม่อยากได้หายนะของการสุ่มมั่ว... วงการจึงพัฒนามาตรฐานใหม่ขึ้นมา:
"UUIDv7 (Time-ordered UUID): เอาเวลา Timestamp ปัจจุบันมาวางไว้ 48 บิตแรก แล้วค่อยเอาเลขสุ่มไปต่อท้าย!"
หน้าตาของมันยังคงเป็น UUID สวยๆ เหมือนเดิม แต่สิ่งที่วิเศษคือ มันเรียงลำดับตามเวลาได้ในตัว (Monotonically Increasing):
018e3f9a-1c20-7a3b-9e45-123456789abc (สร้างตอน 10:00:00)
018e3f9a-2d40-7f12-8a90-987654321def (สร้างตอน 10:00:01) ➔ ค่ามากกว่าตัวบนเสมอ!
ผลคือ: ได้ทั้งความปลอดภัย ไม่เดายอดขายง่ายๆ และ B-Tree Index ทำงานแบบต่อท้ายแถวเหมือน Auto-Increment 100% หมดปัญหา Page Split ไปตลอดกาล
💡 5-Min Takeaway
"การสุ่มค่า (Random) คือศัตรูตัวฉกาจของโครงสร้าง B-Tree Index" ถ้าต้องการใช้ UUID เป็น Primary Key ในระบบที่มีข้อมูลหลักล้านแถว จงหลีกเลี่ยง UUIDv4 แบบดั้งเดิม แล้วเปลี่ยนมาใช้ UUIDv7, ULID หรือ TSID ที่มีส่วนประกอบของเวลาอยู่ข้างหน้า เพื่อรักษาประสิทธิภาพการเขียนระดับมิลลิวินาทีของ Database เอาไว้