ข้ามไปยังเนื้อหา
Toova
เครื่องมือทั้งหมด

UUID v4 vs v7 — ความแตกต่างและเมื่อใดใช้แต่ละตัว

Toova

UUID v4 เป็นค่าเริ่มต้นสำหรับ primary key แบบกระจายมาเกินทศวรรษ สร้างค่าสุ่ม 128-bit, format เป็นแปดกลุ่ม hex, เสร็จ ไม่ต้องการการประสาน, มีโอกาสการชนใกล้ศูนย์ในทางปฏิบัติ, ทำงานทุกที่ ดังนั้นทำไม UUID v7 — เป็นมาตรฐานใน RFC 9562 ในปี 2024 — แพร่กระจายอย่างรวดเร็วผ่านระบบ production ในปี 2026?

คำตอบสั้น: ประสิทธิภาพฐานข้อมูล UUID v4 ทำลาย locality B-tree index UUID v7 แก้สิ่งนั้นในขณะที่รักษาทุกอย่างที่นักพัฒนารักเกี่ยวกับ UUID บทความนี้อธิบายว่าแต่ละเวอร์ชันคืออะไรจริง, ทำไมความแตกต่างสำคัญที่ระดับใหญ่, เมื่อใดเลือกแต่ละตัว และวิธี migrate โดยไม่หยุดทำงาน

ต้องสร้าง UUID ตอนนี้? ลอง Toova UUID generator ซึ่งรองรับทั้ง v4 และ v7 ในจำนวนมาก สำหรับ identifier สุ่มอื่น, random string generator และ password generator ครอบคลุม token สั้นกว่า

UUID v4 — ความสุ่มล้วน

UUID เวอร์ชัน 4 ใช้ตัวสร้างเลขสุ่ม pseudorandom ที่ปลอดภัยเชิงเข้ารหัส (CSPRNG) เติม 122 ของ 128 bit อีก 6 bit ที่เหลือคงที่: 4 bit เข้ารหัสเวอร์ชัน (0100) และ 2 bit เข้ารหัส variant (10) ผลดูเช่นนี้:

f47ac10b-58cc-4372-a567-0e02b2c3d479
              ^^^^
              เวอร์ชัน = 4 (สุ่ม)

ความสุ่มคือฟีเจอร์ ระบบอิสระสองตัวสามารถสร้าง UUID โดยไม่ประสานและไม่ชน — ความน่าจะเป็นของการชนในชุด UUID v4 จำนวน 2.71 ล้านล้านตัวประมาณ 50% ซึ่งหมายถึงสำหรับการใช้งานปฏิบัติใด ความเสี่ยงไม่สำคัญ คุณไม่ต้องการ server ID กลาง, sequence ฐานข้อมูล หรือ distributed lock

วิธีสร้าง v4

import { v4 as uuidv4 } from 'uuid';
const id = uuidv4();
// => 'f47ac10b-58cc-4372-a567-0e02b2c3d479'

Library uuid (JavaScript), uuid.uuid4() ของ Python, google/uuid ของ Go และ runtime ภาษาหลักทุกตัว delegate ไป OS CSPRNG — /dev/urandom บน Linux, CryptGenRandom บน Windows ต้นทุนการสร้างเป็นศูนย์อย่างมีประสิทธิภาพ

ปัญหา: การ insert สุ่มฆ่า B-tree index

B-tree index เก็บข้อมูลที่เรียง เมื่อคุณ insert แถวใหม่ ฐานข้อมูลหาที่ที่คีย์ใหม่พอดีในลำดับที่เรียงและวางที่นั่น หากทุกคีย์ใหม่สุ่ม มันลงที่ตำแหน่งสุ่มใน index — ซึ่งหมายถึงทุก insert ต้องโหลดหน้าต่างกันจากดิสก์เข้า buffer pool ที่ปริมาณต่ำสิ่งนี้มองไม่เห็น ที่ปริมาณสูง (ล้านแถว, อัตรา INSERT สูง) มันสร้างรูปแบบที่เรียกว่า index fragmentation: index เต็มไปด้วยหน้าครึ่งว่างเพราะแต่ละ insert ตีตำแหน่งต่างกัน และ buffer pool churn ตลอดเวลาเพราะ hot working set ครอบคลุม index ทั้งหมดแทน slice ล่าสุดที่คาดเดาได้

