Uniqueidentifier trong SQL là gì

Từ khi bắt đầu xây dựng dự án ta thường xuyên phải ra các quyết định khiến cho việc phát triển quy mô sau này trở nên dễ dàng hoặc khó khăn hơn. Đôi khi hướng đến những mục tiêu ngắn hạn không phải là một ý kiến tồi, nhất là khi deadline đã đến gần và ta phải hoàn thành sớm nhất có thể. Nhưng cũng có những lúc ta buộc phải lựa chọn những giải pháp kỹ thuật khó khăn hơn để đạt được mục tiêu lâu dài.

Dưới đây là một vài mẹo nhỏ để giúp cho database giữ được tính ổn định trong khi lượng thông tin lớn dần lên:

Sử dụng UUID

Khi nhắc đến "primary key" - hay "khóa chính", đa số chúng ta thường liên tưởng đến một trường ID tự động tăng. Phường pháp này rất tiện lợi với các cơ sở dữ liệu nhỏ, nhưng nó sẽ trở thành một vấn đề lớn khi cơ sở dữ liệu đủ lớn.

Với hệ quản trị cơ bản MySQL thì trường ID có định dạng là UNSIGNED INT có giá trị tối đa là 4,294,967,295 - không đủ để đánh ID cho toàn bộ người trên trái đất. Có một cách để tạm thời giải quyết vấn đề này là mở rộng bộ nhớ lên định dạng UNSIGNED BIGINT, giá trị tối đa lên đến 18,446,744,073,709,551,615.

Thêm vào đó, hệ quản trị CSDL truyền thống cần đảm bảo tính thống nhất. Có nghĩa là các primary key phải được đánh dấu lưu trữ trên một server duy nhất, vậy nên sẽ là một rắc rối lớn nếu có hàng nghìn yêu cầu ghi cùng một lúc.

Giải pháp UUID được đưa ra với hy vọng có thể giải quyết cả hai vấn đề trên. Nếu bạn chưa từng gặp UUID, thì nó là một key đặc trưng có dạng như sau: 123e4567-e89b-12d3-a456-426655440000.

Đây là định nghĩa đơn giản của wikipedia:

A universally unique identifier (UUID) is a 128-bit number used to identify information in computer systems. The term globally unique identifier (GUID) is also used. When generated according to the standard methods, UUIDs are for practical purposes unique, without requiring a central registration authority or coordination between the parties generating them. The probability that a UUID will be duplicated is not zero, but is so close to zero as to be negligible. Thus, anyone can create a UUID and use it to identify something with near certainty that the identifier does not duplicate one that has already been created to identify something else, and will not be duplicated in the future. Information labeled with UUIDs by independent parties can therefore be later combined into a single database, or transmitted on the same channel, without needing to resolve conflicts between identifiers.

Tạm dịch:

Một định danh phổ cập duy nhất (universally unique identifier - UUID) là một số 128 bit sử dụng để định danh dữ liệu trong hệ thống máy tính. Khái niệm này còn được gọi là định danh toàn cầu duy nhất (globally unique identifier - GUID). Có thể được tạo ra bởi các phương pháp thông thường, UUID phục vị mục đích thực tiễn là tạo ra các key duy nhất, mà không cần đến một trung tâm đăng ký chung hoặc liên kết giữa các cơ quan có thể sinh ra nó. Vì vậy. tỉ lệ một UUID bị trùng không hoàn toàn bằng 0, nhưng có gần 0 đến mức có thể được bỏ qua. Thêm vào đó, bất cứ ai có thể tạo ra một UUID và sử dụng nó để định danh gần như có thể chắc chắn rằng định danh nó không bị trùng lặp và đã được sử dụng để định danh thứ khác, và sẽ không bị sử dụng lại trong tương lai. Các thông tin được đánh dấu bởi UUID của các cơ sở độc lập có thể được trộn vào làm một, hoặc truyền trên cùng một kênh, mà không lo phải xử lý việc nhầm lẫn hay xung đột giữa các định danh.

