Khóa chính tổng hợp trong cơ sở dữ liệu SQL Server nhiều bên thuê


15

Tôi đang xây dựng một ứng dụng nhiều người thuê (cơ sở dữ liệu đơn, lược đồ đơn) bằng cách sử dụng API Web ASP, Entity Framework và cơ sở dữ liệu SQL Server / Azure. Ứng dụng này sẽ được sử dụng bởi 1000-5000 khách hàng. Tất cả các bảng sẽ có trường TenantId(Hướng dẫn / UNIQUEIDENTIFIER). Ngay bây giờ, tôi sử dụng Khóa chính trường đơn là Id (Hướng dẫn). Nhưng bằng cách chỉ sử dụng trường Id, tôi phải kiểm tra xem dữ liệu do người dùng cung cấp có phải từ / cho người thuê đúng không. Ví dụ, tôi có một SalesOrderbảng có một CustomerIdtrường. Mỗi lần người dùng đăng / cập nhật đơn đặt hàng, tôi phải kiểm tra xem có phải CustomerIdtừ cùng một người thuê không. Nó trở nên tồi tệ hơn bởi vì mỗi người thuê nhà có thể có một số cửa hàng. Sau đó tôi phải kiểm tra TenantIdOutletId. Đó thực sự là một cơn ác mộng bảo trì và xấu cho hiệu suất.

Tôi đang suy nghĩ để thêm TenantIdvào Khóa chính cùng với Id. Và có thể thêm OutletId, quá. Vì vậy, các Primary Key trong SalesOrderbảng sẽ là: Id, TenantId, và OutletId. Nhược điểm của phương pháp này là gì? Hiệu suất có bị tổn thương nặng khi sử dụng khóa tổng hợp không? Liệu thứ tự khóa tổng hợp có vấn đề? Có giải pháp tốt hơn cho vấn đề của tôi?

Câu trả lời:


33