อาการใน production: ความล่าช้า INSERT เพิ่ม, overhead autovacuum สูงขึ้น (PostgreSQL), แรงดัน checkpoint โต และประสิทธิภาพอ่านสำหรับ record ล่าสุดเสื่อมเพราะ "ล่าสุด" ไม่ map กับ locality ใน index อีก นี่ไม่ใช่สมมติฐาน — เป็นจุดเจ็บปวดที่บันทึกดีบนตาราง PostgreSQL และ MySQL ขนาดใหญ่ที่มี primary key UUID v4

UUID v7 — ความสุ่มเรียงตามเวลา

UUID v7 ออกแบบโดยชัดเจนเพื่อแก้ปัญหา fragmentation B-tree มันเข้ารหัส Unix millisecond timestamp 48-bit ใน bit ที่สำคัญที่สุด ตามด้วย version bit, random bit 12 ตัว, variant bit และ random bit 62 ตัวเพิ่ม ความสุ่มรวม: 74 bit — ยังเกินที่จำเป็นต่อการป้องกัน collision มาก

018f4e6b-a23c-7d45-9abc-0e02b2c3d479
 ^^^^^^^^^^^^^^
 Unix timestamp 48-bit (ความแม่นยำมิลลิวินาที)
                   ^
                   เวอร์ชัน = 7

เพราะ timestamp อยู่ใน bit สูง, UUID ที่สร้างทีหลัง sort หลัง UUID ที่สร้างก่อน Insert ถูก append ที่ขอบขวาของ index เสมอ ฐานข้อมูลต้องการเก็บเพียงหน้า index ล่าสุดร้อนใน buffer pool ไม่ใช่ทั้ง index ที่อัตรา INSERT สูง สิ่งนี้สามารถลดความล่าช้าเขียน 30-60% และลด I/O หนึ่งระดับขนาดบนตารางที่มีหลายสิบล้านแถว

วิธีสร้าง v7

import { v7 as uuidv7 } from 'uuid';
const id = uuidv7();
// => '018f4e6b-a23c-7d45-9abc-0e02b2c3d479'

Library uuid เดียวกันเพิ่มการรองรับ v7 ในเวอร์ชัน 10 Library มาตรฐานของ Python เพิ่มใน 3.14 PostgreSQL 17 รวม uuidv7() เป็น function ในตัว หากคุณอยู่ใน stack เก่ากว่า, library เล็กหลายตัวให้การสร้าง v7 โดยไม่มี dependency

Monotonicity ต่ำกว่ามิลลิวินาที

เกิดอะไรเมื่อ UUID v7 สองตัวถูกสร้างภายในมิลลิวินาทีเดียวกัน? RFC 9562 อนุญาตการนำไปใช้เพิ่ม counter monotonic ใน random bit เพื่อรับประกันลำดับภายในมิลลิวินาทีเดียวกัน Library uuid ทำสิ่งนี้โดยค่าเริ่มต้น ผล: แม้ ID 10,000 ตัวถูกสร้างในหนึ่งมิลลิวินาที, พวกมันยัง sort ถูกต้อง

ปัญหา Fragmentation B-Tree Index ในรายละเอียด

เพื่อเข้าใจทำไมเรื่องนี้สำคัญ, พิจารณาสิ่งที่เกิดที่ 10,000 insert ต่อวินาทีพร้อม primary key UUID v4 บนตารางที่มี 100 ล้านแถวที่มีอยู่:

  • แต่ละ insert สร้างคีย์สุ่ม 128-bit ที่ตกที่ตำแหน่งสุ่มท่ามกลาง 100 ล้านรายการที่มีอยู่
  • ฐานข้อมูลต้องโหลดหน้า B-tree เฉพาะที่มีตำแหน่งนั้นเข้า buffer pool
  • กับ 100 ล้านแถวและหน้า 8 KB, index ครอบคลุมประมาณ 100,000 หน้า แต่ละวินาที, 10,000 หน้าต่างกันจำเป็น — ไกลกว่า shared_buffers 8 GB ทั่วไปสามารถถือสำหรับเพียง index นี้
  • ทุก cache miss ผลในการอ่านดิสก์ ที่ 10,000 insert/sec, สิ่งนี้สามารถสร้างหลายพันการอ่านดิสก์สุ่มต่อวินาที, ทำให้ SSD ขนาดใหญ่อิ่ม

กับ UUID v7, ทั้ง 10,000 insert ต่อวินาทีลงที่หน้า leaf ขวาสุด (หรือหยิบมือเล็กของหน้าล่าสุด) Buffer pool เพียงต้องการเก็บหน้าเล็กเหล่านั้นร้อน อัตรา cache hit สำหรับการเขียนใกล้ 100% การขยายการเขียนลดลงอย่างมาก

