Tôi đã thực hiện một cách tiếp cận hơi khác, chủ yếu để xem kỹ thuật này sẽ so sánh với các phương pháp khác như thế nào, bởi vì có các lựa chọn là tốt, phải không?
Thử nghiệm
Tại sao chúng ta không bắt đầu bằng cách chỉ nhìn vào cách các phương thức khác nhau xếp chồng lên nhau. Tôi đã làm ba bộ kiểm tra:
- Bộ đầu tiên chạy không có sửa đổi DB
- Bộ thứ hai chạy sau khi một chỉ mục được tạo để hỗ trợ
TransactionDate
các truy vấn dựa trên Production.TransactionHistory
.
- Bộ thứ ba đưa ra một giả định hơi khác nhau. Vì cả ba bài kiểm tra đều chạy cùng một danh sách Sản phẩm, điều gì sẽ xảy ra nếu chúng tôi lưu vào danh sách đó? Phương thức của tôi sử dụng bộ đệm trong bộ nhớ trong khi các phương thức khác sử dụng bảng tạm thời tương đương. Chỉ số hỗ trợ được tạo cho bộ thử nghiệm thứ hai vẫn tồn tại cho bộ thử nghiệm này.
Chi tiết kiểm tra bổ sung:
- Các thử nghiệm đã được chạy
AdventureWorks2012
trên SQL Server 2012, SP2 (Phiên bản dành cho nhà phát triển).
- Đối với mỗi bài kiểm tra, tôi dán nhãn câu trả lời mà tôi đã lấy câu hỏi và câu hỏi cụ thể.
- Tôi đã sử dụng tùy chọn "Hủy kết quả sau khi thực hiện" của Tùy chọn truy vấn | Các kết quả.
- Xin lưu ý rằng trong hai bộ thử nghiệm đầu tiên,
RowCounts
dường như là "tắt" cho phương pháp của tôi. Điều này là do phương pháp của tôi là triển khai thủ công những gì CROSS APPLY
đang thực hiện: nó chạy truy vấn ban đầu Production.Product
và nhận lại 161 hàng, sau đó nó sử dụng cho các truy vấn Production.TransactionHistory
. Do đó, các RowCount
giá trị cho các mục của tôi luôn cao hơn 161 so với các mục khác. Trong bộ thử nghiệm thứ ba (có bộ đệm), số lượng hàng là như nhau cho tất cả các phương thức.
- Tôi đã sử dụng SQL Server Profiler để nắm bắt các số liệu thống kê thay vì dựa vào các kế hoạch thực hiện. Aaron và Mikael đã làm một công việc tuyệt vời cho thấy các kế hoạch cho các truy vấn của họ và không cần phải sao chép thông tin đó. Và mục đích của phương pháp của tôi là giảm các truy vấn thành một hình thức đơn giản đến mức nó không thực sự quan trọng. Có một lý do bổ sung cho việc sử dụng Profiler, nhưng điều đó sẽ được đề cập sau.
- Thay vì sử dụng
Name >= N'M' AND Name < N'S'
cấu trúc, tôi đã chọn sử dụng Name LIKE N'[M-R]%'
và SQL Server xử lý chúng giống nhau.
Kết quả
Không có chỉ số hỗ trợ
Đây thực chất là AdventureWorks2012. Trong mọi trường hợp, phương pháp của tôi rõ ràng tốt hơn một số phương pháp khác, nhưng không bao giờ tốt như phương pháp 1 hoặc 2 hàng đầu.
Bài kiểm tra 1
Aaron của CTE rõ ràng là người chiến thắng ở đây.
Thử nghiệm 2
CT của Aaron (một lần nữa) và apply row_number()
phương pháp thứ hai của Mikael là một giây thứ hai.
Thử nghiệm 3
Aaron của CTE (một lần nữa) là người chiến thắng.
Kết luận
Khi không có chỉ số hỗ trợ trên TransactionDate
, phương pháp của tôi tốt hơn so với thực hiện một tiêu chuẩn CROSS APPLY
, nhưng vẫn sử dụng phương pháp CTE rõ ràng là cách tốt nhất.
Với chỉ số hỗ trợ (không có bộ đệm)
Đối với tập kiểm tra này, tôi đã thêm chỉ mục rõ ràng vào TransactionHistory.TransactionDate
vì tất cả các truy vấn sắp xếp trên trường đó. Tôi nói "rõ ràng" vì hầu hết các câu trả lời khác cũng đồng ý về điểm này. Và vì tất cả các truy vấn đều muốn những ngày gần đây nhất, TransactionDate
nên trường được đặt hàng DESC
, vì vậy tôi chỉ cần lấy CREATE INDEX
câu lệnh ở cuối câu trả lời của Mikael và thêm một câu rõ ràng FILLFACTOR
:
CREATE INDEX [IX_TransactionHistoryX]
ON Production.TransactionHistory (ProductID ASC, TransactionDate DESC)
WITH (FILLFACTOR = 100);
Một khi chỉ số này được đưa ra, kết quả thay đổi khá nhiều.
Bài kiểm tra 1
Lần này là phương pháp của tôi được đưa ra phía trước, ít nhất là về mặt Đọc hợp lý. Các CROSS APPLY
phương pháp, trước đây người biểu diễn tồi tệ nhất cho thử nghiệm 1, thắng trên Thời gian và thậm chí đánh bại phương pháp CTE trên logic Reads.
Bài kiểm tra 2
Lần này, đây là apply row_number()
phương pháp đầu tiên của Mikael là người chiến thắng khi xem Reads, trong khi trước đó nó là một trong những bài biểu diễn tệ nhất. Và bây giờ phương pháp của tôi xuất hiện ở vị trí thứ hai rất gần khi nhìn vào Đọc. Trên thực tế, bên ngoài phương pháp CTE, phần còn lại đều khá gần về mặt Đọc.
Thử nghiệm 3
Ở đây, CTE vẫn là người chiến thắng, nhưng bây giờ sự khác biệt giữa các phương pháp khác hầu như không đáng chú ý so với sự khác biệt lớn đã tồn tại trước khi tạo chỉ số.
Kết luận
Khả năng áp dụng phương pháp của tôi hiện rõ ràng hơn, mặc dù nó ít khả năng phục hồi hơn khi không có chỉ số thích hợp.
Với chỉ số hỗ trợ và bộ nhớ đệm
Đối với tập kiểm tra này, tôi đã sử dụng bộ nhớ đệm bởi vì, tại sao không? Phương thức của tôi cho phép sử dụng bộ nhớ đệm trong bộ nhớ mà các phương thức khác không thể truy cập. Vì vậy, để công bằng, tôi đã tạo bảng tạm thời sau đây được sử dụng thay Product.Product
cho tất cả các tham chiếu trong các phương thức khác đó trong cả ba thử nghiệm. Trường DaysToManufacture
chỉ được sử dụng trong Bài kiểm tra số 2, nhưng việc thống nhất các tập lệnh SQL để sử dụng cùng một bảng sẽ dễ dàng hơn và không có gì đau đớn khi có nó ở đó.
CREATE TABLE #Products
(
ProductID INT NOT NULL PRIMARY KEY,
Name NVARCHAR(50) NOT NULL,
DaysToManufacture INT NOT NULL
);
INSERT INTO #Products (ProductID, Name, DaysToManufacture)
SELECT p.ProductID, p.Name, p.DaysToManufacture
FROM Production.Product p
WHERE p.Name >= N'M' AND p.Name < N'S'
AND EXISTS (
SELECT *
FROM Production.TransactionHistory th
WHERE th.ProductID = p.ProductID
);
ALTER TABLE #Products REBUILD WITH (FILLFACTOR = 100);
Kiểm tra 1
Tất cả các phương thức dường như được hưởng lợi như nhau từ bộ nhớ đệm và phương pháp của tôi vẫn được đưa ra phía trước.
Thử nghiệm 2
Ở đây bây giờ chúng ta thấy một sự khác biệt trong đội hình khi phương pháp của tôi xuất hiện trước mắt, chỉ có 2 Đọc tốt hơn apply row_number()
phương pháp đầu tiên của Mikael , trong khi không có bộ nhớ đệm thì phương thức của tôi bị chậm hơn 4 lần đọc.
Kiểm tra 3
Vui lòng xem cập nhật về phía dưới (bên dưới dòng) . Ở đây chúng tôi một lần nữa thấy một số khác biệt. Hương vị "được tham số hóa" của phương pháp của tôi bây giờ hầu như không bị dẫn trước bởi 2 lần đọc so với phương pháp CROSS ỨNG DỤNG của Aaron (không có bộ nhớ đệm mà chúng bằng nhau). Nhưng điều thực sự kỳ lạ là lần đầu tiên chúng ta thấy một phương pháp bị ảnh hưởng tiêu cực bởi bộ nhớ đệm: phương pháp CTE của Aaron (trước đây là phương pháp tốt nhất cho Bài kiểm tra số 3). Tuy nhiên, tôi sẽ không nhận tín dụng khi chưa đến hạn và vì không có phương thức CTE của bộ nhớ đệm vẫn nhanh hơn phương pháp của tôi ở đây với bộ nhớ đệm, cách tiếp cận tốt nhất cho tình huống cụ thể này dường như là phương pháp CTE của Aaron.
Kết luận Vui lòng xem cập nhật về phía dưới (bên dưới dòng) Các
tình huống sử dụng nhiều lần kết quả của truy vấn thứ cấp thường có thể (nhưng không phải luôn luôn) được hưởng lợi từ việc lưu trữ các kết quả đó. Nhưng khi bộ nhớ đệm là một lợi ích, sử dụng bộ nhớ cho bộ nhớ đệm có một số lợi thế so với sử dụng các bảng tạm thời.
Phương pháp
Nói chung là
Tôi đã tách truy vấn "tiêu đề" (nghĩa là lấy ProductID
s và trong một trường hợp cũng là DaysToManufacture
, dựa trên Name
bắt đầu bằng một số chữ cái) từ các truy vấn "chi tiết" (tức là lấy TransactionID
s và TransactionDate
s). Khái niệm này là để thực hiện các truy vấn rất đơn giản và không cho phép trình tối ưu hóa bị lẫn lộn khi THAM GIA chúng. Rõ ràng điều này không phải lúc nào cũng thuận lợi vì nó cũng không cho phép trình tối ưu hóa, tốt, tối ưu hóa. Nhưng như chúng ta đã thấy trong các kết quả, tùy thuộc vào loại truy vấn, phương pháp này có giá trị của nó.
Sự khác biệt giữa các hương vị khác nhau của phương pháp này là:
Hằng số: Gửi bất kỳ giá trị thay thế nào dưới dạng hằng nội tuyến thay vì là tham số. Điều này sẽ đề cập đến ProductID
trong cả ba thử nghiệm và số lượng hàng sẽ trả về trong Thử nghiệm 2 vì đó là chức năng của "năm lần DaysToManufacture
thuộc tính Sản phẩm". Phương thức phụ này có nghĩa là mỗi phương thức ProductID
sẽ có kế hoạch thực hiện riêng, có thể có lợi nếu có sự khác biệt lớn trong phân phối dữ liệu cho ProductID
. Nhưng nếu có ít sự thay đổi trong phân phối dữ liệu, chi phí tạo ra các gói bổ sung có thể sẽ không đáng giá.
Parameterized: Gửi ít nhất ProductID
là @ProductID
, cho phép sử dụng bộ nhớ đệm và sử dụng lại kế hoạch thực hiện. Có một tùy chọn kiểm tra bổ sung để xử lý số lượng hàng thay đổi để trả về Kiểm tra 2 làm tham số.
Tối ưu hóa không xác định: Khi tham chiếu ProductID
dưới dạng @ProductID
, nếu có sự thay đổi lớn về phân phối dữ liệu thì có thể lưu trữ một gói có ảnh hưởng tiêu cực đến các ProductID
giá trị khác, vì vậy sẽ rất tốt nếu biết sử dụng Truy vấn Gợi ý này có giúp ích gì không.
Các sản phẩm bộ nhớ cache: Thay vì truy vấn Production.Product
bảng mỗi lần, chỉ để có cùng một danh sách, hãy chạy truy vấn một lần (và trong khi chúng tôi đang ở đó, hãy lọc bất kỳ ProductID
s nào thậm chí không có trong TransactionHistory
bảng để chúng tôi không lãng phí tài nguyên ở đó) và bộ nhớ cache danh sách đó. Danh sách nên bao gồm các DaysToManufacture
lĩnh vực. Sử dụng tùy chọn này, có một lần truy cập ban đầu cao hơn một chút vào Logical Reads cho lần thực hiện đầu tiên, nhưng sau đó chỉ là TransactionHistory
bảng được truy vấn.
Đặc biệt
Ok, nhưng vì vậy, ừm, làm thế nào có thể đưa ra tất cả các truy vấn phụ dưới dạng các truy vấn riêng biệt mà không sử dụng HIỆN TẠI và bỏ từng kết quả được đặt thành một bảng tạm thời hoặc biến bảng? Rõ ràng thực hiện phương pháp CURSOR / Temp Table sẽ phản ánh khá rõ ràng trong phần Đọc và Viết. Vâng, bằng cách sử dụng SQLCLR :). Bằng cách tạo một thủ tục được lưu trữ SQLCLR, tôi có thể mở một tập kết quả và về cơ bản truyền kết quả của từng truy vấn phụ đến nó, như một tập kết quả liên tục (và không phải là nhiều tập kết quả). Bên ngoài của Thông tin sản phẩm (ví dụ ProductID
, Name
vàDaysToManufacture
), không có kết quả truy vấn phụ nào phải được lưu trữ ở bất kỳ đâu (bộ nhớ hoặc đĩa) và được chuyển qua dưới dạng tập kết quả chính của thủ tục được lưu trữ SQLCLR. Điều này cho phép tôi thực hiện một truy vấn đơn giản để lấy thông tin Sản phẩm và sau đó duyệt qua nó, đưa ra các truy vấn rất đơn giản TransactionHistory
.
Và, đây là lý do tại sao tôi phải sử dụng SQL Server Profiler để thu thập số liệu thống kê. Quy trình được lưu trữ SQLCLR không trả về kế hoạch thực hiện, bằng cách đặt Tùy chọn truy vấn "Bao gồm kế hoạch thực hiện thực tế" hoặc bằng cách ban hành SET STATISTICS XML ON;
.
Đối với bộ đệm Thông tin sản phẩm, tôi đã sử dụng readonly static
Danh sách chung (nghĩa là _GlobalProducts
trong mã bên dưới). Dường như thêm vào bộ sưu tập không vi phạm các readonly
tùy chọn, do đó mã này hoạt động khi lắp ráp có PERMISSON_SET
của SAFE
:), ngay cả khi đó là phản trực giác.
Các câu hỏi được tạo
Các truy vấn được tạo bởi thủ tục lưu trữ SQLCLR này như sau:
Thông tin sản phẩm
Số kiểm tra 1 và 3 (không có bộ đệm)
SELECT prod1.ProductID, prod1.Name, 1 AS [DaysToManufacture]
FROM Production.Product prod1
WHERE prod1.Name LIKE N'[M-R]%';
Kiểm tra số 2 (không lưu đệm)
;WITH cte AS
(
SELECT prod1.ProductID
FROM Production.Product prod1 WITH (INDEX(AK_Product_Name))
WHERE prod1.Name LIKE N'[M-R]%'
)
SELECT prod2.ProductID, prod2.Name, prod2.DaysToManufacture
FROM Production.Product prod2
INNER JOIN cte
ON cte.ProductID = prod2.ProductID;
Số kiểm tra 1, 2 và 3 (Bộ nhớ đệm)
;WITH cte AS
(
SELECT prod1.ProductID
FROM Production.Product prod1 WITH (INDEX(AK_Product_Name))
WHERE prod1.Name LIKE N'[M-R]%'
AND EXISTS (
SELECT *
FROM Production.TransactionHistory th
WHERE th.ProductID = prod1.ProductID
)
)
SELECT prod2.ProductID, prod2.Name, prod2.DaysToManufacture
FROM Production.Product prod2
INNER JOIN cte
ON cte.ProductID = prod2.ProductID;
Thông tin giao dịch
Số kiểm tra 1 và 2 (Hằng số)
SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = 977
ORDER BY th.TransactionDate DESC;
Số kiểm tra 1 và 2 (Tham số hóa)
SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
;
Số kiểm tra 1 và 2 (Tham số hóa + TỐI ƯU HÓA)
SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
OPTION (OPTIMIZE FOR (@ProductID UNKNOWN));
Kiểm tra số 2 (Tham số hóa cả hai)
SELECT TOP (@RowsToReturn) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
;
Kiểm tra số 2 (Tham số hóa cả hai + TỐI ƯU HÓA KHÔNG GIỚI HẠN)
SELECT TOP (@RowsToReturn) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
OPTION (OPTIMIZE FOR (@ProductID UNKNOWN));
Bài kiểm tra số 3 (Hằng số)
SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = 977
ORDER BY th.TransactionDate DESC, th.TransactionID DESC;
Bài kiểm tra số 3 (Tham số)
SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC, th.TransactionID DESC
;
Bài kiểm tra số 3 (Tham số hóa + TỐI ƯU HÓA)
SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC, th.TransactionID DESC
OPTION (OPTIMIZE FOR (@ProductID UNKNOWN));
Mật mã
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
public class ObligatoryClassName
{
private class ProductInfo
{
public int ProductID;
public string Name;
public int DaysToManufacture;
public ProductInfo(int ProductID, string Name, int DaysToManufacture)
{
this.ProductID = ProductID;
this.Name = Name;
this.DaysToManufacture = DaysToManufacture;
return;
}
}
private static readonly List<ProductInfo> _GlobalProducts = new List<ProductInfo>();
private static void PopulateGlobalProducts(SqlBoolean PrintQuery)
{
if (_GlobalProducts.Count > 0)
{
if (PrintQuery.IsTrue)
{
SqlContext.Pipe.Send(String.Concat("I already haz ", _GlobalProducts.Count,
" entries :)"));
}
return;
}
SqlConnection _Connection = new SqlConnection("Context Connection = true;");
SqlCommand _Command = new SqlCommand();
_Command.CommandType = CommandType.Text;
_Command.Connection = _Connection;
_Command.CommandText = @"
;WITH cte AS
(
SELECT prod1.ProductID
FROM Production.Product prod1 WITH (INDEX(AK_Product_Name))
WHERE prod1.Name LIKE N'[M-R]%'
AND EXISTS (
SELECT *
FROM Production.TransactionHistory th
WHERE th.ProductID = prod1.ProductID
)
)
SELECT prod2.ProductID, prod2.Name, prod2.DaysToManufacture
FROM Production.Product prod2
INNER JOIN cte
ON cte.ProductID = prod2.ProductID;
";
SqlDataReader _Reader = null;
try
{
_Connection.Open();
_Reader = _Command.ExecuteReader();
while (_Reader.Read())
{
_GlobalProducts.Add(new ProductInfo(_Reader.GetInt32(0), _Reader.GetString(1),
_Reader.GetInt32(2)));
}
}
catch
{
throw;
}
finally
{
if (_Reader != null && !_Reader.IsClosed)
{
_Reader.Close();
}
if (_Connection != null && _Connection.State != ConnectionState.Closed)
{
_Connection.Close();
}
if (PrintQuery.IsTrue)
{
SqlContext.Pipe.Send(_Command.CommandText);
}
}
return;
}
[Microsoft.SqlServer.Server.SqlProcedure]
public static void GetTopRowsPerGroup(SqlByte TestNumber,
SqlByte ParameterizeProductID, SqlBoolean OptimizeForUnknown,
SqlBoolean UseSequentialAccess, SqlBoolean CacheProducts, SqlBoolean PrintQueries)
{
SqlConnection _Connection = new SqlConnection("Context Connection = true;");
SqlCommand _Command = new SqlCommand();
_Command.CommandType = CommandType.Text;
_Command.Connection = _Connection;
List<ProductInfo> _Products = null;
SqlDataReader _Reader = null;
int _RowsToGet = 5; // default value is for Test Number 1
string _OrderByTransactionID = "";
string _OptimizeForUnknown = "";
CommandBehavior _CmdBehavior = CommandBehavior.Default;
if (OptimizeForUnknown.IsTrue)
{
_OptimizeForUnknown = "OPTION (OPTIMIZE FOR (@ProductID UNKNOWN))";
}
if (UseSequentialAccess.IsTrue)
{
_CmdBehavior = CommandBehavior.SequentialAccess;
}
if (CacheProducts.IsTrue)
{
PopulateGlobalProducts(PrintQueries);
}
else
{
_Products = new List<ProductInfo>();
}
if (TestNumber.Value == 2)
{
_Command.CommandText = @"
;WITH cte AS
(
SELECT prod1.ProductID
FROM Production.Product prod1 WITH (INDEX(AK_Product_Name))
WHERE prod1.Name LIKE N'[M-R]%'
)
SELECT prod2.ProductID, prod2.Name, prod2.DaysToManufacture
FROM Production.Product prod2
INNER JOIN cte
ON cte.ProductID = prod2.ProductID;
";
}
else
{
_Command.CommandText = @"
SELECT prod1.ProductID, prod1.Name, 1 AS [DaysToManufacture]
FROM Production.Product prod1
WHERE prod1.Name LIKE N'[M-R]%';
";
if (TestNumber.Value == 3)
{
_RowsToGet = 1;
_OrderByTransactionID = ", th.TransactionID DESC";
}
}
try
{
_Connection.Open();
// Populate Product list for this run if not using the Product Cache
if (!CacheProducts.IsTrue)
{
_Reader = _Command.ExecuteReader(_CmdBehavior);
while (_Reader.Read())
{
_Products.Add(new ProductInfo(_Reader.GetInt32(0), _Reader.GetString(1),
_Reader.GetInt32(2)));
}
_Reader.Close();
if (PrintQueries.IsTrue)
{
SqlContext.Pipe.Send(_Command.CommandText);
}
}
else
{
_Products = _GlobalProducts;
}
SqlDataRecord _ResultRow = new SqlDataRecord(
new SqlMetaData[]{
new SqlMetaData("ProductID", SqlDbType.Int),
new SqlMetaData("Name", SqlDbType.NVarChar, 50),
new SqlMetaData("TransactionID", SqlDbType.Int),
new SqlMetaData("TransactionDate", SqlDbType.DateTime)
});
SqlParameter _ProductID = new SqlParameter("@ProductID", SqlDbType.Int);
_Command.Parameters.Add(_ProductID);
SqlParameter _RowsToReturn = new SqlParameter("@RowsToReturn", SqlDbType.Int);
_Command.Parameters.Add(_RowsToReturn);
SqlContext.Pipe.SendResultsStart(_ResultRow);
for (int _Row = 0; _Row < _Products.Count; _Row++)
{
// Tests 1 and 3 use previously set static values for _RowsToGet
if (TestNumber.Value == 2)
{
if (_Products[_Row].DaysToManufacture == 0)
{
continue; // no use in issuing SELECT TOP (0) query
}
_RowsToGet = (5 * _Products[_Row].DaysToManufacture);
}
_ResultRow.SetInt32(0, _Products[_Row].ProductID);
_ResultRow.SetString(1, _Products[_Row].Name);
switch (ParameterizeProductID.Value)
{
case 0x01:
_Command.CommandText = String.Format(@"
SELECT TOP ({0}) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC{2}
{1};
", _RowsToGet, _OptimizeForUnknown, _OrderByTransactionID);
_ProductID.Value = _Products[_Row].ProductID;
break;
case 0x02:
_Command.CommandText = String.Format(@"
SELECT TOP (@RowsToReturn) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
{0};
", _OptimizeForUnknown);
_ProductID.Value = _Products[_Row].ProductID;
_RowsToReturn.Value = _RowsToGet;
break;
default:
_Command.CommandText = String.Format(@"
SELECT TOP ({0}) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = {1}
ORDER BY th.TransactionDate DESC{2};
", _RowsToGet, _Products[_Row].ProductID, _OrderByTransactionID);
break;
}
_Reader = _Command.ExecuteReader(_CmdBehavior);
while (_Reader.Read())
{
_ResultRow.SetInt32(2, _Reader.GetInt32(0));
_ResultRow.SetDateTime(3, _Reader.GetDateTime(1));
SqlContext.Pipe.SendResultsRow(_ResultRow);
}
_Reader.Close();
}
}
catch
{
throw;
}
finally
{
if (SqlContext.Pipe.IsSendingResults)
{
SqlContext.Pipe.SendResultsEnd();
}
if (_Reader != null && !_Reader.IsClosed)
{
_Reader.Close();
}
if (_Connection != null && _Connection.State != ConnectionState.Closed)
{
_Connection.Close();
}
if (PrintQueries.IsTrue)
{
SqlContext.Pipe.Send(_Command.CommandText);
}
}
}
}
Các câu hỏi kiểm tra
Không có đủ chỗ để đăng các bài kiểm tra ở đây vì vậy tôi sẽ tìm một địa điểm khác.
Kết luận
Đối với các kịch bản nhất định, SQLCLR có thể được sử dụng để thao tác các khía cạnh nhất định của các truy vấn không thể thực hiện được trong T-SQL. Và có khả năng sử dụng bộ nhớ để lưu vào bộ nhớ cache thay vì các bảng tạm thời, mặc dù điều đó nên được thực hiện một cách tiết kiệm và cẩn thận vì bộ nhớ không được tự động giải phóng trở lại hệ thống. Phương pháp này cũng không phải là thứ sẽ giúp truy vấn ad hoc, mặc dù có thể làm cho nó linh hoạt hơn tôi đã chỉ ra ở đây chỉ bằng cách thêm tham số để điều chỉnh nhiều khía cạnh của các truy vấn đang được thực hiện.
CẬP NHẬT
Thử nghiệm bổ sung Các thử nghiệm
ban đầu của tôi bao gồm một chỉ mục hỗ trợ về TransactionHistory
định nghĩa được sử dụng sau đây:
ProductID ASC, TransactionDate DESC
Lúc đó tôi đã quyết định từ bỏ kể cả TransactionId DESC
cuối cùng, hình dung rằng trong khi nó có thể giúp Kiểm tra Số 3 (trong đó chỉ định phá vỡ liên kết trên - gần đây nhất TransactionId
, "gần đây nhất" được giả định vì không được nêu rõ ràng, nhưng mọi người dường như đồng ý với giả định này), có khả năng sẽ không có đủ mối quan hệ để tạo ra sự khác biệt.
Nhưng, sau đó Aaron đã kiểm tra lại với một chỉ số hỗ trợ bao gồm TransactionId DESC
và thấy rằng CROSS APPLY
phương pháp này là người chiến thắng trong cả ba bài kiểm tra. Điều này khác với thử nghiệm của tôi chỉ ra rằng phương pháp CTE là tốt nhất cho Thử nghiệm số 3 (khi không sử dụng bộ nhớ đệm, phản ánh thử nghiệm của Aaron). Rõ ràng là có một biến thể bổ sung cần được thử nghiệm.
Tôi đã xóa chỉ mục hỗ trợ hiện tại, tạo một chỉ mục mới với TransactionId
và xóa bộ nhớ cache của kế hoạch (chỉ để chắc chắn):
DROP INDEX [IX_TransactionHistoryX] ON Production.TransactionHistory;
CREATE UNIQUE INDEX [UIX_TransactionHistoryX]
ON Production.TransactionHistory (ProductID ASC, TransactionDate DESC, TransactionID DESC)
WITH (FILLFACTOR = 100);
DBCC FREEPROCCACHE WITH NO_INFOMSGS;
Tôi chạy lại Bài kiểm tra số 1 và kết quả vẫn như mong đợi. Sau đó tôi chạy lại Bài kiểm tra số 3 và kết quả thực sự đã thay đổi:
Các kết quả trên dành cho thử nghiệm tiêu chuẩn, không lưu trữ. Lần này, không chỉ CROSS APPLY
đánh bại CTE (giống như thử nghiệm của Aaron đã chỉ ra), mà SQLCLR đã dẫn đầu trong 30 lần đọc (woo hoo).
Các kết quả trên dành cho thử nghiệm với kích hoạt bộ đệm. Lần này hiệu suất của CTE không bị suy giảm, mặc dù vậy CROSS APPLY
vẫn đánh bại nó. Tuy nhiên, bây giờ, SQLCLR Proc dẫn đầu bởi 23 lần đọc (woo hoo, một lần nữa).
Đi đường
Có nhiều lựa chọn để sử dụng. Tốt nhất là thử một vài cái vì chúng đều có điểm mạnh. Các thử nghiệm được thực hiện ở đây cho thấy sự chênh lệch khá nhỏ về cả Đọc và Thời lượng giữa những người thực hiện tốt nhất và kém nhất trong tất cả các thử nghiệm (với chỉ số hỗ trợ); biến thể trong Đọc là khoảng 350 và Thời lượng là 55 ms. Mặc dù Proc của SQLCLR đã giành chiến thắng trong tất cả trừ 1 bài kiểm tra (về số lần đọc), nhưng chỉ lưu một số lần đọc thường không đáng với chi phí bảo trì khi đi theo lộ trình SQLCLR. Nhưng trong AdventureWorks2012, Product
bảng chỉ có 504 hàng và TransactionHistory
chỉ có 113.443 hàng. Sự khác biệt hiệu suất giữa các phương thức này có thể trở nên rõ rệt hơn khi số lượng hàng tăng lên.
Mặc dù câu hỏi này là cụ thể để có được một tập hợp các hàng cụ thể, nhưng không nên bỏ qua rằng yếu tố lớn nhất trong hiệu suất là lập chỉ mục và không phải là SQL cụ thể. Một chỉ số tốt cần được đưa ra trước khi xác định phương pháp nào thực sự tốt nhất.
Bài học quan trọng nhất được tìm thấy ở đây không phải là về CROSS ỨNG DỤNG vs CTE vs SQLCLR: đó là về KIỂM TRA. Đừng giả sử. Lấy ý tưởng từ nhiều người và thử nghiệm càng nhiều kịch bản càng tốt.