Cân nhắc khóa chính không nguyên


16

Bối cảnh

Tôi đang thiết kế một cơ sở dữ liệu (trên PostgreQuery 9.6) sẽ lưu trữ dữ liệu từ một ứng dụng phân tán. Do tính chất phân tán của ứng dụng, tôi không thể sử dụng số nguyên tăng tự động ( SERIAL) làm khóa chính vì điều kiện chủng tộc tiềm năng.

Giải pháp tự nhiên là sử dụng UUID hoặc mã định danh duy nhất toàn cầu. Postgres đi kèm với một built-in UUIDloại , đó là hoàn toàn phù hợp.

Vấn đề tôi gặp phải với UUID liên quan đến gỡ lỗi: đó là một chuỗi không thân thiện với con người. Mã định danh không ff53e96d-5fd7-4450-bc99-111b91875ec5cho tôi biết gì, trong khi ACC-f8kJd9xKCd, trong khi không được đảm bảo là duy nhất, cho tôi biết tôi đang xử lý một ACCđối tượng.

Từ góc độ lập trình, thông thường để gỡ lỗi các truy vấn ứng dụng liên quan đến một số đối tượng khác nhau. Giả sử lập trình viên tìm kiếm sai ACCđối tượng (tài khoản) tại bảng ORD(thứ tự). Với một định danh có thể đọc được bằng con người, lập trình viên ngay lập tức xác định vấn đề, trong khi sử dụng UUID, anh ta sẽ dành thời gian để tìm ra điều gì sai.

Tôi không cần sự duy nhất "được bảo đảm" của UUID; Tôi làm cần một số phòng để tạo ra các phím mà không xung đột, nhưng UUID là quá mức cần thiết. Ngoài ra, trường hợp xấu nhất, đó sẽ không phải là ngày tận thế nếu xảy ra va chạm (cơ sở dữ liệu từ chối và ứng dụng có thể phục hồi). Vì vậy, đánh đổi cân nhắc, một định danh nhỏ hơn nhưng thân thiện với con người sẽ là giải pháp lý tưởng cho trường hợp sử dụng của tôi.

Xác định đối tượng ứng dụng

Mã định danh tôi đã đưa ra có định dạng sau : {domain}-{string}, nơi {domain}được thay thế bằng miền đối tượng (tài khoản, đơn hàng, sản phẩm) và {string}là một chuỗi được tạo ngẫu nhiên. Trong một số trường hợp, thậm chí có thể có ý nghĩa khi chèn a {sub-domain}trước chuỗi ngẫu nhiên. Chúng ta hãy bỏ qua chiều dài {domain}{string}cho mục đích đảm bảo tính độc đáo.

Định dạng có thể có kích thước cố định nếu nó giúp hiệu suất lập chỉ mục / truy vấn.

Vấn đề

Biết rằng:

  • Tôi muốn có các khóa chính với định dạng như ACC-f8kJd9xKCd.
  • Các khóa chính này sẽ là một phần của một số bảng.
  • Tất cả các khóa này sẽ được sử dụng trên một số liên kết / mối quan hệ, trên cơ sở dữ liệu 6NF.
  • Hầu hết các bảng sẽ có kích thước trung bình đến lớn (trung bình ~ 1M hàng; các bảng lớn nhất với ~ 100M hàng).

Về hiệu suất, cách tốt nhất để lưu trữ khóa này là gì?

Dưới đây là bốn giải pháp khả thi, nhưng vì tôi có ít kinh nghiệm với cơ sở dữ liệu nên tôi không chắc chắn (nếu có) là tốt nhất.

Giải pháp được coi là

1. Lưu trữ dưới dạng chuỗi ( VARCHAR)

(Postgres không có sự khác biệt giữa CHAR(n)VARCHAR(n), vì vậy tôi bỏ qua CHAR).

Sau một số nghiên cứu, tôi đã phát hiện ra rằng so sánh chuỗi với VARCHAR, đặc biệt là về các hoạt động tham gia, chậm hơn so với sử dụng INTEGER. Điều này có ý nghĩa, nhưng nó có phải là điều mà tôi nên lo lắng ở quy mô này?

2. Lưu trữ dưới dạng nhị phân ( bytea)

Không giống như Postgres, MySQL không có UUIDkiểu bản địa . Có một số bài viết giải thích cách lưu trữ UUID bằng trường 16 byte BINARY, thay vì 36 byte VARCHAR. Những bài đăng này đã cho tôi ý tưởng lưu trữ khóa dưới dạng nhị phân ( byteatrên Postgres).

Điều này giúp tiết kiệm kích thước, nhưng tôi quan tâm nhiều hơn đến hiệu suất. Tôi đã có một chút may mắn khi tìm thấy một lời giải thích về việc so sánh nhanh hơn: nhị phân hoặc chuỗi. Tôi tin rằng so sánh nhị phân là nhanh hơn. Nếu có, thì byteacó lẽ tốt hơn VARCHAR, mặc dù lập trình viên bây giờ phải mã hóa / giải mã dữ liệu mỗi lần.