ประโยชน์เดียวกันใช้กับ range scan: WHERE created_at BETWEEN x AND y บนตาราง v4 ต้องการ index scan เต็มหรือ index timestamp แยก บนตาราง v7, primary key เองคือ index timestamp — query สามารถ seek ตรงไปยังช่วงที่ถูก

วิธีใช้ UUID v7 ใน PostgreSQL

-- ทำงานเนทีฟกับ UUID type ใน PostgreSQL 16+
CREATE TABLE events (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- v4 (สุ่ม)
  -- สลับเป็น UUIDv7 ผ่าน extension หรือการสร้างฝั่งแอป
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- กับ UUIDv7: timestamp ฝังแล้ว ดังนั้น
-- คอลัมน์ created_at แยกนี้มักซ้ำซ้อน

PostgreSQL 17 ส่ง uuidv7() เนทีฟ สำหรับ PostgreSQL 14-16, extension pg_uuidv7 ให้ function เดียวกัน UUID data type เก็บทั้ง v4 และ v7 เหมือนกัน — 16 byte, ไม่มี overhead ความแตกต่างเดียวคือรูปแบบ bit ที่กำหนดลำดับ sort

ผลที่มีประโยชน์ข้อหนึ่ง: เนื่องจาก timestamp ฝัง, หลายตารางไม่ต้องการคอลัมน์ created_at แยกสำหรับการเรียงหรือแสดงอีก คุณสามารถดึง timestamp จาก UUID v7 ด้วยการเรียก function เดียว สิ่งนี้ลดความซับซ้อน schema และขจัดการเขียนต่อ insert

UUID v4 vs v7 — การแลกเปลี่ยน

คุณสมบัติ UUID v4 UUID v7
Bit สุ่ม 122 74
ลำดับ sort สุ่ม ตามลำดับเวลา
ประสิทธิภาพ INSERT B-tree แย่ (fragmentation) ยอดเยี่ยม (ตามลำดับ)
การรั่ว Timestamp ไม่มี ใช่ (ความแม่นยำ ms)
มาตรฐาน RFC RFC 4122 (2005), RFC 9562 (2024) RFC 9562 (2024)
การรองรับ library ทั่วโลก โตเร็ว (2024-2026)
created_at ฝัง ไม่ ใช่

คู่มือการ Migrate — v4 ไป v7

การ migrate ตารางที่มีอยู่จาก primary key UUID v4 เป็น v7 เป็นการดำเนินการหลายขั้น ข้อจำกัดหลัก: คุณไม่สามารถเปลี่ยนค่า primary key ที่ถูกอ้างอิงโดย foreign key โดยไม่อัปเดตตารางที่อ้างทั้งหมดด้วย วางแผนหน้าต่างบำรุงรักษาหรือใช้วิธี dual-write

-- 1. เพิ่มคอลัมน์ใหม่
ALTER TABLE orders ADD COLUMN id_v7 UUID;

-- 2. backfill แถวที่มีอยู่ (รักษา created_at เดิมเป็น
--    แหล่ง timestamp; ใช้ function library UUIDv7 ในแอปของคุณ)
UPDATE orders SET id_v7 = generate_uuidv7(created_at);

-- 3. ตรวจสอบไม่มี NULL เหลือ
SELECT COUNT(*) FROM orders WHERE id_v7 IS NULL;

-- 4. สลับคอลัมน์ (ต้องการหน้าต่างบำรุงรักษาสั้น)
ALTER TABLE orders ALTER COLUMN id_v7 SET NOT NULL;
ALTER TABLE orders ALTER COLUMN id_v7 SET DEFAULT generate_uuidv7(now());
ALTER TABLE orders RENAME COLUMN id TO id_v4_old;
ALTER TABLE orders RENAME COLUMN id_v7 TO id;
ALTER TABLE orders ADD PRIMARY KEY (id);

สำหรับการ migration ที่ไม่หยุดทำงานบนตาราง traffic สูง, วิธีแนะนำคือ:

  1. เพิ่มคอลัมน์ id_v7 ใหม่พร้อมค่าเริ่มต้นเซิร์ฟเวอร์ที่สร้าง UUID v7 ไปข้างหน้า
  2. Backfill แถวเก่าในชุดระหว่างช่วงเวลา traffic ต่ำ ใช้ timestamp created_at ที่มีเป็น seed สำหรับส่วน timestamp v7
  3. อัปเดตคอลัมน์ foreign key ทั้งหมดในตารางที่อ้างให้ชี้ไปที่ id_v7
  4. เปลี่ยนชื่อคอลัมน์และวาง constraint primary key เก่าระหว่างหน้าต่างบำรุงรักษาสั้น

ขั้น backfill ยาวที่สุด ที่ 10,000 แถวต่อชุดด้วย sleep 50 ms ระหว่างชุด ตาราง 100 ล้านแถวใช้ประมาณ 8 ชั่วโมง เริ่มแต่เนิ่นๆ

ผลกระทบในโลกจริง

ทีมวิศวกรรมหลายทีมได้เผยแพร่ benchmark เปรียบเทียบ UUID v4 และ v7 บน dataset ขนาด production การค้นพบสม่ำเสมอ:

  • Throughput INSERT: ปรับปรุง 2-5 เท่าบนตารางที่มี 50M+ แถวเมื่อสลับจาก v4 เป็น v7, กำไรเพิ่มเมื่อขนาดตารางโต
  • ความล่าช้าเขียน p99: ลดจากหลายร้อยมิลลิวินาที (v4, ภายใต้โหลด) เป็นมิลลิวินาทีเลขเดียว (v7) บนฮาร์ดแวร์เดียวกัน
  • ขนาด index: B-tree index บนคอลัมน์ v7 เล็กกว่า index v4 เทียบเท่าบนข้อมูลเดียวกัน 15-30% เพราะ fragmentation ทิ้งหน้าครึ่งว่างน้อยลง
  • ประสิทธิภาพ buffer pool: อัตรา shared buffer hit สำหรับ index primary key ไปจาก ~40% (v4, ตารางใหญ่) เป็น ~99% (v7) เพราะเพียงหน้าล่าสุดต้องคงร้อน

กำไรไม่สำคัญต่ำกว่าประมาณ 1 ล้านแถว หากตารางของคุณคงเล็ก, ติด v4 เพื่อความเรียบง่าย เหนือ 10 ล้านแถวพร้อมอัตรา INSERT ที่มีความหมายใด, v7 เป็นค่าเริ่มต้นดีกว่า

เมื่อใดใช้แต่ละตัว

ใช้ UUID v7 เมื่อ:

  • คุณกำลังออกแบบ schema ใหม่และตารางจะโตใหญ่ (10M+ แถว)
  • ตารางมีอัตรา INSERT สูง — event, log, order, message, notification
  • คุณต้องการ primary key ที่ทำหน้าที่ double เป็น timestamp การสร้าง (ขจัดคอลัมน์)
  • คุณอยู่บน PostgreSQL 17, MySQL 8.0+ หรือ ORM สมัยใหม่ที่รองรับการสร้าง v7
  • การเรียงตาม ID เทียบเท่าเชิงความหมายกับการเรียงตามเวลาสร้าง — ซึ่งจะเป็นสำหรับตารางที่ append หนักส่วนใหญ่

ใช้ UUID v4 เมื่อ:

  • Identifier เปิดเผยให้ผู้ใช้และต้องไม่เผยเวลาสร้าง (รหัสคำเชิญ, link แบ่งปัน, handle billing)
  • ตารางเล็กและคงที่ — ไม่มีประโยชน์ประสิทธิภาพที่ smbenarkan ต้นทุน migration
  • คุณกำลังสร้าง ID ในบริบทที่ timestamp การสร้างละเอียดอ่อน (record ผู้ใช้ส่วนตัวในผลิตภัณฑ์ที่แข่งบนภาพการเติบโต)
  • คุณต้องการสิ่งใดเป็น credential ครั้งเดียวหรือ token — ใช้ตัวสร้าง secret เฉพาะ ไม่ใช่เวอร์ชัน UUID ใด

สรุป

UUID v4 เป็นสุ่ม, ส่วนตัว และรองรับทั่วโลก UUID v7 เรียงตามเวลา, เป็นมิตรกับฐานข้อมูล และตอนนี้มาตรฐานใน library และฐานข้อมูลหลัก สำหรับ schema ใหม่ที่มีตารางใหญ่หรือโตเร็ว, v7 เป็นค่าเริ่มต้นดีกว่า สำหรับ ID ที่เปิดเผยให้ผู้ใช้ที่การรั่ว timestamp เป็นความกังวล, v4 ยังเป็นตัวเลือกที่ถูก

สร้างทั้งสองเวอร์ชันทันทีด้วย Toova UUID generator — ไม่ต้องบัญชี สำหรับ token สั้นกว่า, random string generator ผลิต ID เลขและตัวอักษรที่ความยาวใด