UUID vs bigint trong PostgreSQL: chọn ID có thể mở rộng
UUID vs bigint trong PostgreSQL: so sánh kích thước index, thứ tự sắp xếp, sẵn sàng sharding, và cách ID di chuyển qua API, web và app di động.

Tại sao việc chọn ID quan trọng hơn bạn nghĩ
Mỗi hàng trong một bảng PostgreSQL cần cách ổn định để tìm lại. Đó là vai trò của ID: nó xác định duy nhất một bản ghi, thường là primary key, và trở thành "keo" cho các quan hệ. Bảng khác lưu nó như foreign key, truy vấn join trên nó, và ứng dụng truyền nó như tay cầm cho “khách hàng đó”, “hóa đơn đó”, hoặc “ticket hỗ trợ đó”.
Bởi vì ID xuất hiện khắp nơi, lựa chọn không chỉ là một chi tiết cơ sở dữ liệu. Nó ảnh hưởng sau này tới kích thước index, kiểu ghi, tốc độ truy vấn, tỉ lệ trúng cache, và cả công việc sản phẩm như analytics, import và gỡ lỗi. Nó cũng quyết định bạn phơi bày gì trong URL và API, và việc một app mobile lưu và đồng bộ dữ liệu an toàn dễ hay khó.
Phần lớn đội so sánh UUID vs bigint trong PostgreSQL. Nói đơn giản, bạn chọn giữa:
- bigint: một số 64-bit, thường sinh bởi sequence (1, 2, 3...).
- UUID: một định danh 128-bit, nhìn ngẫu nhiên, hoặc sinh theo kiểu có thứ tự thời gian.
Không có lựa chọn nào thắng trong mọi trường hợp. Bigint thường gọn và thân thiện với index và sắp xếp. UUID phù hợp khi cần ID toàn cục duy nhất giữa các hệ thống, muốn ID công khai an toàn hơn, hoặc kỳ vọng dữ liệu được tạo ở nhiều nơi (nhiều dịch vụ, mobile offline, hoặc sharding trong tương lai).
Một quy tắc hữu ích: quyết định dựa trên cách dữ liệu sẽ được tạo và chia sẻ, chứ không chỉ cách bạn lưu trữ hôm nay.
Cơ bản về Bigint và UUID nói dễ hiểu
Khi mọi người so sánh UUID vs bigint trong PostgreSQL, họ chọn giữa hai cách đặt tên hàng: một số đếm nhỏ giống bộ đếm, hoặc một giá trị dài hơn và toàn cục.
Một bigint là số nguyên 64-bit. Trong PostgreSQL bạn thường sinh nó bằng identity column (hoặc pattern cũ serial). Cơ sở dữ liệu giữ một sequence phía sau và cấp số tiếp theo mỗi khi bạn insert. Điều đó có nghĩa ID thường là 1, 2, 3, 4... Nó đơn giản, dễ đọc và thân thiện trong công cụ và báo cáo.
Một UUID (Universally Unique Identifier) là 128 bit. Thường thấy viết dưới dạng 36 ký tự có dấu gạch nối, như 550e8400-e29b-41d4-a716-446655440000. Các loại phổ biến gồm:
- v4: UUID ngẫu nhiên. Dễ sinh trên client, nhưng không sắp theo thứ tự tạo.
- v7: UUID có thứ tự thời gian. Vẫn duy nhất, nhưng được thiết kế tăng theo thời gian.
Dung lượng lưu trữ là khác biệt thực tế đầu tiên: bigint dùng 8 bytes, trong khi UUID dùng 16 bytes. Khoảng cách kích thước đó xuất hiện trong index và ảnh hưởng tỉ lệ trúng cache (cơ sở dữ liệu có thể chứa ít bản ghi index trong bộ nhớ hơn).
Cũng nghĩ về nơi ID xuất hiện ngoài database. Bigint ngắn trong URL và dễ đọc từ log hoặc ticket hỗ trợ. UUID dài và khó gõ, nhưng khó đoán hơn và có thể sinh an toàn trên client khi cần.
Kích thước index và bloat bảng: thay đổi gì
Khác biệt thực tế lớn nhất giữa bigint và UUID là kích thước. Bigint 8 bytes; UUID 16 bytes. Nghe có vẻ nhỏ cho đến khi nhớ rằng index lặp lại ID của bạn nhiều lần.
Index khóa chính cần giữ nóng trong bộ nhớ để nhanh. Index nhỏ hơn nghĩa là nhiều dữ liệu hơn vừa vào shared buffers và cache CPU, nên lookup và join cần ít đọc đĩa hơn. Với primary key UUID, index thường lớn hơn đáng kể với cùng số hàng.
Hệ số tăng lên xuất hiện ở các index phụ. Trong B-tree của PostgreSQL, mỗi entry của index phụ cũng lưu giá trị primary key (để DB tìm hàng). Vì vậy khóa rộng hơn làm phình to không chỉ index chính, mà còn mọi index khác bạn thêm. Nếu bạn có ba index phụ, 8 bytes thừa từ UUID thực tế xuất hiện ở bốn chỗ.
Foreign key và bảng join cũng cảm nhận điều đó. Bất kỳ bảng tham chiếu ID của bạn đều lưu giá trị đó trong hàng và index của chính nó. Một bảng join nhiều-nhiều có thể chủ yếu là hai foreign key cộng chút overhead, nên nhân đôi bề rộng khóa có thể làm thay đổi khá lớn kích thước của nó.
Thực tế:
- UUID thường làm index chính và phụ lớn hơn, và khác biệt tăng theo số index.
- Index lớn hơn gây áp lực bộ nhớ và nhiều đọc trang hơn khi tải cao.
- Càng nhiều bảng tham chiếu khóa (events, logs, join tables), khác biệt kích thước càng đáng kể.
Nếu một user_id xuất hiện trong users, orders, order_items, và audit_log, cùng một giá trị được lưu và index trên tất cả những bảng đó. Chọn khóa rộng hơn là quyết định lưu trữ cũng như quyết định ID.
Thứ tự sắp xếp và kiểu ghi: sequential vs random IDs
Hầu hết primary key PostgreSQL nằm trên B-tree. B-tree hoạt động tốt nhất khi hàng mới rơi gần cuối index, vì DB chỉ cần append mà ít phải dọn dẹp lại.
ID tuần tự: dự đoán và thân thiện với lưu trữ
Với bigint identity hoặc sequence, ID mới tăng theo thời gian. Các chèn thường vào phần cuối index, nên trang giữ chặt, cache ấm và PostgreSQL làm ít công hơn.
Điều này quan trọng ngay cả khi bạn chưa bao giờ chạy ORDER BY id. Đường ghi vẫn phải đặt mỗi khóa mới vào index theo thứ tự.
UUID ngẫu nhiên: phân tán hơn, nhiều churn hơn
UUID ngẫu nhiên (thường là UUIDv4) phân tán chèn khắp index. Điều đó tăng khả năng page split, nơi PostgreSQL phải cấp trang index mới và di chuyển entry để chừa chỗ. Hệ quả là write amplification: nhiều byte index bị ghi hơn, nhiều WAL được sinh ra, và thường là nhiều công việc nền hơn sau đó (vacuum và quản lý bloat).
UUID theo thời gian thay đổi câu chuyện. UUID tăng theo thời gian (như kiểu v7 hoặc các scheme theo thời gian) khôi phục phần lớn locality, trong khi vẫn giữ 16 bytes và vẫn nhìn như UUID trong API.
Bạn sẽ cảm nhận khác biệt nhất khi có tần suất chèn cao, bảng lớn không vừa bộ nhớ, và nhiều index phụ. Nếu bạn nhạy cảm với spike độ trễ ghi do page split, tránh dùng UUID hoàn toàn ngẫu nhiên trên các bảng ghi nóng.
Ví dụ: một bảng events bận rộn nhận log từ mobile cả ngày thường chạy mượt hơn với khóa tuần tự hoặc UUID theo thời gian so với UUID ngẫu nhiên hoàn toàn.
Tác động hiệu năng bạn có thể cảm thấy
Hầu hết chậm thực tế không phải “UUID chậm” hay “bigint nhanh”. Mà là những gì DB phải chạm tới để trả lời truy vấn.
Kế hoạch truy vấn quan tâm chủ yếu đến việc liệu chúng có thể dùng index scan cho filter, join nhanh trên key, và liệu bảng có được sắp xếp vật lý (hoặc gần như vậy) để đọc theo range rẻ không. Với primary key bigint, hàng mới nằm theo thứ tự tăng, nên index chính giữ được gọn và thân thiện với locality. Với UUID ngẫu nhiên, chèn phân tán khắp index, gây nhiều page split và trật tự trên đĩa lộn xộn hơn.
Đọc là nơi nhiều đội nhận thấy trước. Khóa lớn hơn nghĩa là index lớn hơn, và index lớn hơn nghĩa là ít trang hữu dụng vừa trong RAM. Điều đó giảm cache hit và tăng IO, đặc biệt ở các màn hình join nặng như “liệt kê đơn hàng kèm thông tin khách”. Nếu working set không vừa bộ nhớ, schema nhiều UUID có thể đẩy bạn vượt ngưỡng đó sớm hơn.
Ghi cũng thay đổi. Chèn UUID ngẫu nhiên tăng churn trong index, gây áp lực lên autovacuum và có thể biểu hiện thành spike độ trễ khi tải cao.
Khi benchmark UUID vs bigint trong PostgreSQL, hãy làm cho công bằng: cùng schema, cùng index, cùng fillfactor, và đủ hàng để vượt RAM (không phải 10k). Đo p95 latency và IO, và thử cả cache ấm và lạnh.
Nếu bạn xây app trên AppMaster trên PostgreSQL, điều này thường xuất hiện dưới dạng trang danh sách chậm hơn và tải DB nặng hơn trước khi nó trông như “vấn đề CPU.”
Bảo mật và tính tiện dụng trong hệ thống công khai
Nếu ID rời database và xuất hiện trong URL, response API, ticket hỗ trợ và màn hình mobile, lựa chọn ảnh hưởng cả an toàn lẫn tính tiện dụng.
Bigint dễ với con người. Ngắn, có thể đọc qua điện thoại, và đội hỗ trợ nhanh chóng nhận ra mô hình như “tất cả đơn hàng lỗi quanh 9,200,000.” Điều đó tăng tốc gỡ lỗi, nhất là khi làm việc từ log hoặc ảnh chụp màn hình khách.
UUID hữu ích khi bạn phơi bày identifier ra công chúng. UUID khó đoán, nên việc dò /users/1, /users/2, /users/3 không hiệu quả. Nó cũng khó cho người ngoài suy ra quy mô bản ghi của bạn.
Cạm bẫy là nghĩ “không đoán được” đồng nghĩa với “an toàn”. Nếu kiểm tra phân quyền yếu, bigint dễ bị lạm dụng nhanh, nhưng UUID vẫn có thể bị lấy từ liên kết chia sẻ, log rò rỉ, hoặc cache API. Bảo mật phải đến từ kiểm tra quyền, không phải từ che giấu ID.
Cách tiếp cận thực tế:
- Thực thi kiểm tra sở hữu hoặc vai trò ở mọi read và write.
- Nếu phơi ID trong API công khai, dùng UUID hoặc token công khai riêng.
- Nếu muốn tham chiếu thân thiện với con người, giữ bigint nội bộ cho ops.
- Không mã hoá ý nghĩa nhạy cảm trong chính ID (ví dụ loại user).
Ví dụ: portal khách hiển thị invoice ID. Nếu invoices dùng bigint và API chỉ kiểm tra “invoice tồn tại”, ai đó có thể duyệt số và tải invoice của người khác. Sửa kiểm tra trước. Rồi quyết định liệu UUID cho invoice công khai có giảm rủi ro và tải hỗ trợ hay không.
Trên các nền tảng như AppMaster, nơi ID chảy qua API sinh tự động và app mobile, mặc định an toàn nhất là phân quyền nhất quán cộng với định dạng ID mà client xử lý đáng tin cậy.
Cách ID chảy qua API và app mobile
Loại dữ liệu bạn chọn không chỉ ở database. Nó lan ra mọi ranh giới: URL, payload JSON, lưu trữ client, log và analytics.
Nếu bạn thay đổi kiểu ID sau này, hỏng hóc hiếm khi là “chỉ một migration”. Foreign key phải thay đổi khắp nơi, không chỉ trong bảng chính. ORM và code generator có thể sinh lại model, nhưng tích hợp vẫn mong định dạng cũ. Ngay cả GET /users/123 cũng rắc rối khi ID biến thành chuỗi 36 ký tự. Bạn cũng phải cập nhật cache, message queue, và mọi nơi đã lưu ID như integer.
Với API, lựa chọn lớn nhất là định dạng và validation. Bigint đi qua như số, nhưng một số hệ thống (và ngôn ngữ) có nguy cơ mất chính xác nếu parse như số dấu phẩy động ở giá trị rất lớn. UUID đi qua như chuỗi, dễ parse hơn, nhưng bạn cần validate chặt để tránh “UUID gần giống” lọt vào log và DB.
Trên mobile, ID liên tục được serialize và lưu: response JSON, SQLite local, và hàng đợi offline lưu hành động đến khi có mạng. ID số nhỏ hơn về kích thước, nhưng UUID chuỗi thường dễ xử lý như token mù. Điều gây đau đầu thực sự là không đồng nhất: lớp này lưu như số, lớp kia như text, so sánh hoặc join trở nên mong manh.
Một vài quy tắc để tránh rắc rối:
- Chọn một biểu diễn canonical cho API (thường là chuỗi) và giữ nó.
- Validate ID ở rìa và trả lỗi 400 rõ ràng.
- Lưu cùng biểu diễn trong cache local và hàng đợi offline.
- Ghi log ID với tên trường và định dạng nhất quán giữa các service.
Nếu bạn xây client web và mobile với stack sinh mã (ví dụ AppMaster sinh backend và app native), hợp đồng ID ổn định càng quan trọng vì nó trở thành một phần của mọi model và request sinh tự động.
Sẵn sàng cho sharding và hệ phân tán
“Sharding-ready” chủ yếu có nghĩa bạn có thể tạo ID ở nhiều nơi mà không phá tính duy nhất, và có thể di chuyển dữ liệu giữa node sau này mà không phải viết lại mọi foreign key.
UUID phổ biến trong multi-region hoặc multi-writer vì bất kỳ node nào cũng sinh ID duy nhất mà không hỏi sequence trung tâm. Điều này giảm phối hợp và cho phép chấp nhận ghi ở nhiều vùng và gộp dữ liệu sau.
Bigint vẫn làm được nhưng cần kế hoạch. Các lựa chọn phổ biến gồm phân bổ dải số cho mỗi shard (shard 1 dùng 1-1B, shard 2 dùng 1B-2B), chạy sequence riêng với tiền tố shard, hoặc dùng ID kiểu Snowflake (bit thời gian cộng bit máy/shard). Những cách này giữ index nhỏ hơn UUID và bảo toàn phần nào thứ tự, nhưng thêm quy tắc vận hành bạn phải thực thi.
Các đánh đổi ảnh hưởng hàng ngày:
- Phối hợp: UUID gần như không cần; bigint thường cần phân bổ dải hoặc dịch vụ sinh.
- Xung đột: Tỉ lệ xung đột UUID cực thấp; bigint an toàn chỉ khi quy tắc phân bổ không chồng chéo.
- Thứ tự: nhiều scheme bigint sắp theo thời gian; UUID thường ngẫu nhiên trừ khi dùng biến thể theo thời gian.
- Độ phức tạp: bigint sharded giữ đơn giản chỉ khi đội kỷ luật.
Với nhiều đội, “sharding-ready” thực ra là “sẵn sàng migrate”. Nếu bạn đang ở một database đơn, chọn ID giúp công việc hiện tại dễ hơn. Nếu bạn đã xây nhiều writer (ví dụ qua API sinh tự động và mobile trên AppMaster), quyết định sớm cách ID được tạo và validate giữa các service.
Bước từng bước: chọn chiến lược ID phù hợp
Bắt đầu bằng việc mô tả thực tế ứng dụng của bạn. Một database PostgreSQL đơn ở một vùng có nhu cầu khác so với multi-tenant, hay hệ sẽ tách theo vùng, hoặc app mobile phải tạo record offline và sync sau.
Tiếp theo, thật lòng về nơi ID sẽ xuất hiện. Nếu identifier chỉ ở backend (job, tool nội bộ, panel admin), đơn giản thường thắng. Nếu ID xuất hiện trong URL, log chia sẻ với khách, ticket hỗ trợ, hoặc deep link mobile, tính dự đoán và quyền riêng tư quan trọng hơn.
Dùng thứ tự như yếu tố quyết định, đừng để nó là suy nghĩ sau. Nếu bạn dựa vào feed “mới nhất trước”, phân trang ổn định, hoặc audit dễ rà soát, ID tuần tự (hoặc ID có thứ tự thời gian) giảm bất ngờ. Nếu thứ tự không gắn với primary key, bạn có thể giữ PK riêng và sắp xếp theo timestamp.
Một luồng quyết định thực tế:
- Phân loại kiến trúc (single DB, multi-tenant, multi-region, offline-first) và liệu bạn có thể gộp dữ liệu từ nhiều nguồn.
- Quyết ID là identifier công khai hay chỉ nội bộ.
- Xác nhận nhu cầu sắp xếp và phân trang. Nếu cần thứ tự chèn tự nhiên, tránh ID hoàn toàn ngẫu nhiên.
- Nếu chọn UUID, chọn phiên bản có chủ đích: ngẫu nhiên (v4) để khó đoán, hoặc theo thời gian để locality index tốt hơn.
- Khoá quy ước sớm: một dạng text canonical, quy tắc chữ hoa/thường, validation, và cách mọi API trả/nhận ID.
Ví dụ: nếu app mobile tạo “draft orders” offline, UUID cho phép thiết bị sinh ID an toàn trước khi server thấy chúng. Trên công cụ như AppMaster, điều đó cũng tiện vì cùng định dạng ID chảy từ DB tới API tới web và native app mà không cần ngoại lệ.
Sai lầm phổ biến và bẫy cần tránh
Phần lớn tranh luận về ID sai vì mọi người chọn một kiểu vì một lý do, rồi bị bất ngờ bởi hệ quả sau này.
Một sai lầm thường gặp là dùng UUID hoàn toàn ngẫu nhiên trên bảng ghi nóng rồi thắc mắc tại sao chèn bị giật. Giá trị ngẫu nhiên phân tán hàng mới khắp index, dẫn đến nhiều page split và công việc DB nhiều hơn khi tải cao. Nếu bảng nhiều ghi, nghĩ về locality trước khi quyết.
Một vấn đề thường gặp khác là trộn kiểu ID giữa các service và client. Ví dụ, một service dùng bigint, service kia dùng UUID, và API của bạn có cả hai dạng số và chuỗi. Điều này tạo bug tinh vi: JSON parser mất chính xác với số lớn, mã mobile coi ID là số ở chỗ này và chuỗi ở chỗ kia, hoặc key cache không trùng.
Bẫy thứ ba là coi “ID không đoán được” là bảo mật. Dù dùng UUID, bạn vẫn cần kiểm tra phân quyền đúng.
Cuối cùng, đội thay đổi kiểu ID muộn mà không có kế hoạch. Phần khó nhất không phải PK mà là mọi thứ gắn với nó: foreign keys, join tables, URL, sự kiện analytics, deep link mobile và trạng thái client đã lưu.
Để tránh đau:
- Chọn một kiểu ID cho API công khai và giữ nó.
- Xử lý ID như chuỗi mù trong client để tránh cạnh trường hợp số.
- Không dùng độ ngẫu nhiên ID làm kiểm soát truy cập.
- Nếu phải migrate, version API và lên kế hoạch cho client tồn tại lâu.
Nếu bạn xây với nền tảng sinh mã như AppMaster, tính nhất quán càng quan trọng vì cùng ID chảy từ schema DB tới backend sinh và vào web và mobile app.
Danh sách kiểm tra nhanh trước khi quyết
Nếu lúng túng, đừng bắt đầu bằng lý thuyết. Bắt đầu với sản phẩm của bạn trong 1 năm tới, và ID sẽ đi qua bao nhiêu chỗ.
Hỏi:
- Bảng lớn nhất sẽ lớn bao nhiêu trong 12–24 tháng, và bạn có giữ năm lịch sử?
- Bạn cần ID sắp theo thời gian tạo để phân trang và gỡ lỗi dễ không?
- Có nhiều hệ thống tạo record cùng lúc không, bao gồm mobile offline hoặc job nền?
- ID xuất hiện trong URL, ticket hỗ trợ, export hay ảnh chụp màn hình khách không?
- Mọi client có thể xử lý ID cùng một cách không (web, iOS, Android, script), gồm validation và lưu trữ?
Sau đó kiểm tra lại phần plumbing. Nếu dùng bigint, đảm bảo có kế hoạch sinh ID ở mọi môi trường (dev local và import). Nếu dùng UUID, đảm bảo hợp đồng API và model client xử lý ID chuỗi nhất quán, và team quen đọc so sánh chúng.
Bài test thực tế nhanh: nếu app mobile cần tạo order offline và sync sau, UUID thường giảm công phối hợp. Nếu app chủ yếu online và bạn muốn index nhỏ gọn, bigint dễ hơn.
Nếu xây trên AppMaster (appmaster.io), quyết sớm vì quy ước ID chảy qua model PostgreSQL, API sinh và cả web lẫn native app.
Ví dụ thực tế
Một công ty nhỏ có tool vận hành nội bộ, portal khách, và app mobile cho nhân viên hiện trường. Cả ba đều gọi cùng PostgreSQL qua một API. Bản ghi mới tạo liên tục: ticket, ảnh, cập nhật trạng thái, hóa đơn.
Với bigint, payload API gọn và dễ đọc:
{ "ticket_id": 4821931, "customer_id": 91244 }
Phân trang tự nhiên: ?after_id=4821931&limit=50. Sắp xếp theo id thường khớp thời gian tạo, nên “ticket mới nhất” nhanh và dự đoán được. Gỡ lỗi cũng đơn giản: support hỏi “ticket 4821931” và hầu hết có thể gõ không nhầm.
Với UUID, payload dài hơn:
{ "ticket_id": "3f9b3c0a-7b9c-4bf0-9f9b-2a1b3c5d1d2e" }
Nếu dùng random UUID v4, chèn nằm khắp index. Điều này có thể dẫn đến churn index và debug hàng ngày hơi lộn xộn (copy/paste trở nên bình thường). Phân trang thường chuyển sang cursor-style thay vì “after id”.
Nếu dùng UUID theo thời gian, bạn giữ phần lớn hành vi “mới nhất trước” trong khi vẫn tránh ID dễ đoán trên URL công khai.
Trên thực tế, đội thường chú ý bốn điều:
- Bao nhiêu lần ID được gõ bởi con người so với copy
- Liệu “sort by id” có khớp “sort by created” không
- Phân trang cursor có sạch và ổn định không
- Dò một bản ghi qua log, API và màn hình mobile có dễ không
Bước tiếp theo: chọn mặc định, thử, và chuẩn hoá
Phần lớn đội bị kẹt vì muốn câu trả lời hoàn hảo. Bạn không cần hoàn hảo. Bạn cần một mặc định phù hợp sản phẩm hiện tại, cộng cách nhanh để xác minh nó không gây hại sau.
Quy tắc bạn có thể chuẩn hoá:
- Dùng bigint khi muốn index nhỏ nhất, thứ tự dự đoán và debug dễ.
- Dùng UUID khi ID phải khó đoán trong URL, bạn kỳ vọng tạo offline (mobile), hoặc muốn ít xung đột giữa hệ thống.
- Nếu có thể tách dữ liệu theo tenant hoặc vùng sau, ưu tiên kế hoạch ID làm việc xuyên node (UUID, hoặc scheme bigint phối hợp).
- Chọn một trong hai làm mặc định và hạn chế ngoại lệ. Tính nhất quán thường thắng việc tối ưu vi mô cho một bảng.
Trước khi khoá, chạy spike nhỏ. Tạo bảng với kích thước hàng thực tế, insert 1–5 triệu hàng, và so sánh (1) kích thước index, (2) thời gian chèn, và (3) vài truy vấn phổ biến với PK và một vài index phụ. Làm trên phần cứng thực và hình dạng dữ liệu của bạn.
Nếu lo sẽ thay đổi sau, lên kế hoạch migration để nó nhàm chán:
- Thêm cột ID mới và một unique index.
- Dual-write: điền cả hai ID cho hàng mới.
- Backfill hàng cũ theo lô.
- Cập nhật API và client để chấp nhận ID mới (giữ ID cũ hoạt động trong suốt chuyển đổi).
- Chuyển đọc, rồi drop key cũ khi log và metrics sạch.
Nếu bạn xây trên AppMaster (appmaster.io), quyết sớm vì quy ước ID chảy qua model PostgreSQL, API sinh và cả web và native app. Loại cụ thể quan trọng, nhưng tính nhất quán thường quan trọng hơn khi có người dùng thực và nhiều client.
Câu hỏi thường gặp
Ưu tiên bigint khi bạn có một database PostgreSQL đơn, hầu hết các ghi chép được tạo trên server, và bạn muốn index nhỏ gọn cùng hành vi chèn tiên đoán được. Chọn UUID khi ID cần được sinh ở nhiều nơi (nhiều service, mobile offline, sharding tương lai) hoặc khi bạn không muốn ID công khai dễ đoán.
Bởi vì ID được sao chép vào rất nhiều nơi: index primary key, mọi index phụ (như con trỏ tới hàng), cột foreign key ở các bảng khác, và các bảng join. UUID là 16 bytes so với 8 bytes của bigint, nên khác biệt kích thước sẽ nhân rộng khắp schema và có thể giảm tỉ lệ cache hit.
Trên các bảng có chèn nặng, có. UUID ngẫu nhiên (như v4) phân tán các chèn khắp B-tree, làm tăng page split và churn index khi tải cao. Nếu muốn dùng UUID nhưng vẫn cần chèn trơn tru, dùng chiến lược UUID theo thời gian để các khóa mới chủ yếu nằm ở cuối.
Thường thể hiện bằng IO nhiều hơn là CPU. Khóa lớn hơn nghĩa là index lớn hơn, và index lớn hơn nghĩa là ít trang vừa trong bộ nhớ, nên join và lookup có thể gây thêm đọc đĩa. Khác biệt rõ nhất trên bảng lớn, truy vấn nặng join, và khi working set không vừa RAM.
UUID giúp giảm việc dò ID như /users/1, nhưng không thay thế kiểm tra phân quyền. Nếu kiểm soát quyền sai, UUID vẫn có thể bị lộ và tái sử dụng. Xem UUID là tiện lợi cho ID công khai, còn bảo mật thực sự phải dựa vào kiểm soát truy cập chặt chẽ.
Dùng một cách biểu diễn chuẩn và giữ cố định. Một mặc định thực tế là coi ID là chuỗi trong API request và response, ngay cả khi database dùng bigint, vì tránh được các vấn đề số học ở client và đơn giản hoá validation. Dù chọn gì, hãy nhất quán trên web, mobile, log và cache.
Bigint có thể gây vấn đề ở một số client nếu bị parse như số dấu phẩy động, gây mất chính xác ở giá trị lớn. UUID tránh được vì là chuỗi, nhưng dài hơn và dễ bị xử lý sai nếu không validate kỹ. Cách an toàn nhất là nhất quán: một kiểu ở mọi nơi, với validation rõ ràng ở rìa API.
UUID là lựa chọn dễ dàng vì mỗi node có thể sinh ID duy nhất mà không cần sequence trung tâm. Bigint vẫn làm được nhưng bạn cần quy tắc như phân bổ dải số cho từng shard hoặc bộ sinh kiểu Snowflake, và phải tuân thủ vĩnh viễn. Nếu muốn câu chuyện phân tán đơn giản nhất, chọn UUID (ưu tiên loại theo thời gian).
Thay đổi kiểu primary key ảnh hưởng nhiều hơn một cột. Bạn phải cập nhật foreign key, bảng join, hợp đồng API, lưu trữ client, dữ liệu cache, sự kiện analytics và mọi tích hợp đã lưu ID như số hoặc chuỗi. Nếu có thể thay đổi, lên kế hoạch di chuyển dần với dual-write và cửa sổ chuyển đổi dài.
Giữ một khoá bigint nội bộ để hiệu quả database, và thêm một UUID (hoặc token) công khai riêng cho URL và API bên ngoài. Cách này cho index nhỏ gọn và debug thân thiện nội bộ, đồng thời tránh dò số dễ dàng cho ID công khai. Quan trọng là xác định sớm ID nào là “public ID” và không trộn lẫn tùy tiện.


