1 điểm bởi GN⁺ 4 giờ trước | 1 bình luận | Chia sẻ qua WhatsApp
  • Cách SQLite triển khai khóa chính tạo ra thứ tự lưu trữ vật lý khác nhau giữa bảng rowid thông thường và bảng WITHOUT ROWID, và nếu dùng UUID4 ngẫu nhiên làm clustered index thì sẽ phát sinh chi phí tái cân bằng B-tree và phân trang bổ sung
  • Mốc chuẩn với rowid kiểu số nguyên đạt khoảng 1 triệu lần chèn mỗi giây ở quy mô chèn 1 triệu hàng, còn UUID4 WITHOUT ROWID ghi nhận thời gian chèn chậm hơn 14–16 lần
  • Đặc tính không có thứ tự của UUID4 khiến các hàng bị chèn ngẫu nhiên vào B-tree, và kết quả profiling cho thấy tốn nhiều thời gian hơn cho cân bằng cây cũng như đọc/ghi
  • UUID7 WITHOUT ROWID là UUID theo thứ tự thời gian, giúp giảm vấn đề sắp xếp của UUID4 nên cho thời gian chèn hợp lý hơn, nhưng vẫn chậm hơn khóa số nguyên 8 byte vì đây là khóa BLOB 16 byte
  • UUID4 WITH ROWID tận dụng được tính tuần tự của rowid ẩn, nhưng vẫn chịu write amplification do có hai chỉ mục và chi phí chèn ngẫu nhiên vào chỉ mục, nên hiệu năng thấp hơn UUID7 WITHOUT ROWID

Clustered index là gì?

  • Clustered index là chỉ mục quyết định thứ tự lưu trữ vật lý của các hàng trong bảng
  • Mỗi hàng chỉ có thể được sắp xếp vật lý theo một cách duy nhất, nên mỗi bảng chỉ có một clustered index
  • Clustered index chính là bản thân bảng, còn non-clustered index chỉ lưu cột được lập chỉ mục và con trỏ tới vị trí chứa dữ liệu hàng thực tế

Rowid

  • Mọi bảng SQLite thông thường đều có khóa chính số nguyên 64-bit ngầm định gọi là rowid
  • Dữ liệu bảng được lưu trong cấu trúc B-tree với một entry cho mỗi hàng, dùng giá trị rowid làm khóa
  • rowid trên thực tế chính là clustered index của SQLite, và thứ tự lưu trữ vật lý của các hàng là theo thứ tự rowid
Quảng cáo

WITHOUT ROWID

  • SQLite hỗ trợ bảng WITHOUT ROWID, loại bảng này không có rowid ngầm định
  • Trong bảng WITHOUT ROWID, khóa chính được khai báo sẽ đóng vai trò clustered index
  • Bảng SQLite rowid được triển khai bằng B*-Tree nơi mọi nội dung được lưu ở lá, còn bảng WITHOUT ROWID dùng B-Tree thông thường lưu nội dung ở cả nút lá lẫn nút trung gian

Mốc chuẩn: khóa chính số nguyên rowid

  • Mốc chuẩn đo thời gian chèn theo lô 1 triệu hàng trên bảng rowid thông thường có cấu trúc id INTEGER PRIMARY KEY, data BLOB
  • Tổng số hàng trong bảng kết quả trải từ 10 triệu đến 100 triệu hàng, với thời gian đo trong khoảng 692ms đến 838ms
  • Hiệu năng mốc chuẩn vào khoảng 1 triệu lần chèn mỗi giây

UUID4 WITHOUT ROWID

  • Bài test UUID4 được chạy trên bảng WITHOUT ROWID có cấu trúc id BLOB PRIMARY KEY, data BLOB, chèn giá trị random-uuid4-bytes làm khóa chính
  • Thời gian đo trong bảng kết quả là 2649ms ở 10 triệu hàng và 12586ms ở 100 triệu hàng
  • Hiệu năng chèn chậm hơn 14–16 lần so với mốc chuẩn rowid số nguyên