Tôi có thể sai, nhưng tôi nghĩ cả hai byteaVARCHARsẽ so sánh (bằng) byte theo byte (hoặc ký tự theo ký tự). Có cách nào để "bỏ qua" so sánh từng bước này và chỉ đơn giản là so sánh "toàn bộ"? (Tôi không nghĩ vậy, nhưng nó không kiểm tra chi phí).

Tôi nghĩ lưu trữ bytealà giải pháp tốt nhất, nhưng tôi tự hỏi liệu có bất kỳ giải pháp thay thế nào khác mà tôi bỏ qua không. Ngoài ra, mối quan tâm tương tự mà tôi thể hiện ở giải pháp 1 là đúng: liệu chi phí so sánh có đủ để tôi lo lắng không?

Giải pháp "Sáng tạo"

Tôi đã đưa ra hai giải pháp rất "sáng tạo" có thể hoạt động, tôi chỉ không chắc chắn ở mức độ nào (nghĩa là nếu tôi gặp khó khăn khi nhân rộng chúng lên hơn một vài nghìn hàng trong một bảng).

3. Lưu trữ dưới dạng UUIDnhưng có "nhãn" đính kèm

Lý do chính để không sử dụng UUID là để các lập trình viên có thể gỡ lỗi ứng dụng tốt hơn. Nhưng điều gì sẽ xảy ra nếu chúng ta có thể sử dụng cả hai: cơ sở dữ liệu chỉ lưu trữ tất cả các khóa dưới dạng UUIDs, nhưng nó bao bọc đối tượng trước / sau khi truy vấn được thực hiện.

Ví dụ, lập trình viên yêu cầu ACC-{UUID}, cơ sở dữ liệu bỏ qua ACC-phần đó, tìm nạp kết quả và trả về tất cả chúng dưới dạng {domain}-{UUID}.

Có thể điều này có thể xảy ra với một số tin tặc với các thủ tục hoặc chức năng được lưu trữ, nhưng một số câu hỏi xuất hiện trong đầu:

  • Đây có phải (loại bỏ / thêm tên miền tại mỗi truy vấn) một chi phí đáng kể?
  • Điều này thậm chí có thể?

Tôi chưa bao giờ sử dụng các thủ tục hoặc chức năng được lưu trữ trước đây, vì vậy tôi không chắc liệu điều này có khả thi hay không. Ai đó có thể làm sáng tỏ? Nếu tôi có thể thêm một lớp trong suốt giữa lập trình viên và dữ liệu được lưu trữ, thì đó có vẻ là một giải pháp hoàn hảo.

4. (Yêu thích của tôi) Lưu trữ dưới dạng IPv6 cidr

Có, bạn đọc nó đúng. Hóa ra định dạng địa chỉ IPv6 giải quyết vấn đề của tôi một cách hoàn hảo .

  • Tôi có thể thêm tên miền và tên miền phụ ở một vài octet đầu tiên và sử dụng các tên miền còn lại làm chuỗi ngẫu nhiên.
  • Các tỷ lệ cược va chạm là OK. (Tôi sẽ không sử dụng 2 ^ 128, nhưng vẫn ổn.)
  • So sánh bình đẳng (hy vọng) được tối ưu hóa, vì vậy tôi có thể có hiệu suất tốt hơn so với việc sử dụng đơn giản bytea.
  • Tôi thực sự có thể thực hiện một số so sánh thú vị, như contains, tùy thuộc vào cách các miền và hệ thống phân cấp của chúng được thể hiện.

Ví dụ: giả sử tôi sử dụng mã 0000để thể hiện tên miền "sản phẩm". Key 0000:0db8:85a3:0000:0000:8a2e:0370:7334sẽ đại diện cho sản phẩm 0db8:85a3:0000:0000:8a2e:0370:7334.

Câu hỏi chính ở đây là: so với bytea, có bất kỳ lợi thế hay bất lợi chính nào khi sử dụng cidrkiểu dữ liệu không?


5
Có bao nhiêu nút phân phối là có thể? Bạn có biết số (và tên) của họ trước thời hạn không? Bạn đã xem xét các PK tổng hợp (nhiều màu) chưa? Một tên miền (tùy thuộc vào câu hỏi đầu tiên của tôi), cộng với một cột nối tiếp đơn giản có thể là nhỏ nhất, đơn giản nhất và nhanh nhất ...
Erwin Brandstetter

@Phil cảm ơn! @ErwinBrandstetter Về ứng dụng, nó được thiết kế để tự động chia tỷ lệ theo tải, do đó có rất ít thông tin trước thời hạn. Tôi đã nghĩ về việc sử dụng (tên miền, UUID) làm PK, nhưng điều này sẽ lặp lại "tên miền" trên tất cả, tên miền vẫn sẽ là varcharmột trong nhiều vấn đề khác. Tôi không biết về tên miền của pg, thật tuyệt khi tìm hiểu về. Tôi thấy các tên miền đang được sử dụng để xác thực nếu một truy vấn nhất định đang sử dụng đúng đối tượng, nhưng nó vẫn dựa vào việc có một chỉ mục không nguyên. Không chắc chắn nếu có một cách "an toàn" sử dụng serialở đây (không có một bước khóa).
Renato Siqueira Massaro