Đã làm việc trên một hệ thống nhiều khách thuê quy mô lớn (phương pháp liên kết với khách hàng trải rộng trên 18 máy chủ, mỗi máy chủ có lược đồ giống hệt nhau, chỉ là khách hàng khác nhau và hàng ngàn giao dịch mỗi giây trên mỗi máy chủ), tôi có thể nói:

  1. Có một số người (ít nhất là một số ít nhất) sẽ đồng ý về việc bạn chọn GUID làm ID cho cả "TenantID" và bất kỳ "ID" thực thể nào. Nhưng không, không phải là một lựa chọn tốt. Bỏ qua tất cả các cân nhắc khác, sự lựa chọn đó sẽ bị tổn thương theo một số cách: bắt đầu phân mảnh, một lượng lớn không gian bị lãng phí (đừng nói rằng đĩa rẻ khi nghĩ về lưu trữ doanh nghiệp - SAN - hoặc các truy vấn mất nhiều thời gian hơn do mỗi trang dữ liệu giữ ít hàng hơn mức có thể với một INThoặc BIGINTthậm chí), hỗ trợ và bảo trì khó khăn hơn, v.v ... GUID rất tốt cho tính di động. Là dữ liệu được tạo ra trong một số hệ thống và sau đó chuyển sang một hệ thống khác? Nếu không, sau đó chuyển sang một loại nhỏ gọn hơn dữ liệu (ví dụ như TINYINT, SMALLINT, INT, hoặc thậm chí BIGINT), và tăng liên tục qua IDENTITYhoặcSEQUENCE.

  2. Với mục 1 trên đường đi, bạn thực sự cần phải có trường TenantID trong bảng MERYI có dữ liệu người dùng. Bằng cách đó bạn có thể lọc bất cứ thứ gì mà không cần THAM GIA thêm. Điều này cũng có nghĩa là TẤT CẢ các truy vấn đối với các bảng dữ liệu khách hàng được yêu cầu phải có TenantIDtrong điều kiện THAM GIA và / hoặc mệnh đề WHERE. Điều này cũng giúp đảm bảo rằng bạn không vô tình trộn dữ liệu từ các khách hàng khác nhau hoặc hiển thị dữ liệu của Người thuê nhà từ Người thuê B.

  3. Tôi đang suy nghĩ để thêm TenantId làm khóa chính cùng với Id. Và có thể thêm OutletId nữa. Vì vậy, khóa chính trong bảng đơn hàng sẽ là Id, TenantId, OutletId.

    Có, bạn nên đặt các chỉ mục được nhóm của mình trên các bảng dữ liệu máy khách thành các khóa tổng hợp, bao gồm TenantIDID ** . Điều này cũng đảm bảo rằng TenantIDtrong mọi chỉ mục NonClustered (vì chúng bao gồm Khóa chỉ mục cụm (s)) mà bạn sẽ cần dù sao vì 98,45% truy vấn đối với bảng dữ liệu khách hàng sẽ cần TenantID(ngoại lệ chính là khi rác thu thập dữ liệu cũ trên CreatedDatevà không quan tâm đến TenantID).

    Không, bạn sẽ không bao gồm các FK như OutletIDPK. PK cần xác định duy nhất hàng và thêm FK sẽ không giúp được gì. Trên thực tế, nó sẽ tăng cơ hội cho dữ liệu trùng lặp, giả sử rằng OrderID là duy nhất cho mỗi dữ liệu, TenantIDtrái ngược với duy nhất cho mỗi dữ liệu OutletIDtrong mỗi dữ liệu TenantID.

    Ngoài ra, không cần thiết phải thêm OutletIDvào PK để đảm bảo rằng các Outlets từ Tenant A không bị lẫn với Tenant B. Vì tất cả các bảng dữ liệu người dùng sẽ có TenantIDtrong PK, điều đó cũng có nghĩa TenantIDlà trong FK . Ví dụ: Outletbảng có PK (TenantID, OutletID)Orderbảng có PK (TenantID, OrderID) FK (TenantID, OutletID)tham chiếu PK trên Outletbảng. Các FK được xác định đúng sẽ ngăn dữ liệu của Người thuê nhà không bị trộn lẫn.

  4. Liệu thứ tự khóa tổng hợp có vấn đề?

    Vâng, đây là nơi nó được vui vẻ. Có một số tranh luận về lĩnh vực nào nên đến trước. Quy tắc "điển hình" để thiết kế các chỉ mục tốt là chọn trường được chọn nhiều nhất làm trường hàng đầu. TenantID, về bản chất, sẽ không phải là lĩnh vực được lựa chọn nhiều nhất; các IDlĩnh vực là lĩnh vực chọn lọc nhất. Dưới đây là một số suy nghĩ:

    • ID đầu tiên: Đây là trường chọn lọc nhất (nghĩa là độc đáo nhất). Nhưng bằng cách là trường tăng tự động (hoặc ngẫu nhiên nếu vẫn sử dụng GUID), dữ liệu của mỗi khách hàng sẽ được trải đều trên mỗi bảng. Điều này có nghĩa là có những lúc khách hàng cần 100 hàng và yêu cầu gần 100 trang dữ liệu đọc từ đĩa (không nhanh) vào Vùng đệm (chiếm nhiều dung lượng hơn 10 trang dữ liệu). Nó cũng làm tăng sự tranh chấp trên các trang dữ liệu vì sẽ thường xuyên hơn rằng nhiều khách hàng sẽ cần cập nhật cùng một trang dữ liệu.

      Tuy nhiên, thông thường bạn không gặp phải nhiều vấn đề về kế hoạch đánh hơi / lưu trữ bộ đệm xấu như các số liệu thống kê trên các giá trị ID khác nhau khá nhất quán. Bạn có thể không có được kế hoạch tối ưu nhất, nhưng bạn sẽ ít có khả năng nhận được những kế hoạch khủng khiếp. Phương pháp này về cơ bản hy sinh hiệu suất (một chút) trên tất cả các khách hàng để đạt được lợi ích của các vấn đề ít gặp hơn.

    • Người thuê trước:Điều này là rất nhiều không chọn lọc ở tất cả. Có thể có rất ít biến thể trên 1 triệu hàng nếu bạn chỉ có 100 TenantID. Nhưng số liệu thống kê cho các truy vấn này chính xác hơn vì SQL Server sẽ biết rằng một truy vấn cho Người thuê A sẽ lấy lại 500.000 hàng nhưng cùng một truy vấn cho Người thuê B chỉ có 50 hàng. Đây là nơi đau chính. Phương pháp này làm tăng đáng kể khả năng gặp phải các vấn đề đánh hơi tham số trong đó lần chạy đầu tiên của Quy trình được lưu trữ dành cho Người thuê A và hành động phù hợp dựa trên Trình tối ưu hóa truy vấn xem các thống kê đó và biết rằng nó cần có hiệu quả khi nhận được 500 nghìn hàng. Nhưng khi Tenant B, chỉ với 50 hàng, chạy, kế hoạch thực hiện đó không còn phù hợp, và trên thực tế, là không phù hợp. VÀ, vì dữ liệu không được chèn theo thứ tự của trường hàng đầu,

      Tuy nhiên, để TenantID đầu tiên chạy Quy trình được lưu trữ, hiệu suất phải tốt hơn so với cách tiếp cận khác vì dữ liệu (ít nhất là sau khi thực hiện bảo trì chỉ mục) sẽ được tổ chức về mặt vật lý và logic để có ít trang dữ liệu hơn để đáp ứng truy vấn. Điều này có nghĩa là I / O vật lý ít hơn, đọc ít logic hơn, ít tranh chấp hơn giữa Người thuê cho cùng một trang dữ liệu, ít lãng phí không gian hơn trong Vùng đệm (do đó Tuổi thọ Trang được cải thiện), v.v.

      Có hai chi phí chính để có được hiệu suất được cải thiện này. Đầu tiên không quá khó: bạn phải bảo trì chỉ mục thường xuyên để chống lại sự phân mảnh tăng lên. Thứ hai là một chút ít vui vẻ.

      Để chống lại các vấn đề đánh hơi tham số gia tăng, bạn cần tách các kế hoạch thực hiện giữa các đối tượng thuê. Cách tiếp cận đơn giản là sử dụng WITH RECOMPILEtrên procs hoặc OPTION (RECOMPILE)gợi ý truy vấn, nhưng đó là một điểm nhấn về hiệu suất có thể xóa sạch tất cả lợi nhuận đạt được bằng cách đặt lên hàng TenantIDđầu. Phương pháp mà tôi thấy hiệu quả nhất là sử dụng SQL động được tham số hóa thông qua sp_executesql. Lý do cần SQL động là để cho phép ghép TenantID vào văn bản của truy vấn, trong khi tất cả các vị từ khác thường là tham số vẫn là tham số. Ví dụ: nếu bạn đang tìm kiếm một Đơn hàng cụ thể, bạn sẽ làm một cái gì đó như:

      DECLARE @GetOrderSQL NVARCHAR(MAX);
      SET @GetOrderSQL = N'
        SELECT ord.field1, ord.field2, etc.
        FROM   dbo.Orders ord
        WHERE  ord.TenantID = ' + CONVERT(NVARCHAR(10), @TenantID) + N'
        AND    ord.OrderID = @OrderID_dyn;
      ';
      
      EXEC sp_executesql
         @GetOrderSQL,
         N'@OrderID_dyn INT',
         @OrderID_dyn = @OrderID;

      Hiệu quả của việc này là tạo ra một kế hoạch truy vấn có thể sử dụng lại cho TenantID đó sẽ khớp với khối lượng dữ liệu của Đối tượng thuê cụ thể đó. Nếu cùng một đối tượng thuê A thực hiện lại thủ tục được lưu trữ cho người khác @OrderIDthì nó sẽ sử dụng lại kế hoạch truy vấn được lưu trong bộ nhớ cache đó. Một đối tượng thuê khác đang chạy cùng một Quy trình được lưu trữ sẽ tạo ra một văn bản truy vấn chỉ khác nhau về giá trị của TenantID, nhưng bất kỳ sự khác biệt nào trong văn bản truy vấn là đủ để tạo ra một kế hoạch khác. Và gói được tạo cho Người thuê B sẽ không chỉ khớp với khối lượng dữ liệu cho Người thuê B mà còn có thể được sử dụng lại cho Người thuê B cho các giá trị khác nhau @OrderID(vì vị từ đó vẫn được tham số hóa).

      Nhược điểm của phương pháp này là:

      • Đó là một công việc nhiều hơn một chút so với việc chỉ nhập một truy vấn đơn giản (nhưng không phải tất cả các truy vấn cần phải là SQL động, chỉ là những truy vấn cuối cùng có vấn đề đánh hơi tham số).
      • Tùy thuộc vào số lượng Người thuê trên một hệ thống, nó sẽ tăng kích thước bộ đệm của gói vì mỗi truy vấn hiện yêu cầu 1 gói cho mỗi TenantID đang gọi nó. Điều này có thể không phải là một vấn đề, nhưng ít nhất là một cái gì đó cần phải nhận thức.
      • SQL động phá vỡ chuỗi sở hữu, có nghĩa là quyền truy cập đọc / ghi vào các bảng có thể được thừa nhận bằng cách có EXECUTEquyền đối với Quy trình được lưu trữ. Cách khắc phục dễ dàng nhưng kém an toàn chỉ là cung cấp cho Người dùng quyền truy cập trực tiếp vào các bảng. Điều này chắc chắn không lý tưởng, nhưng đó thường là sự đánh đổi cho nhanh chóng và dễ dàng. Cách tiếp cận an toàn hơn là sử dụng bảo mật dựa trên Chứng chỉ. Ý nghĩa, tạo Chứng chỉ, sau đó tạo Người dùng từ Chứng chỉ đó, cấp cho Người dùng đó các quyền mong muốn (Người dùng hoặc Đăng nhập dựa trên Chứng chỉ không thể tự kết nối với Máy chủ SQL), sau đó ký vào Quy trình được lưu trữ sử dụng SQL động với điều đó cùng chứng chỉ qua THÊM ĐĂNG KÝ .

        Để biết thêm thông tin về ký mô-đun và Chứng chỉ, vui lòng xem: ModuleSigning.Info
         

    Vui lòng xem phần CẬP NHẬT vào cuối cho các chủ đề bổ sung liên quan đến vấn đề xử lý giảm thiểu các vấn đề thống kê do quyết định này.