Profiling

  • Diffgraph đã chuẩn hóa so sánh snapshot profiling của INTEGER và UUID4, và hiển thị khác biệt dưới dạng flamegraph
  • Chế độ xem chuẩn hóa điều chỉnh tổng số mẫu của hai profile về bằng nhau để có thể xem chênh lệch tương đối theo phần trăm
  • Khung màu xanh biểu thị thời gian của hàm đó trong profile thứ hai là UUID4 giảm so với INTEGER, còn khung màu đỏ là tăng lên trong UUID4
  • Cường độ màu thể hiện thay đổi tương đối về số mẫu của chính khung đó, tức mức thay đổi self time delta
  • Diffgraph cho thấy tốn nhiều thời gian hơn cho cân bằng cây cũng như đọc/ghi
  • Do UUID4 không có tính thứ tự, các khóa được sắp xếp theo thứ tự ngẫu nhiên, khiến SQLite liên tục phải tái cân bằng B-tree
Quảng cáo

UUID7 WITHOUT ROWID

  • UUID7 là UUID theo thứ tự thời gian, có thể loại bỏ vấn đề sắp xếp của UUID4
  • Bài test UUID7 cũng được chạy trên bảng WITHOUT ROWID có cấu trúc id BLOB PRIMARY KEY, data BLOB
  • Thời gian đo trong bảng kết quả là 1372ms ở 10 triệu hàng và 1258ms ở 100 triệu hàng
  • UUID7 WITHOUT ROWID quay về mức hợp lý hơn so với UUID4 WITHOUT ROWID, nhưng vẫn chậm hơn mốc chuẩn
  • Khóa chính UUID BLOB có kích thước 16 byte, trong khi khóa chính số nguyên chỉ là 8 byte

UUID4 WITH ROWID

  • Bài test UUID4 WITH ROWID dùng bảng id BLOB PRIMARY KEY, data BLOB mà không có WITHOUT ROWID
  • Trong cấu hình này, rowid ẩn là clustered index, và ưu điểm của rowid là tính tuần tự
  • Nhược điểm là bảng sẽ có hai chỉ mục, kéo theo write amplification
  • Thời gian đo trong bảng kết quả là 2003ms ở 10 triệu hàng và 7119ms ở 100 triệu hàng
  • UUID4 WITH ROWID không đạt hiệu năng tốt như UUID7 WITHOUT ROWID; dù không phải clustered index, nó vẫn phải liên tục xây chỉ mục do các lần chèn ngẫu nhiên

Kết luận

  • Trong SQLite, việc dùng UUID làm khóa chính có thể tạo ra khác biệt lớn về hiệu năng chèn tùy theo clustered index và tính sắp xếp của khóa
  • Vấn đề của UUID ngẫu nhiên không chỉ giới hạn ở SQLite mà còn mở rộng sang các cơ sở dữ liệu khác dùng clustered index
  • Toàn bộ mã benchmark được công khai tại kho GitHub