Khi sử dụng UUID làm primary key, các yêu cầu ghi không nhất thiết phải đi qua một database. Thay vào đó database có thể được chia nhỏ trên nhiều server.

Thêm vào đó, nó còn cho phép thực hiện những yêu cầu như tạo ra ID cho một bản ghi trước khi nó được lưu vào database. Việc này có thể hữu dụng nếu muốn gửi bản ghi đến một server cache hoặc search, nhưng không muốn chờ đến khi transaction của database hoàn tất.

Để sử dụng UUID trên Rails app rất dễ dàng. Chỉ cần chỉnh sửa trên vào config file:

# config/application.rb config.active_record.primary_key = :uuid

Và thêm vào một bảng muốn sử dụng UUID với Rails migrations:

create_table :users, id: :uuid do |t| t.string :name end

Với những tùy chọn đơn giản, nếu được bật lên từ lúc bắt đầu phát triển, sẽ giúp tiếp kiệm được rất nhiều effort xử lý khi database trở nên quá lớn. Một tác dụng phụ khác là nó sẽ khỏ hơn với bot để có thể đoán được các private URL.

Count và counter

Khi thực sự quan sát, các trường hợp đếm số có mặt ở khắp mọi nơi. Email client đếm số lương mail chưa đọc, các trang blog đếm số bài viết, số comment và số trang để hiển thị list các bài viết đó.

Có hai vấn đề với dữ liệu lớn ở đây:

  1. Các query có dạng như SELECT COUNT(*) FROM users thường rất chậm. Nó thường là một vòng lặp chạy qua tất cả các bản ghi để đưa ra kết quả. Trong trường hợp có vài triệu bản ghi, việc đếm này sẽ mất một khoảng thời gian.
  2. Tăng tốc bằng cách sử dụng "counter cache" tỏ ra khá hiệu quả, nhưng cách này lại bị hạn chế trong việc chia database ra nhiều server.

Cách giải quyết dễ dàng nhất ở đây là tránh sử dụng counter bất cứ khi nào có thể, điều này cần được thực hiện ở bước đầu thiết kế.

Ví dụ, ta có thể chọn cho hiện list thông tin theo khoảng thời gian thay vì phân trang theo tổng số. Ta có thể chọn hiển thị những chỉ số tính toán phức tạp sau, hoặc lưu trữ giá trị vào database để làm giảm gánh nặng cho query. Nhưng giải pháp đại loại như vậy.

Hạn sử dụng và cất trữ dữ liệu

Trong thực tế, tồn tại một giới hạn trên của số dữ liệu có thể lưu trong một bảng SQL bất kì tùy thuộc vào số lượng RAM, CPU và ổ đĩa ngoài sở hữu. Điều này có nghĩa là sẽ đến một thời điểm khi mà ta cần phải chuyển những data không quan trọng ra khỏi các bảng chính.

Cùng xem xét một trường hợp đơn giản. Bạn muốn xóa bỏ những bản ghi cũ hơn 1 năm trong cơ sở dữ liệu, tổng cộng khoảng vài GB.

Để thực hiện điều này cần có một dòng code kiểu như:

MyRecords.where("created_at < ?", 1.year.ago).destroy

Vấn đề là câu query này có thể tốn hàng giờ, thậm chí lên đến hàng ngày để hoàn thành nếu database quá lớn.

Đây thực sự là một vấn đề đau đầu, bởi vì ta thường không nhận ra điều này cho đến khi quá muộn. Thường thí khó có người nào tính trước đến cả các chiến lược dọn dẹp dữ liệu thừa từ khi sản phẩm còn nhỏ, hay với các môi trường thử nghiệm chỉ với vài nghìn bản ghi.

