UUID กับ bigint ใน PostgreSQL: เลือก ID ให้รองรับการเติบโต
UUID กับ bigint ใน PostgreSQL: เปรียบเทียบขนาดดัชนี ลำดับการเรียง ความพร้อมสำหรับการชาร์ด และวิธีที่ ID ไหลผ่าน API เว็บ และแอปมือถือ

ทำไมการเลือก ID ถึงสำคัญกว่าที่คิด
แต่ละแถวในตาราง PostgreSQL ต้องมีวิธีที่มั่นคงเพื่อค้นหาซ้ำ นั่นคือหน้าที่ของ ID: ระบุระเบียนอย่างเฉพาะ มักเป็น primary key และกลายเป็นกาวสำหรับความสัมพันธ์ ตารางอื่นเก็บเป็น foreign key, คำสั่ง join ใช้มัน และแอปส่งผ่านมันไปรอบ ๆ เพื่ออ้างถึง “ลูกค้านั้น” “ใบแจ้งหนี้นั้น” หรือ “ตั๋วจำนวนนี้”
เพราะ ID ปรากฏเกือบทุกที่ การตัดสินใจไม่ใช่แค่รายละเอียดฐานข้อมูลเท่านั้น แต่มันส่งผลต่อขนาดดัชนี รูปแบบการเขียน ความเร็วของการค้นหา อัตราการโดนแคช และงานผลิตภัณฑ์อย่างการวิเคราะห์ การนำเข้าข้อมูล และการดีบัก นอกจากนี้ยังมีผลต่อสิ่งที่คุณเปิดเผยใน URL และ API และความง่ายในการให้แอปมือถือเก็บและซิงค์ข้อมูลอย่างปลอดภัย
ทีมส่วนใหญ่จะเปรียบเทียบ UUID vs bigint ใน PostgreSQL แบบเรียบง่าย คือคุณกำลังเลือกระหว่าง:
- bigint: ตัวเลข 64 บิต มักสร้างโดย sequence (1, 2, 3...)
- UUID: ตัวระบุ 128 บิต รูปร่างเหมือนสุ่ม หรือสร้างแบบเรียงตามเวลา
ไม่มีตัวเลือกไหนชนะในทุกกรณี Bigint มักกะทัดรัดและเป็นมิตรกับดัชนีและการเรียง UUID เหมาะเมื่อคุณต้องการ ID ที่ไม่ชนกันทั่วระบบ ต้องการ ID สาธารณะที่ปลอดภัยกว่า หรือตั้งใจให้ข้อมูลถูกสร้างจากหลายที่ (หลายบริการ ออฟไลน์บนมือถือ หรือต้องการชาร์ดในอนาคต)
กฎง่าย ๆ: ตัดสินใจจากวิธีที่ข้อมูลของคุณจะถูกสร้างและแชร์ ไม่ใช่แค่จากวิธีที่จะเก็บมันวันนี้
พื้นฐานของ Bigint และ UUID แบบเข้าใจง่าย
เมื่อคนเปรียบเทียบ UUID vs bigint ใน PostgreSQL พวกเขากำลังเลือกระหว่างสองวิธีตั้งชื่อแถว: ตัวเลขเรียงแบบ counter เล็ก ๆ หรือค่าที่ยาวกว่าที่ไม่ชนกันทั่วโลก
bigint คือจำนวนเต็ม 64 บิต ใน PostgreSQL มักสร้างด้วย identity column (หรือรูปแบบเก่า serial) ฐานข้อมูลเก็บ sequence ไว้เบื้องหลังและจ่ายหมายเลขถัดไปเมื่อแทรกแถวใหม่ นั่นหมายความว่า ID มักเป็น 1, 2, 3, 4... ง่าย อ่านง่าย และเป็นมิตรในเครื่องมือและรายงาน
UUID (Universally Unique Identifier) เป็น 128 บิต มักเห็นเป็น 36 อักขระรวม hyphen เช่น 550e8400-e29b-41d4-a716-446655440000 ประเภทที่พบบ่อยได้แก่:
- v4: UUID แบบสุ่ม สร้างง่ายที่ใดก็ได้ แต่ไม่เรียงตามลำดับการสร้าง
- v7: UUID เรียงตามเวลา ออกแบบมาให้เพิ่มขึ้นตามเวลาโดยประมาณ
การจัดเก็บเป็นหนึ่งในความต่างเชิงปฏิบัติ: bigint ใช้ 8 ไบต์ ขณะที่ UUID ใช้ 16 ไบต์ ช่องว่างขนาดนี้ปรากฏในดัชนีและส่งผลต่ออัตราการโดนแคช (ฐานข้อมูลสามารถเก็บรายการดัชนีน้อยลงในหน่วยความจำ)
คิดถึงที่ที่ ID ปรากฏนอกฐานข้อมูลด้วย: ID แบบ bigint สั้นใน URL และอ่านง่ายจาก log หรือในตั๋วบริการ UUID ยาวและพิมพ์ยาก แต่เดายากกว่าและสามารถสร้างได้อย่างปลอดภัยบน client เมื่อจำเป็น
ขนาดดัชนีและการบวมของตาราง: สิ่งที่จะเปลี่ยน
ความแตกต่างเชิงปฏิบัติมากที่สุดระหว่าง bigint และ UUID คือขนาด Bigint 8 ไบต์; UUID 16 ไบต์ ฟังดูเล็กจนกว่าคุณจะจำได้ว่าดัชนีทำซ้ำค่า ID ของคุณหลายครั้ง
ดัชนี primary key ของคุณต้องร้อนในหน่วยความจำเพื่อให้รู้สึกเร็ว ดัชนีที่เล็กกว่าทำให้ส่วนใหญ่พอดีใน shared buffers และ CPU cache ดังนั้น lookup และ join ต้องอ่านดิสก์น้อยลง ด้วย primary key เป็น UUID ดัชนีมักใหญ่ขึ้นอย่างเห็นได้ชัดสำหรับจำนวนแถวเท่ากัน
ตัวคูณคือดัชนีรอง ใน B-tree ของ PostgreSQL ทุก entry ของดัชนีรองยังเก็บค่าของ primary key (เพื่อให้ฐานข้อมูลค้นหาแถวได้) ดังนั้น ID ที่กว้างขึ้นจะเพิ่มขนาดไม่เพียงแต่ดัชนี primary แต่ยังรวมถึงดัชนีอื่น ๆ ที่คุณเพิ่มด้วย ถ้าคุณมีดัชนีรองสามตัว ความต่าง 8 ไบต์จาก UUID ปรากฏในสี่ที่
foreign key และตาราง join ก็ได้รับผลกระทบเช่นกัน ตารางใด ๆ ที่อ้างอิง ID จะเก็บค่านั้นในแถวและดัชนีของตัวเอง ตาราง many-to-many อาจเป็นสอง foreign key บวก overhead เล็กน้อย ดังนั้นการเพิ่มความกว้างคีย์เป็นสองเท่าอาจเปลี่ยน footprint อย่างมาก
ในทางปฏิบัติ:
- UUID มักทำให้ดัชนีหลักและดัชนีรองใหญ่ขึ้น และความต่างจะทวีคูณเมื่อคุณเพิ่มดัชนีมากขึ้น
- ดัชนีที่ใหญ่ขึ้นหมายถึงแรงกดดันในหน่วยความจำมากขึ้นและการอ่านหน้าเพจเพิ่มขึ้นภายใต้โหลด
- ยิ่งตารางที่อ้างอิงคีย์มีจำนวนมาก (events, logs, join tables) ความต่างของขนาดก็ยิ่งสำคัญ
ถ้า user ID ปรากฏใน users, orders, order_items, และ audit_log ค่านั้นจะถูกเก็บและทำดัชนีข้ามตารางทั้งหมด การเลือก ID ที่กว้างขึ้นจึงเป็นการตัดสินใจด้านการจัดเก็บมากพอ ๆ กับการตัดสินใจเรื่อง ID
ลำดับการเรียงและรูปแบบการเขียน: ID แบบต่อเนื่อง vs แบบสุ่ม
primary key ส่วนใหญ่ใน PostgreSQL อยู่บนดัชนี B-tree B-tree ทำงานได้ดีที่สุดเมื่อแถวใหม่ลงไปใกล้ปลายของดัชนี เพราะฐานข้อมูลสามารถต่อท้ายได้โดยมีการสับเปลี่ยนน้อย
ID แบบต่อเนื่อง: คาดเดาได้และเป็นมิตรกับการจัดเก็บ
กับ bigint ที่มาจาก identity หรือ sequence ID ใหม่จะเพิ่มขึ้นตามเวลา การแทรกมักชนที่ส่วนขวาสุดของดัชนี ทำให้หน้าเพจแน่น แคชอุ่น และ PostgreSQL ทำงานน้อยลง
สิ่งนี้สำคัญแม้ว่าคุณจะไม่เคยรัน ORDER BY id ก็ตาม เส้นทางการเขียนยังต้องวางคีย์ใหม่แต่ละอันในดัชนีตามลำดับที่เรียง
UUID แบบสุ่ม: กระจัดกระจายและสร้าง churn มากขึ้น
UUID แบบสุ่ม (พบบ่อยกับ UUIDv4) กระจายการแทรกไปทั่วทั้งดัชนี ซึ่งเพิ่มโอกาสของ page split ที่ PostgreSQL ต้องจัดสรรหน้าใหม่และย้ายรายการเพื่อให้มีที่ว่าง ผลคือการเพิ่ม write amplification: ไบต์ดัชนีถูกเขียนมากขึ้น WAL เพิ่มขึ้น และมักมีงานเบื้องหลังมากขึ้นภายหลัง (vacuum และการจัดการบวม)
UUID เรียงตามเวลาเปลี่ยนเรื่องเล่า UUID ที่เพิ่มขึ้นตามเวลาโดยประมาณ (เช่น UUIDv7 หรือสคีมแบบ time-based อื่น ๆ) คืน locality ส่วนใหญ่กลับมา ในขณะที่ยังเป็น 16 ไบต์และยังดูเป็น UUID ใน API ของคุณ
คุณจะรู้สึกความต่างเหล่านี้มากที่สุดเมื่ออัตราการแทรกสูง ตารางใหญ่ที่ใส่ไม่พอดีหน่วยความจำ และมีดัชนีรองหลายตัว ถ้าคุณไวต่อ latency การเขียนที่มาจาก page split ให้หลีกเลี่ยง ID แบบสุ่มเต็มรูปแบบในตารางที่เขียนบ่อย
ตัวอย่าง: ตาราง events ที่มีคนส่ง log จากแอปมือถือทั้งวันมักทำงานได้ราบรื่นกว่าด้วยคีย์แบบต่อเนื่องหรือ UUID เรียงตามเวลาเมื่อเทียบกับ UUID แบบสุ่มเต็มรูปแบบ
ผลกระทบด้านประสิทธิภาพที่คุณสัมผัสได้จริง
ความช้าส่วนใหญ่ในโลกจริงไม่ใช่ “UUID ช้า” หรือ “bigint เร็ว” แต่มาจากสิ่งที่ฐานข้อมูลต้องสัมผัสเพื่อให้คำตอบ
แผนการค้นหาสนใจว่าสามารถใช้ index scan สำหรับฟิลเตอร์หรือไม่ ทำการ join บนคีย์ได้เร็วแค่ไหน และตารางถูกจัดเรียงทางกายภาพ (หรือใกล้เคียงพอ) ให้การอ่านแบบช่วงถูกข้ามได้ถูกหรือเปล่า กับ primary key แบบ bigint แถวใหม่ลงในลำดับที่เพิ่มขึ้น ดังนั้นดัชนี primary มักคงความกะทัดรัดและเป็นมิตรกับ locality กับ UUID แบบสุ่ม การแทรกกระจัดกระจายทั่วดัชนี ซึ่งสามารถสร้าง page split เพิ่มและทำให้ลำดับบนดิสก์ยุ่งเหยิง
การอ่านเป็นสิ่งที่หลายทีมสังเกตเห็นก่อน ดัชนีที่ใหญ่ขึ้นหมายถึงดัชนีที่ใส่ใน RAM ได้น้อยกว่า ซึ่งทำให้อัตราการถูกแคชลดลงและเพิ่ม IO โดยเฉพาะในหน้าจอที่มี join หนัก เช่น “รายการคำสั่งซื้อพร้อมข้อมูลลูกค้า” ถ้า working set ของคุณไม่พอดีในหน่วยความจำ โครงสร้างที่มี UUID มากอาจผลักคุณไปข้างหน้าสู่ข้อจำกัดนั้นเร็วกว่าที่คาด
การเขียนก็อาจเปลี่ยนได้ UUID แบบสุ่มสามารถเพิ่ม churn ในดัชนี ซึ่งเพิ่มแรงกดดันต่อ autovacuum และอาจปรากฏเป็น latency spike ในช่วงที่มีงานหนาแน่น
ถ้าคุณจะ benchmark UUID vs bigint ใน PostgreSQL ให้ยุติธรรม: สคีมาเดียวกัน ดัชนีเดียวกัน fillfactor เดียวกัน และแถวจำนวนพอที่จะเกิน RAM (ไม่ใช่ 10k) วัด p95 latency และ IO และทดสอบทั้ง cache ร้อนและเย็น
ถ้าคุณสร้างแอปด้วย AppMaster บน PostgreSQL สิ่งนี้มักปรากฏเป็นหน้ารายการที่ช้าลงและโหลดฐานข้อมูลที่หนักขึ้น ก่อนที่จะดูเป็น “ปัญหา CPU”
ความปลอดภัยและการใช้งานในระบบที่เปิดเผยสู่สาธารณะ
ถ้า ID ของคุณออกจากฐานข้อมูลและปรากฏใน URL, การตอบ API, ตั๋วบริการ และหน้าจอมือถือ การเลือกมีผลทั้งด้านความปลอดภัยและการใช้งาน
ID แบบ bigint อ่านง่ายสำหรับมนุษย์ สั้น พูดออกทางโทรศัพท์ได้ และทีมสนับสนุนสามารถสังเกตแพทเทิร์นได้เร็วกว่าตัวอย่างเช่น “คำสั่งล้มเหลวส่วนใหญ่รอบ 9,200,000” ซึ่งช่วยเร่งการดีบักโดยเฉพาะเมื่อทำงานจาก log หรือภาพหน้าจอของลูกค้า
UUID มีประโยชน์เมื่อคุณเปิดเผยตัวระบุสู่สาธารณะ UUID เดายาก ดังนั้นการเก็บข้อมูลแบบสุ่มเหมือน /users/1, /users/2 จะใช้ไม่ได้ นอกจากนี้ยังทำให้นอกคนนอกเดาได้ยากว่าคุณมีเรคคอร์ดกี่รายการ
กับดักคือคิดว่า “เดายาก” เท่ากับ “ปลอดภัย” ถาการตรวจสิทธิ์อ่อนแอ ID ที่คาดเดาได้สามารถถูกใช้ในทางที่ผิด แต่ UUID ก็ยังถูกขโมยจากลิงก์ที่แชร์ log ที่รั่วไหล หรือการตอบ API ที่แคชไว้ได้ ความปลอดภัยต้องมาจากการตรวจสิทธิ์ ไม่ใช่จากการซ่อน ID
แนวทางปฏิบัติที่ใช้งานได้จริง:
- บังคับการเป็นเจ้าของหรือการตรวจบทบาทในทุกการอ่านและเขียน
- ถ้าคุณเปิดเผย ID ใน API สาธารณะ ให้ใช้ UUID หรือ token สาธารณะแยกต่างหาก
- ถ้าต้องการอ้างอิงที่เป็นมิตรสำหรับมนุษย์ ให้เก็บ bigint ภายในสำหรับงานปฏิบัติการ
- อย่าเข้ารหัสความหมายที่สำคัญลงใน ID เอง (เช่น ประเภทผู้ใช้)
ตัวอย่าง: พอร์ทัลลูกค้าแสดงหมายเลขใบแจ้งหนี้ ถ้าใบแจ้งหนี้ใช้ bigint และ API ของคุณแค่ตรวจ “invoice exists” คนอื่นสามารถวนหมายเลขและดาวน์โหลดใบแจ้งหนี้ของผู้อื่นได้ แก้การตรวจสอบก่อน แล้วค่อยตัดสินใจว่า UUID สำหรับ ID สาธารณะช่วยลดความเสี่ยงและภาระการสนับสนุนหรือไม่
ในแพลตฟอร์มอย่าง AppMaster ที่ ID ไหลผ่าน API ที่สร้างอัตโนมัติและแอปมือถือ ค่าเริ่มต้นที่ปลอดภัยที่สุดคือการมีการตรวจสิทธิ์ที่สม่ำเสมอบวกกับรูปแบบ ID ที่ไคลเอ็นต์จัดการได้อย่างเชื่อถือได้
วิธีที่ ID ไหลผ่าน API และแอปมือถือ
ชนิดฐานข้อมูลที่คุณเลือกไม่ได้หยุดอยู่ในฐานข้อมูล มันรั่วไหลไปยังขอบทุกจุด: URL, payload JSON, การจัดเก็บในไคลเอ็นต์, log และการวิเคราะห์
ถ้าคุณเปลี่ยนชนิด ID ภายหลัง การแตกหักมักไม่ใช่แค่ “การย้ายข้อมูล” foreign key ต้องเปลี่ยนทุกที่ ไม่ใช่แค่ในตารางหลัก ORM และตัวสร้างโค้ดอาจสร้างโมเดลใหม่ แต่การผนวกรวมยังคงคาดหวังรูปแบบเดิม แม้ endpoint ง่าย ๆ อย่าง GET /users/123 ก็ซับซ้อนเมื่อ ID กลายเป็น UUID ยาว 36 ตัว คุณยังต้องอัปเดตแคช คิวข้อความ และทุกที่ที่เก็บ ID เป็นจำนวนเต็ม
สำหรับ API การเลือกหลัก ๆ คือรูปแบบและการตรวจสอบ Bigint ส่งเป็นตัวเลข แต่บางระบบ (และบางภาษา) อาจมีปัญหาความแม่นยำเมื่อพาร์สเป็น float UUID ส่งเป็นสตริง ซึ่งปลอดภัยกว่าในการพาร์ส แต่คุณต้องมีการตรวจสอบเข้มงวดเพื่อหลีกเลี่ยงข้อมูล junk ที่เป็น "เกือบ UUID"
บนมือถือ ID ถูกอนุกรมและเก็บเสมอ: การตอบ JSON, ตาราง SQLite ท้องถิ่น, และคิวออฟไลน์ที่บันทึกการกระทำจนกว่าเครือข่ายกลับมา ID แบบตัวเลขเล็กกว่า แต่ UUID เป็นสตริงมักจัดการง่ายกว่าเป็นโทเค็นทึบ สิ่งที่สร้างปัญหาจริงคือความไม่สอดคล้อง: เลเยอร์หนึ่งเก็บเป็นจำนวน อีกเลเยอร์เก็บเป็นข้อความ การเปรียบเทียบหรือ join กลายเปราะบาง
กฎไม่กี่ข้อที่ช่วยให้ทีมพ้นปัญหา:
- เลือกรูปแบบ canonical สำหรับ API (บ่อยครั้งคือสตริง) และยึดตามมัน
- ตรวจสอบ ID ที่ขอบระบบและส่งข้อผิดพลาด 400 ชัดเจน
- เก็บ representation เดียวกันในแคชท้องถิ่นและคิวออฟไลน์
- บันทึก ID ด้วยชื่อฟิลด์และรูปแบบที่สอดคล้องกันข้ามบริการ
ถ้าคุณสร้างเว็บและไคลเอ็นต์มือถือด้วยสแตกที่สร้างโค้ด (เช่น AppMaster ที่สร้าง backend และ native apps) สัญญา ID ที่เสถียรยิ่งมีความหมายเพราะมันกลายเป็นส่วนหนึ่งของโมเดลและคำขอที่สร้างอัตโนมัติ
ความพร้อมสำหรับชาร์ดและระบบแบบกระจาย
“พร้อมชาร์ด” ส่วนใหญ่หมายถึงคุณสามารถสร้าง ID ได้จากหลายที่โดยไม่ทำให้เกิดการชน และสามารถย้ายข้อมูลข้ามโหนดในภายหลังโดยไม่ต้องเขียนทับ foreign key ทั้งหมด
UUID ได้รับความนิยมในสภาพแวดล้อม multi-region หรือ multi-writer เพราะโหนดใดก็สามารถสร้าง ID ที่ไม่ชนกันได้โดยไม่ต้องขอ sequence ศูนย์กลาง ซึ่งลดการประสานและทำให้รับการเขียนในหลายภูมิภาคและการรวมข้อมูลภายหลังง่ายขึ้น
Bigint ยังใช้งานได้ แต่คุณต้องมีแผน ตัวเลือกทั่วไปคือจัดสรรช่วงตัวเลขต่อชาร์ด (ชาร์ด 1 ใช้ 1–1B, ชาร์ด 2 ใช้ 1B–2B), รัน sequence แยกกับ prefix ของชาร์ด, หรือใช้ตัวสร้างแบบ Snowflake (บิตเวลารวมกับบิตเครื่องหรือชาร์ด) วิธีเหล่านี้ช่วยให้ดัชนียังคงเล็กกว่าการใช้ UUID และรักษาการเรียงลำดับบางส่วนไว้ได้ แต่เพิ่มกฎการปฏิบัติการที่ต้องบังคับใช้
ข้อแลกเปลี่ยนที่สำคัญในงานประจำวัน:
- การประสานงาน: UUID แทบไม่ต้องการ; bigint มักต้องมีการวางแผนช่วงหรือบริการสร้าง
- การชนกัน: การชนของ UUID แทบจะเป็นไปไม่ได้; bigint ปลอดภัยก็ต่อเมื่อกฎการจัดสรรไม่ทับซ้อน
- การเรียงลำดับ: หลายสคีม bigint เรียงตามเวลาได้โดยประมาณ; UUID มักสุ่มเว้นแต่ใช้เวอร์ชันเรียงตามเวลา
- ความซับซ้อน: bigint แบบชาร์ดคงเรียบง่ายก็ต่อเมื่อทีมมีวินัย
สำหรับหลายทีม “พร้อมชาร์ด” จริง ๆ คือ “พร้อมย้ายข้อมูล” ถ้าคุณอยู่บนฐานข้อมูลเดียววันนี้ ให้เลือก ID ที่ทำให้งานตอนนี้ง่ายกว่า ถ้าคุณกำลังสร้าง writer หลายตัวแล้ว (เช่น ผ่าน API และแอปมือถือที่สร้างโดย AppMaster) ให้ตัดสินใจตั้งแต่ต้นว่า ID ถูกสร้างและตรวจสอบอย่างไรข้ามบริการ
ขั้นตอนทีละขั้นตอน: เลือกกลยุทธ์ ID ที่เหมาะสม
เริ่มจากระบุรูปร่างจริงของแอป คุณมีฐานข้อมูล PostgreSQL เดียวในหนึ่งภูมิภาค แตกต่างจากระบบ multi-tenant ที่อาจจะแยกตามภูมิภาค หรือแอปมือถือที่ต้องสร้างระเบียนออฟไลน์แล้วซิงค์ทีหลัง
ต่อมา ซื่อสัตย์เกี่ยวกับที่ที่ ID จะปรากฏ ถ้าตัวระบุติดอยู่ใน backend เท่านั้น (jobs, เครื่องมือภายใน, แผง admin) ความเรียบง่ายมักชนะ ถ้า ID ปรากฏใน URL, log ที่แชร์กับลูกค้า, ตั๋วบริการ, หรือ deep link มือถือ ความคาดเดาได้และความเป็นส่วนตัวมีความสำคัญมากขึ้น
ใช้การเรียงลำดับเป็นปัจจัยตัดสินใจ ไม่ใช่แค่หลังเหตุผล ถ้าคุณพึ่งพา feed “ใหม่ล่าสุดก่อน” การแบ่งหน้าแบบเสถียร หรือ audit trail ที่อ่านง่าย ID แบบต่อเนื่อง (หรือ ID เรียงตามเวลา) จะลดความประหลาดใจ หากการเรียงลำดับไม่ขึ้นกับ primary key คุณสามารถแยกการเลือก PK และเรียงโดย timestamp แทนได้
โฟลว์การตัดสินใจที่ใช้ได้จริง:
- ระบุสถาปัตยกรรม (single DB, multi-tenant, multi-region, offline-first) และว่าคุณอาจรวมข้อมูลจากหลายแหล่งหรือไม่
- ตัดสินใจว่า ID เป็นตัวระบุสาธารณะหรือภายในล้วน ๆ
- ยืนยันความต้องการการเรียงลำดับและการแบ่งหน้า หากต้องการลำดับการแทรกธรรมชาติ หลีกเลี่ยง ID แบบสุ่มเต็มรูปแบบ
- ถ้าเลือก UUID ให้เลือกเวอร์ชันอย่างมีวัตถุประสงค์: แบบสุ่ม (v4) เพื่อความเดายาก หรือเรียงตามเวลาเพื่อ locality ของดัชนีที่ดีกว่า
- ล็อกข้อบังคับตั้งแต่ต้น: รูปแบบข้อความ canonical, กฎตัวพิมพ์, การตรวจสอบ และวิธีที่ API ทุกตัวส่งและรับ ID
ตัวอย่าง: ถ้าแอปมือถือสร้าง “คำสั่งร่าง” ออฟไลน์ UUID ให้ดีไวซ์สร้าง ID ได้อย่างปลอดภัยก่อนเซิร์ฟเวอร์เห็น ในเครื่องมืออย่าง AppMaster นั่นสะดวกเพราะรูปแบบ ID เดียวกันไหลจากฐานข้อมูลไปยัง API เว็บ และแอปเนทีฟโดยไม่ต้องกรณีพิเศษ
ข้อผิดพลาดและกับดักที่พบบ่อย
การถกเถียงเรื่อง ID มักผิดพลาดเพราะคนเลือกชนิด ID ด้วยเหตุผลหนึ่ง แล้วตกใจเมื่อผลกระทบตามมาทีหลัง
ข้อผิดพลาดทั่วไปคือใช้ UUID แบบสุ่มเต็มรูปแบบบนตารางที่มีการเขียนหนาแน่น แล้วสงสัยว่าทำไมการแทรกถึงมีลักษณะเป็น spurts ค่าที่สุ่มกระจายแถวใหม่ทั่วดัชนี ซึ่งทำให้ page split มากขึ้นและงานของฐานข้อมูลมากขึ้นภายใต้โหลด ถ้าตารางเขียนหนัก ให้คิดเรื่อง locality ของการแทรกก่อนตัดสินใจ
ปัญหาบ่อยอีกอย่างคือผสมชนิด ID ข้ามบริการและไคลเอนต์ เช่น บริการหนึ่งใช้ bigint อีกบริการใช้ UUID และ API ของคุณจบด้วย ID ทั้งแบบตัวเลขและสตริง มักกลายเป็นบั๊กจาง ๆ: JSON parser สูญเสียความแม่นยำกับตัวเลขใหญ่ โค้ดมือถือถือ ID เป็นตัวเลขในหน้าหนึ่งและเป็นสตริงในอีกหน้า หรือคีย์แคชไม่ตรงกัน
กับดักที่สามคือตีความว่า “ID เดายาก” = การควบคุมการเข้าถึง แม้จะใช้ UUID คุณยังต้องมีการตรวจสิทธิ์อย่างถูกต้อง
สุดท้าย ทีมเปลี่ยนชนิด ID ช้าโดยไม่มีแผน ส่วนที่ยากที่สุดไม่ใช่ primary key เอง แต่เป็นทุกอย่างที่แนบกับมัน: foreign key, ตาราง join, URL, event ในการวิเคราะห์, deep link บนมือถือ และสถานะที่เก็บในไคลเอนต์
เพื่อหลีกเลี่ยงความเจ็บปวด:
- เลือกชนิด ID เดียวสำหรับ API สาธารณะและยึดตามมัน
- ใช้ ID เป็นสตริงทึบในไคลเอนต์เพื่อหลีกเลี่ยงปัญหาตัวเลข
- อย่าใช้ความสุ่มของ ID เป็นการควบคุมการเข้าถึง
- หากต้องย้าย ให้มีเวอร์ชันของ API และวางแผนสำหรับไคลเอนต์ที่ใช้งานยาวนาน
ถ้าคุณสร้างด้วยแพลตฟอร์มที่สร้างโค้ดอย่าง AppMaster ความสม่ำเสมอยิ่งสำคัญเพราะชนิด ID เดียวไหลจากสคีมาฐานข้อมูลไปยัง backend และแอปเว็บ/มือถือที่สร้างอัตโนมัติ
เช็คลิสต์ด่วนก่อนตัดสินใจ
ถ้าคุณติด ให้เริ่มจากสิ่งที่ผลิตภัณฑ์ของคุณจะเป็นในอีกปี และ ID จะเดินทางไปกี่ที่
ถามตัวเอง:
- ตารางที่ใหญ่ที่สุดจะมีขนาดเท่าไรใน 12–24 เดือน และคุณจะเก็บประวัติยาวนานไหม?
- คุณต้องการ ID ที่เรียงตามเวลาสร้างโดยประมาณเพื่อการแบ่งหน้าและการดีบักง่ายไหม?
- จะมีมากกว่าหนึ่งระบบสร้างระเบียนพร้อมกันไหม รวมถึงแอปมือถือออฟไลน์หรือ background job?
- ID จะปรากฏใน URL, ตั๋วบริการ, การส่งออก หรือภาพหน้าจอที่ลูกค้าแชร์ไหม?
- ไคลเอนต์ทุกตัวสามารถจัดการ ID เหมือนกันได้ไหม (เว็บ, iOS, Android, สคริปต์) รวมถึงการตรวจสอบและการจัดเก็บ?
หลังตอบคำถาม ให้ตรวจระบบต่อ:
- ถ้าใช้ bigint ให้แน่ใจว่ามีแผนการสร้าง ID ในทุกสภาพแวดล้อม (โดยเฉพาะ dev ท้องถิ่นและการนำเข้า)
- ถ้าใช้ UUID ให้มั่นใจว่าสัญญา API และโมเดลไคลเอนต์รองรับ ID เป็นสตริงอย่างสม่ำเสมอ และทีมอ่าน/เปรียบเทียบมันได้สะดวก
การทดสอบความเป็นจริงอย่างรวดเร็ว: ถ้าแอปมือถือต้องสร้างคำสั่งระหว่างออฟไลน์และซิงค์ทีหลัง UUID มักลดงานประสาน ถ้าแอปของคุณส่วนใหญ่ออนไลน์และต้องการดัชนีกะทัดรัด bigint มักง่ายกว่า
ถ้าคุณสร้างบน AppMaster ให้ตัดสินใจตั้งแต่ต้นเพราะรูปแบบ ID ไหลผ่านโมเดล PostgreSQL, endpoint API ที่สร้าง และไคลเอนต์เว็บ/มือถือที่สร้างอัตโนมัติ
ตัวอย่างสถานการณ์จริง
บริษัทเล็ก ๆ มีเครื่องมือภายใน พอร์ทัลลูกค้า และแอปมือถือสำหรับพนักงานภาคสนาม ทั้งสามเข้าถึง PostgreSQL เดียวผ่าน API หนึ่งตัว ระเบียนใหม่ถูกสร้างทั้งวัน: ตั๋ว รูปถ่าย อัปเดตสถานะ และใบแจ้งหนี้
กับ bigint payload ของ API จะกะทัดรัดและอ่านง่าย:
{ "ticket_id": 4821931, "customer_id": 91244 }
การแบ่งหน้ารู้สึกเป็นธรรมชาติ: ?after_id=4821931&limit=50 การเรียงตาม id มักตรงกับเวลาสร้าง ดังนั้น “ตั๋วล่าสุด” จึงเร็วและคาดเดาได้ การดีบักก็ง่าย: ฝ่ายสนับสนุนขอ “ticket 4821931” และคนส่วนใหญ่สามารถพิมพ์ได้โดยไม่ผิดพลาด
กับ UUIDs payload จะยาวขึ้น:
{ "ticket_id": "3f9b3c0a-7b9c-4bf0-9f9b-2a1b3c5d1d2e" }
ถ้าใช้ UUID v4 แบบสุ่ม การแทรกจะลงทั่วดัชนี ซึ่งอาจหมายถึง churn ดัชนีมากขึ้นและการดีบักที่ซับซ้อนขึ้นเล็กน้อย (การคัดลอก/วางเป็นเรื่องปกติ) การแบ่งหน้ามักเปลี่ยนเป็น cursor-style แทน after id
ถ้าใช้ UUID เรียงตามเวลา คุณยังคงพฤติกรรม “ใหม่สุดก่อน” ส่วนใหญ่ไว้ได้ ในขณะที่ยังหลีกเลี่ยงการคาดเดา ID ใน URL สาธารณะ
ในทางปฏิบัติ ทีมมักสังเกตสี่เรื่อง:
- ความถี่ที่ต้องพิมพ์ ID โดยมนุษย์เทียบกับการคัดลอก
- ว่า “เรียงตาม id” ตรงกับ “เรียงตาม created” หรือไม่
- ความสะอาดและความเสถียรของ cursor pagination
- ความง่ายในการติดตามระเบียนใน log, การเรียก API และหน้าจอมือถือ
ขั้นตอนต่อไป: เลือกค่าเริ่มต้น ทดสอบ และทำเป็นมาตรฐาน
ทีมส่วนใหญ่ติดเพราะต้องการคำตอบสมบูรณ์แบบ คุณไม่ต้องการความสมบูรณ์แบบ คุณต้องการค่าเริ่มต้นที่เหมาะกับผลิตภัณฑ์ตอนนี้ รวมถึงวิธีพิสูจน์อย่างรวดเร็วว่ามันจะไม่ทำร้ายคุณในภายหลัง
กฎที่คุณสามารถทำเป็นมาตรฐาน:
- ใช้ bigint เมื่อต้องการดัชนีกะทัดรัดที่สุด การเรียงลำดับคาดเดาได้ และการดีบักง่าย
- ใช้ UUID เมื่อต้องการให้ ID เดายากใน URL คาดว่าจะมีการสร้างออฟไลน์ (มือถือ) หรืออยากลดการชนกันระหว่างระบบ
- ถ้าอาจแยกข้อมูลตาม tenant หรือภูมิภาคในอนาคต ให้เลือกแผน ID ที่ทำงานข้ามโหนดได้ (UUID หรือสคีม bigint ที่ประสานกัน)
- เลือกหนึ่งเป็นค่าเริ่มต้นและให้ข้อยกเว้นเกิดขึ้นไม่บ่อย ความสม่ำเสมอมักชนะการจูนแบบเฉพาะจุด
ก่อนล็อกการตัดสินใจ ทำ spike เล็ก ๆ สร้างตารางขนาดแถวจริง แทรก 1–5 ล้านแถว แล้วเปรียบเทียบ (1) ขนาดดัชนี (2) เวลาแทรก และ (3) คิวรีทั่วไปกับ primary key และดัชนีรองเล็กน้อย ทำบนฮาร์ดแวร์จริงและรูปแบบข้อมูลจริงของคุณ
ถ้ากังวลว่าจะเปลี่ยนภายหลัง ให้วางแผน migration ให้เรียบง่าย:
- เพิ่มคอลัมน์ ID ใหม่และสร้าง unique index
- dual-write: เติมทั้งสอง ID สำหรับแถวใหม่
- backfill แถวเก่าเป็นแบตช์
- อัปเดต API และไคลเอนต์เพื่อรับ ID ใหม่ (คงของเก่าไว้ทำงานระหว่างการเปลี่ยน)
- สลับไปอ่านจาก ID ใหม่ แล้วค่อยลบคีย์เก่าเมื่อ log และ metrics ดูสะอาด
ถ้าคุณสร้างบน AppMaster (appmaster.io) ควรตัดสินใจตั้งแต่ต้นเพราะคอนเวนชัน ID ไหลผ่านโมเดล PostgreSQL, API ที่สร้าง และเว็บ/มือถือเนทีฟที่สร้างอัตโนมัติ รูปแบบเฉพาะมีความสำคัญ แต่ความสม่ำเสมอสำคัญกว่าหลังจากมีผู้ใช้จริงและไคลเอนต์หลายตัว
คำถามที่พบบ่อย
ค่าเริ่มต้นให้เลือกว่าเป็น bigint เมื่อคุณมีฐานข้อมูล PostgreSQL เดียว ส่วนใหญ่เขียนจากเซิร์ฟเวอร์ และคุณให้ความสำคัญกับดัชนีที่กะทัดรัดและพฤติกรรมการแทรกที่คาดเดาได้ เลือก UUID เมื่อ ID ต้องถูกสร้างจากหลายที่ (หลายบริการ แอปมือถือออฟไลน์ การชาร์ดในอนาคต) หรือต้องการให้ public ID ยากต่อการเดา
เพราะค่า ID ถูกคัดลอกไปยังที่ต่าง ๆ: ดัชนี primary key, ทุกดัชนีรอง (เพื่อชี้แถว), คอลัมน์ foreign key ในตารางอื่น ๆ และตาราง join UUID มีขนาด 16 ไบต์ เทียบกับ 8 ไบต์ของ bigint ดังนั้นความต่างจะทวีคูณในสคีมของคุณและอาจลดอัตราการถูกเก็บในแคช
บนตารางที่มีการเขียนร้อน ๆ ใช่ ในกรณีของ UUID แบบสุ่ม (เช่น v4) การแทรกจะกระจายไปทั่ว B-tree ซึ่งเพิ่ม page split และ churn ของดัชนีภายใต้โหลด หากต้องการใช้ UUID แต่ยังต้องการการเขียนที่นุ่มนวล ให้ใช้กลยุทธ์ UUID แบบเรียงตามเวลาเพื่อให้คีย์ใหม่ตกที่ส่วนท้ายของดัชนีเป็นส่วนใหญ่
ผลมักจะปรากฏเป็น IO มากขึ้น มากกว่าการใช้ CPU ช้าลง คีย์ที่ใหญ่กว่านำไปสู่ดัชนีที่ใหญ่ขึ้น และเมื่อดัชนีใหญ่ขึ้น หน้าดัชนีที่ใส่ในหน่วยความจำได้น้อยกว่า ทำให้ join และ lookup ต้องอ่านจากดิสก์บ่อยขึ้น ความต่างจะชัดเมื่อโต๊ะใหญ่ การ query ติด join หนาแน่น และ working set เกิน RAM
UUID ช่วยลดการเดาง่ายเช่น /users/1 แต่ไม่ทดแทนการตรวจสิทธิ์ หากการตรวจสิทธิ์ผิดพลาด UUID ก็ยังถูกขโมยได้จากลิงก์ที่แชร์ หรือ log ที่รั่วไหล ปรับใช้การควบคุมการเข้าถึงอย่างเคร่งครัดเพื่อความปลอดภัยที่แท้จริง
ใช้รูปแบบตัวแทนเดียวและยึดตามมัน ค่าเริ่มต้นที่ใช้ได้จริงคือมอง ID เป็นสตริงในคำขอและการตอบ API ถึงแม้ว่าฐานข้อมูลจะเก็บเป็น bigint ก็ตาม เพราะจะหลีกเลี่ยงปัญหาทางตัวเลขบนไคลเอ็นต์และทำให้การตรวจสอบง่ายขึ้น ตราบใดที่คุณสอดคล้องทั้งเว็บ มือถือ บันทึก และแคช
ค่า bigint อาจมีปัญหาในไคลเอ็นต์บางตัวถ้าพาร์สเป็นตัวเลขทศนิยมซึ่งอาจสูญเสียความแม่นยำเมื่อค่ามีขนาดใหญ่ UUID หลีกปัญหานี้เพราะเป็นสตริง แต่ยาวกว่าและต้องการการตรวจสอบอย่างเคร่งครัด แนวทางที่ปลอดภัยคือความสม่ำเสมอ: แบบเดียวในทุกที่ พร้อมการตรวจสอบที่ชัดเจนที่ขอบระบบ
UUID เป็นทางเลือกตรงไปตรงมาสำหรับการใช้งานหลายโหนดเพราะแต่ละโหนดสามารถสร้าง ID ได้โดยไม่ต้องประสานกับศูนย์กลาง Bigint ก็ยังทำงานได้ แต่คุณต้องมีกฎ เช่น แบ่งช่วงตัวเลขต่อชาร์ด หรือใช้ตัวสร้างแบบ Snowflake ซึ่งต้องบังคับใช้อย่างสม่ำเสมอ ถ้าต้องการเรื่องกระจายที่ง่ายที่สุด เลือก UUID (แนะนำแบบเรียงตามเวลา)
การเปลี่ยนชนิด primary key กระทบมากกว่าหนึ่งคอลัมน์ ต้องอัปเดต foreign key ตาราง join สัญญา API ที่ไคลเอ็นต์เก็บค่าแคช ข้อมูลวิเคราะห์ และการผนวกรวมอื่น ๆ ถ้าต้องเปลี่ยน ให้วางแผน gradual migration ด้วย dual-write และช่วงเปลี่ยนผ่านที่ยาวนาน
เก็บ primary key ภายในเป็น bigint เพื่อประสิทธิภาพในฐานข้อมูล และเพิ่ม UUID สาธารณะหรือ token แยกต่างหากสำหรับ URL และ API ภายนอก วิธีนี้ทำให้ดัชนีกระชับและการดีบักภายในง่าย ในขณะเดียวกันก็ลดการเรียงลำดับแบบเดาง่ายในตัวระบุสาธารณะ ห้ามสับสนว่าค่าไหนเป็น “public ID” และอย่าใช้ทั้งสองแบบอย่างขาดความระมัดระวัง


