10 điểm bởi GN⁺ 2025-12-16 | 1 bình luận | Chia sẻ qua WhatsApp
  • UUID v4 có tính ngẫu nhiên cao, gây ra chỉ mục kém hiệu quả và I/O quá mức; khi dùng làm khóa chính trong PostgreSQL sẽ làm suy giảm hiệu năng
  • Do chèn ngẫu nhiên, hiện tượng tách trang (page split)phân mảnh chỉ mục xảy ra thường xuyên, đồng thời làm tăng kích thước log WAL và gây trễ ghi
  • UUID có kích thước 16 byte, chiếm không gian gấp đôi bigint, dẫn đến giảm tỷ lệ trúng cache và lãng phí bộ nhớ
  • Dù thường bị hiểu nhầm là định danh bảo mật, theo RFC 4122 thì UUID không phải là biện pháp bảo mật để chống đoán
  • Với cơ sở dữ liệu mới, nên dùng khóa dựa trên sequence kiểu số nguyên; nếu bắt buộc thì nên dùng UUID v7 theo thứ tự thời gian

Vấn đề hiệu năng của UUID v4

  • Cơ sở dữ liệu PostgreSQL dùng khóa chính UUID v4 trong 10 năm qua đã liên tục cho thấy suy giảm hiệu năng và I/O quá mức
    • UUID v4 tạo ngẫu nhiên 122 bit nên không thể sắp xếp chỉ mục hiệu quả
    • Khi chèn, dữ liệu không được ghi vào các trang tuần tự nên phát sinh truy cập ngẫu nhiên; khi cập nhật hoặc xóa cũng cần dò tìm kém hiệu quả
  • Chỉ mục B-Tree giả định dữ liệu có thứ tự, nhưng UUID v4 không có tính tuần tự nên hiệu quả chèn thấp
    • Mỗi lần chèn đều được ghi vào một trang bất kỳ, khiến tách trang ở giữa xảy ra thường xuyên
    • Điều này làm tăng độ trễ ghi (latency)WAL

Cấu trúc của UUID và các lựa chọn thay thế

  • UUID là định danh 128 bit (16 byte), trong PostgreSQL được lưu dưới dạng kiểu uuid nhị phân
  • UUID v4 dựa trên bit ngẫu nhiên, còn UUID v7 chứa timestamp ở 48 bit đầu nên hiệu quả chỉ mục cao hơn
  • PostgreSQL 18 (dự kiến năm 2025) sẽ hỗ trợ sẵn UUID v7
  • UUID v7 có thể sắp xếp theo thời gian nên cải thiện mật độ trang và hiệu quả cache

Lý do chọn UUID và các giới hạn

  • UUID được dùng khi cần tạo định danh không va chạm trong môi trường nhiều client hoặc microservice
    • Ví dụ: tạo ID đồng thời trên nhiều instance cơ sở dữ liệu
  • Tuy nhiên, RFC 4122 nêu rõ rằng “không nên giả định UUID là khó đoán”, nên không phù hợp làm định danh bảo mật
  • Xác suất va chạm đạt 50% khi tạo 2.71×10¹⁸ UUID; trên thực tế khả năng va chạm là rất thấp nhưng chi phí hiệu năng lại cao

Sự kém hiệu quả về không gian và I/O của UUID

  • UUID chiếm không gian gấp hai lần bigint (8 byte)bốn lần int (4 byte)
    • Với bảng lớn, điều này dẫn đến tăng dung lượng lưu trữtăng thời gian backup/restore
  • Kết quả thử nghiệm về mật độ trang chỉ mục
    • chỉ mục integer: 97.64%
    • chỉ mục UUID v4: 79.06%
    • chỉ mục UUID v7: 90.09%
  • Trong bài test của Cybertec, khi tra cứu chỉ mục UUID v4 đã phát sinh thêm 8.5 triệu lần truy cập trang, tương đương I/O tăng 31229%
    • Trong cùng điều kiện, chỉ mục bigint có 27,332 lần truy cập buffer, còn UUID v4 có 8,562,960 lần truy cập buffer

