10 คะแนน โดย GN⁺ 2025-12-16 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • UUID v4 มีความสุ่มสูง ทำให้เกิด ความไม่มีประสิทธิภาพของดัชนีและ I/O ที่มากเกินไป และเมื่อใช้เป็นคีย์หลักใน PostgreSQL จะทำให้ประสิทธิภาพลดลง
  • การแทรกข้อมูลแบบสุ่มทำให้เกิด page split และ index fragmentation บ่อยครั้ง รวมถึงทำให้ ขนาด WAL log เพิ่มขึ้น และเกิดความหน่วงในการเขียน
  • UUID มีขนาด 16 ไบต์ ซึ่งกินพื้นที่มากกว่า bigint ถึงสองเท่า ส่งผลให้ cache hit rate ลดลงและเกิด การสิ้นเปลืองหน่วยความจำ
  • แม้มักถูกเข้าใจผิดว่าเป็นตัวระบุเพื่อความปลอดภัย แต่ตาม RFC 4122 ระบุว่า UUID ไม่ใช่มาตรการความปลอดภัยเพื่อป้องกันการเดา
  • สำหรับฐานข้อมูลใหม่ แนะนำให้ใช้ คีย์แบบลำดับชนิดจำนวนเต็ม และหากเลี่ยงไม่ได้ควรใช้ UUID v7 แบบเรียงตามเวลา

ปัญหาด้านประสิทธิภาพของ UUID v4

  • ฐานข้อมูลที่ใช้ UUID v4 เป็นคีย์หลัก ใน PostgreSQL ตลอด 10 ปีที่ผ่านมา แสดงให้เห็นอย่างสม่ำเสมอถึง ประสิทธิภาพที่ลดลงและ I/O ที่มากเกินไป
    • UUID v4 สร้างแบบสุ่ม 122 บิต ทำให้ไม่สามารถจัดเรียงดัชนีได้
    • เมื่อแทรกข้อมูลจะไม่ถูกเก็บลงในหน้าที่เรียงต่อเนื่อง จึงเกิด random access และเมื่อต้องอัปเดตหรือลบก็ยังต้องค้นหาอย่างไม่มีประสิทธิภาพ
  • ดัชนี B-Tree ตั้งอยู่บนสมมติฐานว่าข้อมูลมีการจัดเรียง แต่ UUID v4 ไม่มีลำดับ จึงทำให้ ประสิทธิภาพการแทรกต่ำ
    • ทุกการแทรกจะถูกเขียนลงหน้าต่าง ๆ แบบสุ่ม ทำให้เกิด การแบ่งหน้ากลางทาง บ่อยครั้ง
    • ส่งผลให้เกิด write latency และ WAL เพิ่มขึ้น

โครงสร้างของ UUID และทางเลือก

  • UUID เป็นตัวระบุขนาด 128 บิต (16 ไบต์) และใน PostgreSQL จะถูกเก็บเป็น ชนิด binary uuid
  • UUID v4 อิงบิตแบบสุ่ม ขณะที่ UUID v7 มี timestamp อยู่ใน 48 บิตแรก จึงมีประสิทธิภาพด้านดัชนีดีกว่า
  • PostgreSQL 18 (กำหนดในปี 2025) จะรองรับ UUID v7 เป็นค่าเริ่มต้น
  • UUID v7 สามารถเรียงตามเวลาได้ จึงช่วยปรับปรุง ความหนาแน่นของหน้าและประสิทธิภาพของแคช

เหตุผลที่เลือกใช้ UUID และข้อจำกัด

  • UUID ถูกใช้เมื่อจำเป็นต้องสร้าง ตัวระบุที่ไม่ชนกัน ในสภาพแวดล้อมแบบหลายไคลเอนต์หรือไมโครเซอร์วิส
    • เช่น การสร้าง ID พร้อมกันจากหลายอินสแตนซ์ของฐานข้อมูล
  • อย่างไรก็ตาม RFC 4122 ระบุชัดว่า “อย่าสมมติว่า UUID เดาได้ยาก” จึง ไม่เหมาะจะใช้เป็นตัวระบุด้านความปลอดภัย
  • ความน่าจะเป็นที่จะชนกันจะอยู่ที่ 50% เมื่อสร้างไป 2.71×10¹⁸ ค่า ซึ่งในทางปฏิบัติโอกาสชนกันต่ำมาก แต่ ต้นทุนด้านประสิทธิภาพสูง