Nhưng nếu đã có kế hoạch từ trước, thì ta có thể chuẩn bị một giải pháp đơn giản. Đó là phân mảnh các bảng. Ví dụ như thay vì viết tất cả dữ liệu vào bảng my_records, ta viết dữ liệu năm nay vào bảng my_records_1, dữ liệu năm sau vào bảng my_records_2... Khi đến thời gian loại bỏ dữ liệu cũ, ta chỉ cần chạy lệnh DROP TABLE my_records_1. Không giống lệnh delete, việc này chỉ tốn một khoảng thời gian rất ngắn.

Việc phân mảnh các trường khác cũng có thể được thực hiện tương tự.

Có cả một extension dành cho postgreSQL tên là pg_partman giúp xử lý phân vùng database mà không cần phải chỉnh sửa code. Hoặc nếu muốn điều khiển phân vùng bằng Ruby, có một gem hỗ trợ tên là partitionable.

Lời kết

Lần sau khi bạn bắt đầu xây dựng một project mới, hy vọng những mẹo nhỏ trên đây có thể có tác dụng trong việc lên trước kễ hoạch với những database lớn. Xin cảm ơn.

Thủ đô xứ Đông Lào,  HÀ LỘI, ngày càng nóng bức quá đi 😐 Trở về căn nhà rộng rãi với hàng mét giường chiếu trải đầy trong phòng càng làm tôi thêm yêu cái mùa hè của xứ sở đầy nắng và gió này hơn :v

Chính vì vậy hôm nay ngẫu hứng kể lại 1 câu chuyện về tình yêu của tôi với GUID, câu chuyện này thì đã được tôi kể đi kể lại khá nhiều lần trong thiên sử blog này mà vẫn không hết chuyện.

Index là gì phong cách Pokemon GO

Bông tuyết cô độc

Tối ưu SQL (phần 7): Chuyện ngắn của ID

Haizz đó là 1 câu chuyện đẹp nhưng cũng sẽ lấy đi của bạn nhiều nước mắt đó. Giờ thì vào phần chính của câu chuyện nhé