Ảnh hưởng tới cache và bộ nhớ

  • Do phân bố ngẫu nhiên, UUID làm tỷ lệ trúng buffer cache (cache hit ratio) thấp
    • Cần nạp nhiều trang hơn vào cache, và các trang cần thiết thường xuyên bị đuổi ra (eviction)
  • Hiệu quả cache giảm gây tăng độ trễ truy vấn và tăng mức sử dụng bộ nhớ
  • Để duy trì hiệu năng, nên thường xuyên tái tạo chỉ mục (REINDEX CONCURRENTLY) hoặc dùng pg_repack

Cách giảm nhẹ tác động hiệu năng

  • Mở rộng bộ nhớ: khuyến nghị RAM gấp 4 lần kích thước cơ sở dữ liệu (ví dụ: DB 25GB → bộ nhớ 128GB)
  • Điều chỉnh work_mem: cấp thêm bộ nhớ cho các phép sắp xếp có thể cải thiện hiệu năng
  • Trong môi trường Rails, có thể dùng thiết lập implicit_order_column để sắp xếp theo trường như created_at thay vì UUID
  • Có thể dùng lệnh CLUSTER để sắp xếp lại bảng theo trường có thể sắp thứ tự, nhưng cần khóa độc quyền

Khuyến nghị dùng khóa số nguyên và sequence

  • Với cơ sở dữ liệu mới, nên dùng khóa dựa trên sequence kiểu số nguyên
    • integer (4 byte) hỗ trợ khoảng 2 tỷ giá trị, còn bigint (8 byte) cung cấp nhiều giá trị duy nhất hơn rất nhiều
  • Phần lớn ứng dụng nghiệp vụ chỉ cần integer; các dịch vụ quy mô lớn nên dùng bigint
  • Thay vì UUID v4, nên dùng UUID v7 hoặc extension sequential_uuids như một giải pháp thay thế thực tế

Tóm tắt

  • UUID v4 gây ra chỉ mục kém hiệu quả, I/O cao, hiệu quả cache thấp do tính ngẫu nhiên
  • Không thể dùng như định danh bảo mật, đồng thời lãng phí không gian
  • Khóa sequence kiểu số nguyên phù hợp với đa số ứng dụng hơn
  • Nếu bắt buộc phải dùng UUID thì nên chọn UUID v7 theo thứ tự thời gian
  • Nên tránh dùng gen_random_uuid() làm khóa chính trong PostgreSQL