ความไม่มีประสิทธิภาพด้านพื้นที่และ I/O ของ UUID

  • UUID ใช้พื้นที่มากกว่า bigint (8 ไบต์) สองเท่า และมากกว่า int (4 ไบต์) สี่เท่า
    • ในตารางขนาดใหญ่จะนำไปสู่ การใช้พื้นที่จัดเก็บเพิ่มขึ้น และ เวลาในการสำรอง/กู้คืนที่นานขึ้น
  • ผลการทดลองเรื่อง ความหนาแน่นของหน้าดัชนี
    • ดัชนี integer: 97.64%
    • ดัชนี UUID v4: 79.06%
    • ดัชนี UUID v7: 90.09%
  • ในการทดสอบของ Cybertec พบว่า การค้นหาดัชนี UUID v4 ต้องเข้าถึงหน้าเพิ่มอีก 8.5 ล้านหน้า และมี I/O เพิ่มขึ้น 31229%
    • ภายใต้เงื่อนไขเดียวกัน ดัชนี bigint มีการเข้าถึงบัฟเฟอร์ 27,332 ครั้ง ส่วน UUID v4 มี 8,562,960 ครั้ง

ผลกระทบต่อแคชและหน่วยความจำ

  • UUID มีการกระจายแบบสุ่ม ทำให้ buffer cache hit ratio ต่ำ
    • ต้องโหลดหน้าจำนวนมากขึ้นเข้าแคช และหน้าที่จำเป็นมักถูก eviction ออกบ่อย
  • เมื่อประสิทธิภาพแคชลดลง จะทำให้ query มีความหน่วง และ การใช้หน่วยความจำเพิ่มขึ้น
  • เพื่อคงประสิทธิภาพ แนะนำให้ทำ การสร้างดัชนีใหม่เป็นระยะ (REINDEX CONCURRENTLY) หรือใช้ pg_repack

แนวทางบรรเทาปัญหาด้านประสิทธิภาพ

  • เพิ่มหน่วยความจำ: แนะนำให้มี RAM ประมาณ 4 เท่าของขนาดฐานข้อมูล (เช่น DB 25GB → หน่วยความจำ 128GB)
  • ปรับ work_mem: สามารถเพิ่มประสิทธิภาพได้โดยจัดสรรหน่วยความจำให้กับงาน sort มากขึ้น
  • ใน สภาพแวดล้อม Rails สามารถตั้งค่า implicit_order_column เพื่อใช้ฟิลด์ที่จัดเรียงได้ เช่น created_at แทน UUID
  • สามารถใช้คำสั่ง CLUSTER เพื่อจัดเรียงตารางใหม่ตามฟิลด์ที่เรียงลำดับได้ แต่ต้องใช้ exclusive lock

แนะนำให้ใช้คีย์จำนวนเต็มและ sequence

  • สำหรับฐานข้อมูลใหม่ แนะนำให้ใช้ คีย์แบบ sequence ชนิดจำนวนเต็ม
    • integer (4 ไบต์) รองรับได้ราว 2 พันล้านค่า ส่วน bigint (8 ไบต์) รองรับค่าที่ไม่ซ้ำได้มากกว่านั้นมาก
  • แอปธุรกิจส่วนใหญ่ใช้ integer ก็เพียงพอ ส่วนบริการขนาดใหญ่เหมาะกับการใช้ bigint
  • ทางเลือกที่ใช้ได้จริงคือ UUID v7 แทน UUID v4 หรือใช้ ส่วนขยาย sequential_uuids

สรุป

  • UUID v4 ก่อให้เกิด ความไม่มีประสิทธิภาพของดัชนี, I/O สูง, และประสิทธิภาพแคชต่ำ จากความสุ่มของมัน
  • ไม่สามารถใช้เป็น ตัวระบุด้านความปลอดภัย ได้ และยัง สิ้นเปลืองพื้นที่
  • คีย์แบบ sequence ชนิดจำนวนเต็ม เหมาะกับแอปพลิเคชันส่วนใหญ่มากกว่า
  • หากจำเป็นต้องใช้ UUID จริง ๆ ควรเลือก UUID v7 แบบเรียงตามเวลา
  • ควรหลีกเลี่ยงการใช้ gen_random_uuid() เป็นคีย์หลักใน PostgreSQL