1
Tên miền không nhất thiết phải là a varchar. Xem xét làm cho nó một FK integerloại và thêm một bảng tra cứu cho nó. Bằng cách đó, bạn có thể có cả khả năng đọc của con người và bạn sẽ bảo vệ tổ hợp của mình PKkhỏi các dị thường chèn / cập nhật (đặt một miền không tồn tại).
từ


1
Tôi muốn có khóa chính với một định dạng như ACC-f8kJd9xKCd. Có vẻ như đó là một công việc cho khóa CHÍNH HÃNG tổng hợp cũ .
MDCCL

Câu trả lời:


5

Sử dụng ltree

Nếu IPV6 hoạt động, thật tuyệt. Nó không hỗ trợ "ACC". ltreelàm.

Đường dẫn nhãn là một chuỗi gồm 0 hoặc nhiều nhãn được phân tách bằng dấu chấm, ví dụ L1.L2.L3, đại diện cho một đường dẫn từ gốc của cây phân cấp đến một nút cụ thể. Độ dài của đường dẫn nhãn phải nhỏ hơn 65kB, nhưng giữ nó dưới 2kB thì tốt hơn. Trong thực tế đây không phải là một hạn chế lớn; ví dụ: đường dẫn nhãn dài nhất trong danh mục DMOZ ( http://www.dmoz.org ) là khoảng 240 byte.

Bạn sẽ sử dụng nó như thế này,

CREATE EXTENSION ltree;
SELECT replace('ACC-f8kJd9xKCd', '-', '.')::ltree;

Chúng tôi tạo dữ liệu mẫu.

SELECT x, (
  CASE WHEN x%7=0 THEN 'ACC'
    WHEN x%3=0 THEN 'XYZ'
    ELSE 'COM'
  END ||'.'|| md5(x::text)
  )::ltree
FROM generate_series(1,10000) AS t(x);

CREATE INDEX ON foo USING GIST (ltree);
ANALYZE foo;


  x  |                ltree                 
-----+--------------------------------------
   1 | COM.c4ca4238a0b923820dcc509a6f75849b
   2 | COM.c81e728d9d4c2f636f067f89cc14862c
   3 | XYZ.eccbc87e4b5ce2fe28308fd9f2a7baf3
   4 | COM.a87ff679a2f3e71d9181a67b7542122c
   5 | COM.e4da3b7fbbce2345d7772b0674a318d5
   6 | XYZ.1679091c5a880faf6fb5e6087eb1b2dc
   7 | ACC.8f14e45fceea167a5a36dedd4bea2543
   8 | COM.c9f0f895fb98ab9159f51fd0297e236d

Và viola ..

                                                          QUERY PLAN                                                          
------------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on foo  (cost=103.23..234.91 rows=1414 width=57) (actual time=0.422..0.908 rows=1428 loops=1)
   Recheck Cond: ('ACC'::ltree @> ltree)
   Heap Blocks: exact=114
   ->  Bitmap Index Scan on foo_ltree_idx  (cost=0.00..102.88 rows=1414 width=0) (actual time=0.389..0.389 rows=1428 loops=1)
         Index Cond: ('ACC'::ltree @> ltree)
 Planning time: 0.133 ms
 Execution time: 1.033 ms
(7 rows)

Xem tài liệu để biết thêm thông tin và toán tử

Nếu bạn đang tạo id sản phẩm, tôi sẽ ltree. Nếu bạn cần một cái gì đó để tạo ra chúng, tôi sẽ sử dụng UUID.


1

Chỉ liên quan đến việc so sánh hiệu suất với bytea. việc so sánh mạng được thực hiện theo 3 bước: đầu tiên là trên các bit chung của phần mạng, sau đó là về độ dài của phần mạng và sau đó là toàn bộ địa chỉ được lột mặt nạ. xem: network_cmp_iternal

Vì vậy, nó sẽ chậm hơn một chút sau đó bytea mà đi straigt để memcmp. Tôi đã chạy thử nghiệm đơn giản trên một bảng có 10 triệu hàng đang tìm kiếm một:

  • sử dụng id số (số nguyên), tôi mất 1000ms.
  • sử dụng cidr mất 1300ms.
  • sử dụng bytea mất 1250ms.

Tôi không thể nói có nhiều sự khác biệt giữa bytea và cidr (mặc dù khoảng cách vẫn nhất quán) Chỉ là iftuyên bố bổ sung - đoán rằng điều đó không quá tệ đối với 10m tuple.

Hy vọng nó sẽ giúp - rất thích nghe những gì bạn đã chọn.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.