Tôi đây, sau hơn 5 năm ra trường đã đi qua không ít hệ thống lớn nhỏ, nhúng tay vào hàng chục con DB và để lại những hậu quả không thể kể ra như Trịnh Xuân Thanh hay như Trầm Bê. Số lượng câu lệnh select trên các bảng nhiều như số tiền mà mấy thằng kia tham nhũng được :)) Nói thì to tát thế chứ chắc éo bằng được đâu 😐 Chúng nó có cả 43 nghìn tỉ cơ mà :(( mình chỉ cần số 3 không nghìn mà để lại chữ tỉ đã có thể vui nguyên 1 tuần rồi :(. Ấy là vì cái lẽ SELECT thì lắm nên việc ngồi nghịch ngợm những thứ liên quan đến ID hay key của các table trong CSDL thì chắc chả phải nói rồi. Đối với tôi bây giờ, khi không được động chạm vào nữa thì GUID nhiều nữa, GUID giờ trong tim tôi như “CÔ GÁI ĐẾN TỪ HÔM QUA”. Do đó tôi xin chia sẻ lại 1 số kỉ niệm đượm buồn của tôi đối với người con gái đấy để anh em hiểu được phần nào những thứ tôi đã từng trải qua để tối ưu CSDL khi chung chăn gối với người con gái này.

Đối với anh em làm việc với SQL Server thì GUID là 1 cái gì đó rất là tự nhiên. Mỗi lần chúng ta CREATE TABLE ra thì việc đặt tiên chúng ta nghĩ đến đó là SET PRIMARY KEY cho em ấy. Vâng chính là em ấy, người con gái tên GUID. Nếu anh em nào hay vọc vạch DB thì khi tạo primary key mặc định nó sẽ tạo INDEX trên trường này với loại INDEX là CLUSTERED. Và đó chính là vấn đề của tôi và em ấy GUID. Tôi đã không nói rõ với gia đình em ấy (MSSQL) để rồi gia đình em đã ép buộc cho em cái còng CLUSTERED oan nghiệt đó. Vừa tội cho em và cũng vừa tội cho tôi.

Như anh em mà biết thì rất rất nhiều bài viết khuyên về việc đặt CLUSTERED INDEX nên là 1 trường nhỏ bé sinh sinh, có tính duy nhất, nên là trường số. Những điều này chỉ để đáp ứng cho việc tối ưu cho tốc độ truy cập, bộ nhớ cũng như dung lượng lưu trữ của SQL. Chính vì thế nên GUID với độ dài 128 bit là 1 cái gì đó thật là quá xa vời so với lý tưởng chúng tôi đã đề ra (Điều này anh em có thể đọc về lợi ích của GUID cũng như tác hại của em ấy trong bài Bông tuyết cô độc)

Tôi xin nói lại 1 số thuật ngữ về PRIMARY KEY và CLUSTERED INDEX cho anh em thấu hiểu lại như sau

Bản chất của PRIMARY KEY là về mặt logic của CSDL nhằm giúp xác định 1 bản ghi là duy nhất trong 1 TABLE. Do để thích để nó là cái quái gì cũng được từ INT cho đến NVARCHAR

Còn đối với CLUSTERD INDEX thì mang tính chất về mặt lưu trữ vật lý. Chính vì thế mới cần nó nhỏ, ổn định như INT hay BIGINT. Do đó khi sử dụng GUID làm CLUSTERD INDEX thì sẽ tốn bộ nhớ hơn rất nhiều, mà đã nhiều thì càng tốn nhiều page trong SQL, càng lắm page thì truy vấn chậm là chả tránh được :3 Tuy vậy nếu ở trên hệ thống phân phối thì sử dụng GUID mang lại lợi ích tuyệt vời khi cần sync dữ liệu giữa các server hay các thiết bị :v Chính vì vậy việc cân nhắc giữa sử dụng GUID hay không cũng là 1 vấn đề đấy chứ :))

Ở đây có ít số liệu so sánh về mức độ lưu trữ dành cho INT và GUID anh em liếc mắt đưa tình nhìn xem

Với 1 bảng 1 triệu bản ghi, khi sử dụng INT so với GUID làm PRIMARY KEY và CLUSTER INDEX thì INT tốn tầm 3.8 MB còn GUID tận 15.5 MB 😐 Trên đó mà sử dụng thêm 6 trường NONCLUSTER INDEX thì 1 thằng là 23MB 1 thằng 92MB

Đã có rất nhiều bài viết tranh cãi về vấn đề có nên đặt GUID làm PRIMARY KEY hay không, cãi nhau gọi là um tí tỏi luôn. Tôi sẽ để link đây và không nói gì.

Nếu như vậy thì giải pháp thế nào để tối ưu cho cái này đấy :3 Theo như ngu ý của mình đưa ra cũng như tham khảo về 1 số cách làm của các bậc tiền bối trên INTERNET thì cách giải quyết đó là sử dụng thêm 1 cột INT làm CLUSTER INDEX thay cho trường PRIMARY KEY kiểu GUID. Làm như vậy thì sẽ nhàn hạ hơn cho DB tuy nhiên chắc code sẽ vất hơn đấy :v

Câu lệnh đơn giản chỉ có

CREATE TABLE dbo.MyTable (PKGUID UNIQUEIDENTIFIER NOT NULL, MyINT INT IDENTITY(1,1) NOT NULL, .... add more columns as needed ...... ) ALTER TABLE dbo.MyTable ADD CONSTRAINT PK_MyTable PRIMARY KEY NONCLUSTERED (PKGUID) CREATE UNIQUE CLUSTERED INDEX CIX_MyTable ON dbo.MyTable(MyINT)

Đấy chỉ có thế thôi. Anh em tự suy ngẫm tiếp về cuộc tình của mình với GUID đi nhé :v Giờ thì mình đi ngâm nước lạnh đây :v


Video liên quan

Chủ đề