1 bình luận

 
GN⁺ 2025-12-16
Ý kiến Hacker News
  • Đây là một ví dụ điển hình của tối ưu hóa quá sớm
    Việc nhét dữ liệu vào định danh vĩnh viễn là điều cấm kỵ trong quản lý dữ liệu
    Nếu đưa ngày sinh vào ID như số định danh cư dân Na Uy, về sau sẽ phát sinh các trường hợp người nhập cư có ngày sinh từng bị ghi sai, hoặc quá nhiều người được gán ngày 1 tháng 1 khiến hết số
    Thời thẻ mục lục giấy thì còn có thể hiểu được việc trộn dữ liệu và định danh để giảm chi phí tra cứu, nhưng giờ đã có cơ sở dữ liệu mạnh nên không cần làm vậy nữa

    • Tôi nghĩ ví dụ này thực ra là vấn đề đặt giá trị mặc định sai
      Vấn đề là gán ngày sinh không rõ thành 1 tháng 1, chứ không phải bản chất của việc đưa ngày vào khóa
      Nếu dùng giá trị không phải ngày như 00 hoặc 99 thì đã không có xung đột
      Việc đưa timestamp vào UUID không phải để gán ý nghĩa mà nhằm tối ưu hiệu năng
      Khóa tăng theo thời gian giúp giảm chi phí ghi lại B-tree và cải thiện hiệu năng chèn của DB
    • Mã định danh cư dân Ý cũng chứa giới tính, và điều đó gây vấn đề sau khi chuyển giới
      “Đừng nhét dữ liệu vào định danh vĩnh viễn” chỉ là nguyên tắc chung, còn trong thực tế vẫn có thể chấp nhận đánh đổi tùy tình huống
      Ví dụ dùng hash md5 làm UUID để tạo chỉ mục thì vẫn có phân mảnh, nhưng ở mức có thể quản lý được
    • UUIDv7 về cơ bản chỉ là cách tạo có thiên lệch theo thời gian (random bias), chứ không chứa thông tin thực sự
      Việc chọn UUID ngẫu nhiên hay dựa trên thời gian có thể tạo khác biệt hiệu năng tính bằng giây chứ không chỉ mili giây
    • Với DB nhỏ thì đó là tối ưu hóa quá sớm, nhưng khi quy mô lớn lên thì lại cần cách tiếp cận ngược lại
      Trong DB lớn, sharding và phân tán là bắt buộc nên UUID hoạt động tốt hơn auto-increment
    • Về ví dụ số định danh Na Uy, tôi nghi ngờ liệu có thể thật sự có nhiều người sinh ngày 1 tháng 1 đến thế không
      Nếu định dạng là DDMMYYXXXXX thì có thể bao phủ tới 100.000 người, nên không rõ liệu có thể dồn đến mức đó hay không
      Có lẽ chỉ xảy ra trong tình huống đặc biệt như một năm nào đó có làn sóng người tị nạn đổ vào lớn
  • Không nên dùng UUID như token bảo mật
    Dùng nó cho mục đích bảo mật chỉ vì khó đoán là rất rủi ro
    Mục đích của giá trị ngẫu nhiên không chỉ là chống đoán ra, mà còn để che giấu mối quan hệ giữa các ID liên tiếp

  • Tùy loại DB mà chiến lược PK thay đổi hoàn toàn
    Trong Postgres, PK ngẫu nhiên kém hiệu quả, nhưng ở các DB phân tán như Cockroach hay Spanner thì khóa tăng đơn điệu lại gây ra vấn đề hot shard

    • Ngay cả trong DB phân tán, khóa có xu hướng tăng vẫn tốt hơn hoàn toàn ngẫu nhiên
      UUIDv7 có các bit cao có thể sắp xếp được và các bit thấp là ngẫu nhiên, nên vừa có phân tán giữa các node vừa giữ hiệu quả lưu trữ cục bộ
    • Nên xem đây là khác biệt về cấu trúc DB hơn là khác biệt về loại DB
      Trong DB thông thường không sharding, khóa ngẫu nhiên gây phân mảnh B-tree
    • Trên Google Cloud Bigtable, người ta dùng khóa tuần tự theo thứ tự đảo ngược (reverse) để tạo phân tán tự động
    • Với Postgres có sharding thì PK ngẫu nhiên có lợi hơn
      Nhưng nếu có nhiều truy vấn phạm vi (range query) thì khóa ngẫu nhiên lại bất lợi
      Cuối cùng vẫn phải chọn theo đặc tính workload
    • Với workload thiên về ghi và có thiên lệch thời gian lớn, trong Postgres PK ngẫu nhiên cũng có thể tốt hơn
  • Bài viết đã chỉ ra khá tốt nhược điểm của việc dùng UUIDv4 làm PK, nhưng cách làm rối bằng số nguyên được đề xuất thì có vẻ không phù hợp cho dịch vụ thực tế
    Với DB quy mô nhỏ, UUIDv7 là một phương án thỏa hiệp hợp lý

    • Tôi thích UUIDv4 hơn UUIDv7
      Vì tôi không muốn thời điểm tạo bị lộ ra
      Trừ khi dữ liệu đủ lớn để tính ngẫu nhiên của UUIDv4 gây ra vấn đề hiệu năng, còn không thì v4 là lựa chọn an toàn hơn
    • Trong Postgres, tôi thích dùng một sequence duy nhất
      Có lộ chút thông tin nhưng về vận hành thì đủ mơ hồ
    • Nếu chỉ muốn che số lượng người dùng, bạn có thể áp dụng hoán vị mã hóa (permutation) cho khóa auto-increment
      Ví dụ biến đổi bằng AES-128 rồi mã hóa base64 để nó trông giống ID video YouTube
  • Tôi từng xem nhiều công ty trong các đợt thẩm định kỹ thuật, và khả năng sharding nhanh là yếu tố cốt lõi cho tăng trưởng doanh nghiệp
    Nếu mọi bảng đều dùng UUID thì khi sharding có thể mở rộng mà không phải đổi cấu trúc
    Lợi ích về khả năng mở rộng lớn hơn nhiều so với chút thiệt hại về không gian và thời gian

    • UUIDv7 cũng có lợi thế hiệu năng trong Postgres nhờ đặc tính tăng đơn điệu
    • Chúng tôi cũng từng rất vất vả trong quá trình sharding vì không có UUID
      Cuối cùng do mô hình dữ liệu quá phức tạp nên bản thân việc migration đã khó, bất kể có UUID hay không
  • Ứng dụng của chúng tôi mã hóa PK số nguyên để trông giống UUID
    Vì nếu lộ ID tuần tự thì có thể suy ra số lượng khách hàng hoặc thực hiện dictionary attack
    ID đã mã hóa còn giúp phát hiện ngay các nỗ lực quét vì giải mã sẽ thất bại

    • Nhưng khi mất hoặc thay khóa thì có thể phát sinh vấn đề không thể giải mã
    • Tôi tò mò về cách quản lý khóa — tiêm qua biến môi trường, nhúng vào mã nguồn, hay dùng lược đồ AEAD như AES-GCM; việc quản lý bảo mật rất quan trọng
  • Câu “2 tỷ là đủ” là một nhận định nguy hiểm
    DBA nào cũng thường có ít nhất một câu chuyện ác mộng bắt đầu từ kiểu quyết định như vậy

  • Bài viết nói “giá trị ngẫu nhiên sắp xếp không hiệu quả”, nhưng thực ra vẫn có thể sắp xếp theo thứ tự byte
    Chỉ là khóa ngẫu nhiên không được chèn tuần tự nên B-tree phải cân bằng lại thường xuyên, dẫn đến giảm hiệu năng

    • UUIDv4 hữu ích trong môi trường phân tán, nhưng phải chấp nhận cái giá của không gian 128 bit và tính không tuần tự
    • Tác giả nói rằng sau đó đã bổ sung thí nghiệm so sánh chỉ mục B-tree
      PK số nguyên giúp chỉ mục vừa bộ nhớ tốt hơn, còn UUIDv4 cần nhiều lần truy cập trang hơn nên độ trễ (latency) tăng lên
    • Cũng có ý kiến cho rằng cơ sở kỹ thuật của bài còn yếu
    • B-tree chèn hiệu quả hơn khi khóa tăng dần, còn khóa ngẫu nhiên thì kém thân thiện với cache
    • Càng gần với thời điểm tạo trong cách truy cập dữ liệu thì việc sắp xếp theo thời gian càng có lợi về hiệu năng
  • Bài này trông giống kiểu tối ưu hóa quá sớm khi đưa ra lời giải trước cả vấn đề hơn
    UUIDv4 là đủ ổn trong phần lớn trường hợp
    Chỉ nên tính đến vấn đề hiệu năng khi nó thực sự xảy ra

    • Nhưng một khi đã bắt đầu với UUIDv4 thì về sau gần như không thể đổi khóa (rekey) sang int64
    • Khi vấn đề hiệu năng thực sự xuất hiện thì thường hệ thống đã ở giai đoạn tăng trưởng, không còn dư địa để thay PK nữa
  • Tóm lại, trong Postgres thì UUIDv7 cho hiệu năng nhỉnh hơn v4 một chút
    Ở các phiên bản mới, UUIDv7 đã được hỗ trợ mà không cần plugin

    • Tuy vậy, trọng tâm của bài là khuyến nghị rằng nếu có thể thì hãy dùng PK kiểu sequence/số nguyên
    • Từ Postgres 18 đã có hàm uuidv7() tích hợp sẵn, nhưng vẫn chưa rõ liệu extension có nhiều tính năng hơn không
    • Với đa số người dùng, giờ đây có lẽ không còn cần extension riêng nữa