1 ความคิดเห็น

 
GN⁺ 2025-12-16
ความคิดเห็นจาก Hacker News
  • นี่เป็นตัวอย่างคลาสสิกของ การปรับแต่งก่อนเวลาอันควร
    การใส่ข้อมูลลงในตัวระบุถาวรถือเป็นข้อห้ามในการจัดการข้อมูล
    ถ้าใส่วันเกิดไว้ใน ID แบบเลขประจำตัวประชาชนนอร์เวย์ ภายหลังก็อาจเกิดกรณีผู้อพยพที่เดิมรู้วันเกิดผิด หรือมีคนที่ถูกกำหนดให้เกิดวันที่ 1 มกราคมมากเกินไปจนเลขไม่พอ
    สมัยตู้บัตรรายการนั้นการผสมข้อมูลกับตัวระบุเพื่อให้ค้นหาได้ถูกลงยังพอเข้าใจได้ แต่ทุกวันนี้เรามีฐานข้อมูลที่ทรงพลังอยู่แล้ว จึงไม่ค่อยมีเหตุผลต้องทำแบบนั้น

    • ผมคิดว่าตัวอย่างนี้จริง ๆ เป็นปัญหาเรื่อง การตั้งค่าเริ่มต้น ที่ผิดมากกว่า
      ปัญหาอยู่ที่ตั้งวันเกิดที่ไม่รู้เป็นวันที่ 1 มกราคม ไม่ใช่การใส่วันที่ลงในคีย์โดยตัวมันเอง
      ถ้าใช้ค่าที่ไม่ใช่วันที่อย่าง 00 หรือ 99 ก็คงไม่ชนกัน
      การใส่ timestamp ลงใน UUID ไม่ได้มีไว้เพื่อเพิ่มความหมาย แต่มีจุดประสงค์ด้าน การปรับแต่งประสิทธิภาพ
      คีย์ที่เพิ่มขึ้นตามลำดับเวลาช่วยลดต้นทุนการเขียน B-tree ใหม่และเพิ่มประสิทธิภาพการ insert ของฐานข้อมูล
    • เลขประจำตัวประชาชนของอิตาลีก็มีข้อมูลเพศรวมอยู่ด้วย ซึ่งกลายเป็นปัญหาหลังการผ่าตัดแปลงเพศ
      คำว่า “อย่าใส่ข้อมูลลงในตัวระบุถาวร” เป็นเพียงหลักทั่วไป และบางสถานการณ์ก็อาจยอมรับ trade-off นี้ได้
      ตัวอย่างเช่น ถ้าใช้ md5 hash เป็น UUID เพื่อทำดัชนี ก็จะมี fragmentation อยู่บ้างแต่ยังอยู่ในระดับที่จัดการได้
    • UUIDv7 เป็นเพียงวิธีสร้างที่มี time bias (random bias) ไม่ได้เก็บข้อมูลจริงเอาไว้
      การเลือก UUID แบบสุ่มเทียบกับแบบอิงเวลาอาจทำให้ต่างกันด้านประสิทธิภาพในระดับวินาที ไม่ใช่แค่มิลลิวินาที
    • ในฐานข้อมูลขนาดเล็กนี่อาจเป็นการปรับแต่งก่อนเวลาอันควร แต่เมื่อระบบใหญ่ขึ้นกลับต้องคิดตรงข้าม
      ในฐานข้อมูลขนาดใหญ่ การทำ sharding และการกระจายข้อมูลเป็นสิ่งจำเป็น ทำให้ UUID อาจทำงานได้ดีกว่า auto-increment
    • สำหรับตัวอย่างเลขประจำตัวประชาชนนอร์เวย์ ผมสงสัยว่าคนที่เกิด 1 มกราคมจะเยอะได้ขนาดนั้นจริงหรือ
      ถ้าฟอร์แมตคือ DDMMYYXXXXX ก็รองรับได้ถึง 100,000 คน อยากรู้ว่าจะเยอะขนาดนั้นจริงไหม
      น่าจะเป็นกรณีพิเศษ เช่น มีผู้ลี้ภัยไหลเข้ามาจำนวนมากในบางปี
  • ไม่ควรใช้ UUID เหมือน security token
    การเอาไปใช้เป็นกลไกความปลอดภัยเพียงเพราะเดายากถือว่าเสี่ยง
    จุดประสงค์ของค่าที่สุ่มไม่ได้มีแค่กันการเดา แต่ยังช่วยซ่อนความสัมพันธ์ระหว่าง ID ที่ต่อเนื่องกันด้วย

  • กลยุทธ์ PK จะแตกต่างกันอย่างสิ้นเชิงตามชนิดของฐานข้อมูล
    ใน Postgres นั้น PK แบบสุ่มไม่มีประสิทธิภาพ แต่ในฐานข้อมูลกระจายอย่าง Cockroach หรือ Spanner กลับเป็นคีย์ที่เพิ่มขึ้นแบบลำดับซึ่งทำให้เกิดปัญหา hot shard

    • แม้ในฐานข้อมูลแบบกระจาย คีย์ที่มี แนวโน้มเพิ่มขึ้น ก็ยังดีกว่าแบบสุ่มล้วน
      UUIDv7 มีบิตส่วนบนที่เรียงลำดับได้และบิตส่วนล่างที่เป็นสุ่ม จึงได้ทั้งการกระจายระหว่างโหนดและประสิทธิภาพในการเก็บข้อมูลภายในเครื่อง
    • ควรมองว่าเป็นความต่างของ โครงสร้างฐานข้อมูล มากกว่าความต่างของชนิดฐานข้อมูล
      ในฐานข้อมูลทั่วไปที่ไม่ได้ sharding คีย์แบบสุ่มจะทำให้ B-tree fragmentation
    • ใน Google Cloud Bigtable จะใช้คีย์แบบลำดับโดย กลับลำดับ (reverse) เพื่อให้ระบบกระจายข้อมูลอัตโนมัติ
    • ถ้าเป็น Postgres ที่ sharding แล้ว PK แบบสุ่มจะมีข้อได้เปรียบ
      แต่ถ้ามี range query เยอะ คีย์แบบสุ่มจะเสียเปรียบ
      สุดท้ายแล้วต้องเลือกตาม ลักษณะของ workload
    • ถ้าเป็น workload ที่เน้นการเขียนและมี time bias สูง แม้แต่ใน Postgres เอง PK แบบสุ่มก็อาจดีกว่า
  • ใจความของบทความชี้ข้อเสียของการใช้ UUIDv4 เป็น PK ได้ดี แต่แนวทาง การทำ integer ให้ดูอ่านไม่ออก ที่เสนอมาไม่น่าเหมาะกับบริการจริง
    ถ้าเป็นฐานข้อมูลขนาดเล็ก UUIDv7 น่าจะเป็นทางสายกลางที่สมเหตุสมผล

    • ผมชอบ UUIDv4 มากกว่า UUIDv7
      เพราะไม่ต้องการให้เวลาในการสร้างถูกเปิดเผย
      ตราบใดที่ข้อมูลยังไม่มากจนความสุ่มของ UUIDv4 กลายเป็นปัญหาด้านประสิทธิภาพ v4 ก็เป็นทางเลือกที่ปลอดภัยกว่า
    • ใน Postgres ผมชอบใช้ sequence เดียว
      แม้จะมีข้อมูลหลุดออกไปนิดหน่อย แต่ในเชิง การปฏิบัติการก็ยังคลุมเครือพอ
    • ถ้าแค่อยากซ่อนจำนวนผู้ใช้ ก็สามารถใช้ การเข้ารหัสแบบ permutation กับคีย์ auto-increment ได้
      เช่น แปลงด้วย AES-128 แล้วเข้ารหัส base64 ก็จะทำให้ดูเหมือน YouTube video ID
  • ผมเห็นหลายบริษัทระหว่างทำ technical due diligence และพบว่า ความสามารถในการ sharding ได้เร็ว เป็นหัวใจของการเติบโตของธุรกิจ
    ถ้าใส่ UUID ไว้ในทุกตาราง ก็สามารถขยายระบบตอนทำ sharding ได้โดยไม่ต้องเปลี่ยนโครงสร้าง
    มันให้ ข้อดีด้านการขยายระบบ ที่มากกว่าต้นทุนด้านพื้นที่และเวลาที่เสียไปเล็กน้อยมาก

    • UUIDv7 มีข้อได้เปรียบด้านประสิทธิภาพใน Postgres ด้วยเพราะมีคุณสมบัติเพิ่มขึ้นแบบลำดับ
    • เราเองก็ลำบากตอนทำ sharding เพราะไม่มี UUID
      สุดท้ายแล้วโมเดลข้อมูลที่ซับซ้อนทำให้การย้ายระบบยากอยู่ดี ไม่ได้ขึ้นกับมี UUID หรือไม่
  • แอปของเรา เข้ารหัส integer PK ให้ดูเหมือน UUID
    เพราะถ้าเปิดเผย ID แบบลำดับ คนก็อาจประมาณจำนวนลูกค้าหรือทำ dictionary attack ได้
    ID ที่เข้ารหัสแล้วช่วยให้ตรวจจับความพยายามสแกนได้ทันทีเมื่อถอดรหัสไม่สำเร็จ

    • แต่ก็อาจเกิดปัญหา ถอดรหัสไม่ได้ หากกุญแจหายหรือมีการเปลี่ยนกุญแจ
    • ผมอยากรู้ว่าจัดการกุญแจกันอย่างไร — inject ผ่าน environment variable, ฝังในโค้ด หรือใช้ AEAD scheme อย่าง AES-GCM หรือไม่ เพราะการจัดการความปลอดภัยตรงนี้สำคัญมาก
  • คำว่า “2 พันล้านก็น่าจะพอ” เป็นความคิดที่อันตราย
    DBA ทุกคนน่าจะมีอย่างน้อยหนึ่งเรื่องสยองที่เริ่มต้นจากการตัดสินใจแบบนั้น

  • บทความบอกว่า “ค่าที่สุ่มเรียงลำดับได้ไม่มีประสิทธิภาพ” แต่จริง ๆ แล้ว การเรียงตามลำดับไบต์ ทำได้
    เพียงแต่คีย์แบบสุ่มไม่ใช่การ insert แบบเรียงลำดับ จึงทำให้ B-tree ต้อง rebalance บ่อยและประสิทธิภาพตก

    • UUIDv4 มีประโยชน์ในสภาพแวดล้อมแบบกระจาย แต่ต้องแลกกับพื้นที่ 128 บิตและความไม่เป็นลำดับ
    • ผู้เขียนบอกว่าภายหลังได้เพิ่มการทดลองเปรียบเทียบดัชนี B-tree
      integer PK ทำให้ดัชนีพอดีกับหน่วยความจำได้ดีกว่า ส่วน UUIDv4 ต้องเข้าถึงเพจมากกว่า ทำให้ latency สูงขึ้น
    • ก็มีความเห็นว่าหลักฐานทางเทคนิคยังไม่แน่นพอ
    • B-tree จะ insert ได้มีประสิทธิภาพกว่าถ้าคีย์เพิ่มขึ้นตามลำดับ และคีย์แบบสุ่มมี cache locality แย่กว่า
    • ยิ่งการเข้าถึงข้อมูลผูกกับเวลาที่สร้างมากเท่าไร การเรียงตามเวลาก็ยิ่งได้เปรียบด้านประสิทธิภาพ
  • บทความนี้ดูเหมือนเป็นกรณี คิดวิธีแก้ก่อนเจอปัญหา มากกว่าจะเป็นปัญหาจริง
    UUIDv4 ใช้ได้ดีพอในกรณีส่วนใหญ่
    เรื่องประสิทธิภาพควรค่อยพิจารณาเมื่อมันเกิดขึ้นจริง

    • แต่ถ้าเริ่มต้นด้วย UUIDv4 ไปแล้ว ภายหลังแทบเป็นไปไม่ได้เลยที่จะ rekey กลับเป็น int64
    • ตอนที่เกิดปัญหาด้านประสิทธิภาพจริง ๆ ระบบก็มักอยู่ในช่วงเติบโตแล้วจนไม่มีเวลามาเปลี่ยน PK
  • สรุปคือ ใน Postgres นั้น UUIDv7 ให้ประสิทธิภาพดีกว่า v4 เล็กน้อย
    ในเวอร์ชันใหม่ ๆ ก็รองรับ UUIDv7 ได้โดยไม่ต้องใช้ปลั๊กอิน

    • แต่ประเด็นหลักของบทความคือการแนะนำว่า ถ้าเป็นไปได้ให้ใช้ sequence และ integer PK
    • ตั้งแต่ Postgres 18 เป็นต้นไปมีฟังก์ชัน uuidv7() ในตัว แต่ยังไม่ชัดเจนว่าส่วนขยายต่าง ๆ จะมีความสามารถมากกว่าหรือไม่
    • สำหรับผู้ใช้ส่วนใหญ่ ตอนนี้ก็น่าจะไม่ต้องใช้ส่วนขยายแยกแล้ว