** Cá nhân tôi thực sự không thích chỉ sử dụng "ID" cho tên trường PK trên mỗi bảng vì nó không có ý nghĩa và nó không nhất quán trên các FK vì PK luôn là "ID" và trường trong bảng con phải bao gồm tên bảng cha. Ví dụ: Orders.ID-> OrderItems.OrderID. Tôi thấy dễ dàng hơn nhiều để đối phó với một mô hình dữ liệu có: Orders.OrderID-> OrderItems.OrderID. Nó dễ đọc hơn và giảm số lần bạn sẽ gặp lỗi "tham chiếu cột không rõ ràng" :-).


CẬP NHẬT

  • Sẽ những OPTIMIZE FOR UNKNOWN Gợi ý Query (giới thiệu trong SQL Server 2008) giúp đỡ với một trong hai Trật tự của các PK composite?

    Không hẳn vậy. Tùy chọn này thực hiện xung quanh các vấn đề đánh hơi thông số, nhưng nó chỉ thay thế một vấn đề này bằng một vấn đề khác. Trong trường hợp này, thay vì ghi nhớ thông tin thống kê cho các giá trị tham số của lần chạy đầu tiên của thủ tục được lưu trữ hoặc truy vấn được tham số hóa (điều này chắc chắn tuyệt vời đối với một số người, nhưng có thể là tầm thường đối với một số người, và có khả năng khủng khiếp đối với một số người), nó sử dụng chung thống kê phân phối dữ liệu để ước tính số lượng hàng. Đây là lần truy cập trúng hoặc có bao nhiêu truy vấn (và ở mức độ nào) sẽ bị ảnh hưởng tích cực, tiêu cực hoặc hoàn toàn không. Ít nhất với tham số đánh hơi một số truy vấn được đảm bảo có lợi. Nếu hệ thống của bạn có Người thuê với khối lượng dữ liệu rất đa dạng, điều này có khả năng ảnh hưởng đến hiệu suất cho tất cả các truy vấn.

    Tùy chọn này thực hiện tương tự như sao chép các tham số đầu vào vào các biến cục bộ và sau đó sử dụng các biến cục bộ trong truy vấn (Tôi đã kiểm tra điều này nhưng không có chỗ cho điều đó ở đây). Thông tin bổ sung có thể được tìm thấy trong bài đăng trên blog này: http://www.brentozar.com/archive/2013/06/optizes-for-unknown-sql-server-parameter-sniffing/ . Đọc các bình luận, Daniel Pepermans đã đi đến một kết luận tương tự như của tôi về việc sử dụng SQL động có biến thể hạn chế.

  • Nếu ID là trường hàng đầu trong Chỉ mục được nhóm, thì nó có giúp / đủ để có Chỉ mục không phân cụm trên (TenantID, ID) hay chỉ (TenantID) để có số liệu thống kê chính xác cho các truy vấn xử lý nhiều hàng của một người thuê không?

    Vâng, nó sẽ giúp. Hệ thống lớn mà tôi đã đề cập làm việc trong nhiều năm dựa trên thiết kế chỉ mục có IDENTITYtrường là trường dẫn đầu vì nó có nhiều vấn đề đánh hơi tham số chọn lọc và giảm tham số. Tuy nhiên, khi chúng tôi cần hoạt động dựa trên một phần tốt dữ liệu của một Người thuê nhà cụ thể, hiệu suất đã không theo kịp. Trên thực tế, một dự án di chuyển tất cả dữ liệu vào cơ sở dữ liệu mới đã phải tạm dừng vì các bộ điều khiển SAN đã đạt tối đa về mặt thông lượng. Cách khắc phục là thêm các Chỉ mục không được nhóm vào tất cả các bảng dữ liệu của người thuê thành (TenantID). Không cần phải làm (TenantID, ID) vì ID đã có trong Chỉ mục cụm, do đó, cấu trúc bên trong của Chỉ mục không phân cụm là tự nhiên (TenantID, ID).

    Mặc dù điều này đã giải quyết được vấn đề tức thời là có thể thực hiện các truy vấn dựa trên TenantID hiệu quả hơn nhiều, nhưng chúng vẫn không hiệu quả như chúng có thể nếu đó là Chỉ số cụm theo thứ tự đó. Và, bây giờ chúng tôi vẫn chưa thêm một chỉ mục trên mỗi bảng. Điều đó làm tăng dung lượng SAN chúng tôi đang sử dụng, tăng kích thước của các bản sao lưu của chúng tôi, khiến các bản sao lưu mất nhiều thời gian hơn để hoàn thành, tăng khả năng chặn và bế tắc, giảm hiệu suất INSERTDELETEhoạt động, v.v.

    VÀ chúng tôi vẫn còn thiếu hiệu quả chung khi dữ liệu của Người thuê được trải rộng trên nhiều trang dữ liệu, được trộn lẫn với nhiều dữ liệu của Người thuê khác. Như tôi đã đề cập ở trên, điều này làm tăng số lượng tranh chấp trên các trang này và nó lấp đầy Vùng đệm với rất nhiều trang dữ liệu có 1 hoặc 2 hàng hữu ích trong đó, đặc biệt là khi một số hàng trên các trang đó dành cho khách hàng đã không hoạt động nhưng chưa được thu gom rác. Có rất ít tiềm năng để sử dụng lại các trang dữ liệu trong Vùng đệm trong phương pháp này, vì vậy Tuổi thọ trang của chúng tôi khá thấp. Và điều đó có nghĩa là có nhiều thời gian hơn để quay lại đĩa để tải nhiều trang hơn.


