Lời mở đầu
Suy nghĩ đơn giản trước: Khi bạn muốn tìm cái gì đó để làm định danh, không cần lo bị trùng, còn gì tuyệt vời hơn uuid, đơn giản, dễ xài, dễ triển khai.
UUID (Universally Unique Identifier) là lựa chọn rất phổ biến khi xây dựng các hệ thống hiện đại, bởi khả năng sinh giá trị duy nhất dễ dàng, không lo trùng lặp. Ban đầu, nó giúp mình rất nhiều trong việc tiết kiệm thời gian, giảm độ phức tạp trong quản lý dữ liệu.
Tuy nhiên, khi hệ thống lớn dần, chính UUID lại trở thành một nút thắt hiệu năng nghiêm trọng. Trong bài này, mình sẽ chia sẻ trải nghiệm thực tế đã cho mình 1 bài học đắt giá
Mục lục bài viết
- UUID là gì và tại sao lại phổ biến?
- Tại sao ban đầu mình lại chọn UUID làm Primary Key?
- Vấn đề phát sinh khi hệ thống lớn dần
- UUID và B-tree index – Nguyên nhân thực sự gây chậm
- Tại sao UUID làm B-tree Index nặng hơn nhiều so với số?
- Query chậm hơn do B-tree index trên chuỗi
- Giải pháp tạm thời: Hash Index
- Tại sao Hash Index lại nhanh hơn?
- Những hạn chế của Hash Index
- Những bất cập khác khi dùng UUID làm Primary Key
- Vậy sử dụng UUID thế nào là hợp lý nhất?
- Kết luận từ trải nghiệm thực tế
1. UUID là gì và tại sao lại phổ biến?
UUID là một chuỗi định danh 128-bit duy nhất, phổ biến trong việc xác định bản ghi dữ liệu. UUID trông giống như:
550e8400-e29b-41d4-a716-446655440000
Lý do UUID phổ biến là vì tính tiện lợi: sinh giá trị duy nhất ngay lập tức mà không cần thêm logic xử lý nào.
2. Tại sao ban đầu mình lại chọn UUID làm Primary Key?
Khi phát triển một hệ thống thương mại điện tử, mình muốn mỗi sản phẩm có một mã định danh duy nhất nhưng không dễ đoán được. Dùng số tự tăng thì sẽ lộ rõ quy luật, kém bảo mật và không chuyên nghiệp trên URL hay API endpoint. Ngoài ra, nếu chọn số tự tăng thì phải thêm một trường "slug" nữa, điều này tốn công quản lý hơn nhiều ( Nói thẳng ra là tại mình lười ) .
UUID giải quyết toàn bộ vấn đề trên. Rất tiện, rất nhanh, không cần suy nghĩ nhiều nữa.
3. Vấn đề phát sinh khi hệ thống lớn dần
Khi dữ liệu đạt ngưỡng hàng triệu bản ghi, mình phát hiện hiệu suất truy vấn bị giảm rõ rệt. Đặc biệt, những câu truy vấn dạng insert, select, update theo ID (UUID) trở nên cực kỳ chậm.
UUID và B-tree index – Nguyên nhân thực sự gây chậm
Mặc định PostgreSQL dùng B-tree index cho khóa chính (Primary Key). B-tree hoạt động hiệu quả nhất khi dữ liệu được chèn vào theo thứ tự tăng dần liên tục.
UUID ngẫu nhiên gây ra vấn đề nghiêm trọng: mỗi bản ghi mới được insert vào đều có giá trị nằm rải rác ngẫu nhiên trong cây B-tree, buộc PostgreSQL phải liên tục sắp xếp lại cấu trúc cây, tạo ra tình trạng phân mảnh dữ liệu nặng nề.
Tại sao UUID làm B-tree Index nặng hơn nhiều so với số?
B-tree là index dạng cây, cũng là cái mặc định mà posgresql sẽ xài cho trường id. Hiểu đơn giản thì nó giống như việc shipper đi tìm số nhà vậy, bạn tìm đường, số rồi vô hẻm, hang, hốc. Nếu số nhà đánh chuẩn, thì việc tìm kiếm này sẽ trở nên khá dễ dễ dàng. Trong B-tree, Mỗi node chứa nhiều key đã sắp xếp, giúp tìm kiếm theo giá trị (WHERE id = ...) rất nhanh. Khi insert, dữ liệu được chèn đúng vị trí, nếu node đầy thì sẽ tách ra và đẩy lên trên để giữ cân bằng. B-tree hoạt động hiệu quả nhất khi dữ liệu có thứ tự tăng dần (như số nguyên). Với UUID ngẫu nhiên, insert sẽ gây phân mảnh và làm chậm hiệu suất.
Khi so sánh trực tiếp với số nguyên (INT
hoặc BIGINT
), UUID là chuỗi ký tự dài hơn rất nhiều. Việc duy trì thứ tự trên cây B-tree của chuỗi ký tự dài và ngẫu nhiên nặng nề hơn đáng kể so với con số nhỏ gọn. Điều này khiến quá trình insert, update và ngay cả select cũng trở nên rất tốn tài nguyên CPU và IO hơn.
Query chậm hơn do B-tree index trên chuỗi
Hơn nữa, UUID là dạng string (chuỗi), khi database thực hiện tìm kiếm, so sánh giá trị chuỗi ký tự sẽ chậm hơn nhiều so với so sánh giá trị số. Chi phí xử lý mỗi node trong cây B-tree khi dùng chuỗi cao hơn rõ rệt so với số.
Kết quả thực tế là các câu query vốn rất nhanh trên trường BIGINT, giờ đây lại rất chậm khi dùng UUID làm khóa chính.
4. Giải pháp tạm thời: Hash Index
Vì không dễ thay đổi thiết kế ngay lập tức, mình thử nghiệm một giải pháp tạm thời là sử dụng Hash Index thay cho B-tree Index.
Tại sao Hash Index lại nhanh hơn?
Hash Index không cần sắp xếp dữ liệu theo thứ tự. Thay vào đó, nó sử dụng hàm băm để xác định chính xác vị trí của dữ liệu, giúp truy vấn dạng equality (so sánh bằng) cực kỳ nhanh.
Câu truy vấn dạng dưới đây được cải thiện rõ rệt về tốc độ:
SELECT * FROM products WHERE id = '550e8400-e29b-41d4-a716-446655440000';
Những hạn chế của Hash Index
Tuy nhiên, Hash Index không phải là giải pháp toàn diện:
- Không hỗ trợ truy vấn theo range (
<
,>
) hayORDER BY
. rất may là ít khi chúng ta cần dùng những dạng query này cho trường id - Có giới hạn nhất định về dung lượng dữ liệu tối đa.
Do đó, đây chỉ là giải pháp tạm thời, không thể sử dụng lâu dài khi hệ thống tiếp tục mở rộng.
5. Những bất cập khác khi dùng UUID làm Primary Key
Ngoài hiệu suất, UUID cũng gây một số vấn đề khác:
- Chiếm dung lượng lưu trữ lớn hơn đáng kể (16 bytes) so với INT (4 bytes) hay BIGINT (8 bytes). Điều này nghe nhỏ nhưng tăng chi phí lưu trữ rất lớn khi dữ liệu đạt hàng triệu, hàng tỷ bản ghi.
- Khó khăn khi debug hay thao tác thủ công do UUID dài và khó ghi nhớ.
6. Vậy sử dụng UUID thế nào là hợp lý nhất?
Từ trải nghiệm thực tế, mình rút ra lời khuyên khi sử dụng UUID là:
- Nên dùng số nguyên tự tăng (
BIGINT
) làm Primary Key chính thức, vì tối ưu tốc độ, chi phí lưu trữ, hiệu suất truy vấn. - UUID chỉ nên dùng làm trường phụ, có unique constraint, phục vụ việc hiển thị công khai trên URL hay API.
- Nếu vẫn muốn dùng UUID trực tiếp làm Primary Key, hãy cân nhắc sử dụng UUID v1 hoặc ULID, những dạng UUID tăng dần theo thời gian, giúp giảm thiểu phân mảnh dữ liệu trên B-tree.
7. Kết luận từ trải nghiệm thực tế
Trải nghiệm thực tế này cho mình một bài học quan trọng:
"Không phải giải pháp tiện lợi lúc đầu nào cũng sẽ tốt khi hệ thống mở rộng."
Những lựa chọn nhỏ ban đầu như UUID tưởng chừng đơn giản nhưng có thể trở thành gánh nặng rất lớn về sau khi quy mô dữ liệu lớn dần.
Hy vọng bài viết chi tiết này giúp các bạn có cái nhìn khác về ưu, nhược điểm của UUID, tránh được "bẫy" trong thiết kế cơ sở dữ liệu. Kiến thức là vô bờ bến, hi vọng nhận được sự đóng góp và chia sẻ của mọi người về những vấn đề tương tự