1 bình luận

 
Ý kiến trên Lobste.rs
  • Hay. Cũng sẽ thú vị nếu thấy cả số liệu khi khóa số nguyên trong bảng rowid không tăng đơn điệu mà là ngẫu nhiên
    Một điểm quan trọng bị thiếu trong bài là chỉ mục mặc định của bảng rowid là cây B+, còn bảng without rowid là cây B
    Vì vậy, nhìn chung khi kích thước bản ghi trung bình vượt qua một ngưỡng nào đó thì cách sau không còn lý tưởng, vì các nút nội bộ của chỉ mục lưu toàn bộ bản ghi; nếu nhớ không nhầm thì tài liệu SQLite đưa ra quy tắc kinh nghiệm là 1/20 kích thước trang

  • Đã bỏ khá nhiều công sức để đo hiệu năng của UUID mà lại không xét đến khóa tự nhiên
    Dù là số nguyên, UUID hay dạng khác, khóa thay thế chỉ làm tăng độ phức tạp, không thêm thông tin, và che giấu lỗi chuẩn hóa
    Tác giả nói dùng UUID để “tránh trùng lặp”, nhưng đó không phải là lý do. Mọi hàng trong mọi bảng của mọi cơ sở dữ liệu đều phải có ít nhất một khóa, tức là một tập cột xác định duy nhất hàng đó
    Một cơ sở dữ liệu không có khóa như vậy sẽ chứa thông tin trùng lặp và dễ gặp bất nhất logic. Cơ sở dữ liệu đã có khóa như vậy thì không cần khóa thay thế. Nếu khóa tự nhiên đã tồn tại và được cưỡng chế, thì lập luận “cần vì hiệu năng” đồng nghĩa với thêm chi phí và độ phức tạp không cần thiết, nên cần được chứng minh

    • Có thể là do tôi thiếu tưởng tượng, nhưng khi xử lý dữ liệu đến từ thế giới thực thì tôi thấy thường rất khó tìm được khóa tự nhiên thật sự
      Những giá trị trông có vẻ duy nhất thường không duy nhất như mong đợi, hoặc những giá trị tưởng là bất biến cuối cùng lại phải thay đổi
      Ngược lại, dùng khóa thay thế cho phép tôi định nghĩa danh tính trong hệ thống của mình mà không phải dựa vào cách người khác định nghĩa tính định danh, hoặc thường là không định nghĩa cho tử tế
      Vẫn có ngoại lệ. Nếu toàn bộ mô hình do tôi định nghĩa và dữ liệu không đến từ cái gọi là thế giới thực, thì khóa tự nhiên có ý nghĩa hơn. Nhưng khi thiết kế lược đồ chứa dữ liệu thực tế vốn có lẽ chưa bao giờ được chuẩn hóa hoàn toàn, khóa thay thế thường khá hữu ích
    • Khóa thay thế là số nguyên tăng đơn điệu có tính thực dụng tức thì khá lớn
      Có thể tham chiếu bất kỳ hàng nào bằng số nguyên nên việc truy cập đơn giản hơn nhiều, và con người cũng dễ nhớ, dễ dùng trong truy vấn
      Nếu tăng đơn điệu thì bản thân nó cũng mang thông tin. Dù là thông tin dư thừa, nhưng đúng là có
      Tốc độ tra cứu cũng được tối ưu. Đây gần như là trường hợp tối ưu để lập chỉ mục bằng cây B, bitmap, v.v.
      Tôi nghĩ mọi người chủ yếu dùng UUID vì nhầm lẫn. Thường là theo logic muốn làm khóa bị làm rối và khó đoán, nhưng tôi đã thôi cố hiểu vì sao lại nghĩ mục tiêu đó phải áp lên cả khóa chính thay vì một định danh riêng cho mục đích ấy
    • Cụm “tránh trùng lặp” không xuất hiện trong bài blog. Thực ra bài đó hoàn toàn không nói vì sao lại dùng UUID
  • UUID phiên bản 7 có dấu thời gian 48 bit ở phần đầu nên sẽ không phân bố ngẫu nhiên kiểu này. Vì thế việc phân trang quá mức và tái cân bằng cũng sẽ giảm

    • Đúng vậy. Bài viết đang nói về UUID7
  • Mọi người thật sự dùng UUID làm khóa chính à? Nếu cần UUID thì lợi ích của việc làm vậy thay vì để nó làm khóa phụ là gì?

    • Là vì mở rộng quy mô. Nếu muốn tạo ID duy nhất theo kiểu phân tán với tốc độ cao trên nhiều máy tính và nhiều trung tâm dữ liệu trên khắp thế giới, chẳng hạn như tải lên S3, thì bạn sẽ không muốn khóa một số nguyên tăng dần duy nhất. Khóa như vậy quá chậm để đồng bộ trên phạm vi toàn cầu
      GUID và UUID về mặt cấu trúc giải quyết bài toán này
      v1 và v6 mã hóa ID máy và dấu thời gian, nên gần giống số nguyên tự tăng có không gian tên riêng cho từng máy
      Nhiều người nhầm vì cho rằng UUID là ngẫu nhiên. Điều đó chỉ đúng với v4, và việc chọn v4 thì đáng tiếc là có cái giá của nó
      Thường sẽ có lúc cần một mức độ tính quyết định nào đó như v3, v5, v7 để giúp dọn dẹp dữ liệu hoặc đảm bảo hiệu năng và va chạm
    • Ngay cả khi dùng UUID ngẫu nhiên hay giá trị ngẫu nhiên phân bố đều làm khóa phụ, việc chèn vẫn chậm hơn. Dù không phải chỉ mục phân cụm thì cây B-tree của chỉ mục đó vẫn phải nhận các lần chèn ngẫu nhiên