2
Bạn đã xem xét hoặc thử nghiệm TỐI ƯU HÓA CHO UNKNOWN trong không gian vấn đề này chưa? Chỉ tò mò thôi.
RLF

1
@RLF Vâng, chúng tôi đã nghiên cứu tùy chọn đó và ít nhất là không tốt hơn và có thể tệ hơn so với hiệu suất tối ưu mà chúng tôi nhận được từ việc có trường IDENTITY trước tiên. Tôi không nhớ là tôi đã đọc cái này ở đâu, nhưng nó được cho là có cùng số liệu thống kê "trung bình" khi gán lại một thông số đầu vào cho một biến cục bộ. Nhưng bài viết này đi vào lý do tại sao tùy chọn đó không thực sự giải quyết được vấn đề: brentozar.com/archive/2013/06/ Từ Đọc các bình luận, Daniel Pepermans đã đi đến một kết luận tương tự: SQL động với biến thể hạn chế :)
Solomon Rutzky

3
Điều gì xảy ra nếu chỉ mục được nhóm bật (ID, TenantID)và bạn cũng tạo một chỉ mục không được nhóm (TenantID, ID)hoặc chỉ đơn giản là (TenantID)có số liệu thống kê chính xác cho các truy vấn xử lý hầu hết các hàng của một người thuê?
Vladimir Baranov

1
@VladimirBaranov Câu hỏi tuyệt vời. Tôi đã giải quyết nó trong phần CẬP NHẬT mới vào cuối câu trả lời :-).
Solomon Rutzky

4
điểm hay về sql năng động để tạo kế hoạch cho mỗi khách hàng.
Max Vernon
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.