Lấy n hàng trên mỗi nhóm


88

Tôi thường cần chọn một số hàng từ mỗi nhóm trong tập kết quả.

Ví dụ: tôi có thể muốn liệt kê các giá trị đơn hàng gần đây cao nhất hoặc thấp nhất cho mỗi khách hàng.

Trong các trường hợp phức tạp hơn, số lượng hàng để liệt kê có thể khác nhau theo từng nhóm (được xác định bởi một thuộc tính của bản ghi nhóm / bản ghi cha). Phần này chắc chắn là tùy chọn / cho tín dụng bổ sung và không có ý định can ngăn mọi người trả lời.

Các tùy chọn chính để giải quyết các loại vấn đề này trong SQL Server 2005 trở lên là gì? Những ưu điểm và nhược điểm chính của từng phương pháp là gì?

Ví dụ AdventureWorks (cho rõ ràng, tùy chọn)

  1. Liệt kê năm ngày giao dịch gần đây nhất và ID từ TransactionHistorybảng, cho mỗi sản phẩm bắt đầu bằng một chữ cái từ M đến R.
  2. Tương tự một lần nữa, nhưng với ncác dòng lịch sử trên mỗi sản phẩm, trong đó nnăm lần DaysToManufacturethuộc tính Sản phẩm.
  3. Tương tự, đối với trường hợp đặc biệt yêu cầu chính xác một dòng lịch sử cho mỗi sản phẩm (mục nhập gần đây nhất bằng cách TransactionDatekết hợp TransactionID.

Câu trả lời:


70

Hãy bắt đầu với kịch bản cơ bản.

Nếu tôi muốn lấy một số hàng ra khỏi bảng, tôi có hai tùy chọn chính: hàm xếp hạng; hoặc TOP.

Trước tiên, hãy xem xét toàn bộ từ Production.TransactionHistorycụ thể ProductID:

SELECT h.TransactionID, h.ProductID, h.TransactionDate
FROM Production.TransactionHistory h
WHERE h.ProductID = 800;

Điều này trả về 418 hàng và kế hoạch cho thấy nó kiểm tra mọi hàng trong bảng đang tìm kiếm điều này - Quét chỉ mục cụm không giới hạn, với một Vị ngữ để cung cấp bộ lọc. 797 đọc ở đây, đó là xấu xí.

Quét đắt với Dự đoán 'dư'

Vì vậy, hãy công bằng với nó và tạo ra một chỉ mục sẽ hữu ích hơn. Các điều kiện của chúng tôi yêu cầu một trận đấu bình đẳng trên ProductID, tiếp theo là tìm kiếm gần đây nhất bởi TransactionDate. Chúng tôi cũng cần TransactionIDtrả lại, vì vậy hãy đi với : CREATE INDEX ix_FindingMostRecent ON Production.TransactionHistory (ProductID, TransactionDate) INCLUDE (TransactionID);.

Thực hiện điều này, kế hoạch của chúng tôi thay đổi đáng kể và giảm số lần đọc xuống chỉ còn 3. Vì vậy, chúng tôi đã cải thiện mọi thứ hơn 250 lần hoặc hơn ...

Kế hoạch cải tiến

Bây giờ chúng ta đã san bằng sân chơi, hãy xem các tùy chọn hàng đầu - chức năng xếp hạng và TOP.

WITH Numbered AS
(
SELECT h.TransactionID, h.ProductID, h.TransactionDate, ROW_NUMBER() OVER (ORDER BY TransactionDate DESC) AS RowNum
FROM Production.TransactionHistory h
WHERE h.ProductID = 800
)
SELECT TransactionID, ProductID, TransactionDate
FROM Numbered
WHERE RowNum <= 5;

SELECT TOP (5) h.TransactionID, h.ProductID, h.TransactionDate
FROM Production.TransactionHistory h
WHERE h.ProductID = 800
ORDER BY TransactionDate DESC;

Hai kế hoạch - TOP \ RowNum cơ bản

Bạn sẽ nhận thấy rằng TOPtruy vấn thứ hai ( ) đơn giản hơn nhiều so với truy vấn thứ nhất, cả trong truy vấn và trong kế hoạch. Nhưng rất đáng kể, cả hai đều sử dụng TOPđể hạn chế số lượng hàng thực sự được rút ra khỏi chỉ mục. Các chi phí chỉ là ước tính và đáng để bỏ qua, nhưng bạn có thể thấy rất nhiều điểm tương đồng trong hai kế hoạch, với ROW_NUMBER()phiên bản thực hiện một số lượng nhỏ công việc bổ sung để gán số và lọc theo đó, và cả hai truy vấn kết thúc chỉ cần 2 lần đọc công việc của họ. Trình tối ưu hóa truy vấn chắc chắn nhận ra ý tưởng lọc trên một ROW_NUMBER()trường, nhận ra rằng nó có thể sử dụng toán tử hàng đầu để bỏ qua các hàng không cần thiết. Cả hai truy vấn này đều đủ tốt - TOPkhông tốt hơn nhiều đến mức đáng để thay đổi mã, nhưng nó đơn giản hơn và có thể rõ ràng hơn cho người mới bắt đầu.

Vì vậy, công việc này trên một sản phẩm duy nhất. Nhưng chúng ta cần xem xét những gì xảy ra nếu chúng ta cần làm điều này trên nhiều sản phẩm.

Lập trình viên lặp đi lặp lại sẽ xem xét ý tưởng lặp qua các sản phẩm quan tâm và gọi truy vấn này nhiều lần và chúng ta thực sự có thể thoát khỏi việc viết một truy vấn ở dạng này - không sử dụng con trỏ, mà sử dụng APPLY. Tôi đang sử dụng OUTER APPLY, hình dung rằng chúng tôi có thể muốn trả lại Sản phẩm bằng NULL, nếu không có Giao dịch nào cho sản phẩm đó.

SELECT p.Name, p.ProductID, t.TransactionID, t.TransactionDate
FROM 
Production.Product p
OUTER APPLY (
    SELECT TOP (5) h.TransactionID, h.ProductID, h.TransactionDate
    FROM Production.TransactionHistory h
    WHERE h.ProductID = p.ProductID
    ORDER BY TransactionDate DESC
) t
WHERE p.Name >= 'M' AND p.Name < 'S';

Kế hoạch cho việc này là phương pháp lập trình lặp - Nested Loop, thực hiện thao tác Top và Seek (2 lần đọc chúng tôi đã có trước đây) cho mỗi Sản phẩm. Điều này cho 4 lần đọc so với Sản phẩm và 360 so với Giao dịch.

Kế hoạch áp dụng

Sử dụng ROW_NUMBER(), phương pháp là sử dụng PARTITION BYtrong OVERmệnh đề, để chúng tôi khởi động lại việc đánh số cho mỗi Sản phẩm. Điều này sau đó có thể được lọc như trước đây. Kế hoạch kết thúc khá khác nhau. Số lần đọc logic thấp hơn khoảng 15% trên TransactionHistory, với Quét chỉ mục đầy đủ đang diễn ra để đưa ra các hàng.

WITH Numbered AS
(
SELECT p.Name, p.ProductID, h.TransactionID, h.TransactionDate, ROW_NUMBER() OVER (PARTITION BY h.ProductID ORDER BY h.TransactionDate DESC) AS RowNum
FROM Production.Product p
LEFT JOIN Production.TransactionHistory h ON h.ProductID = p.ProductID
WHERE p.Name >= 'M' AND p.Name < 'S'
)
SELECT Name, ProductID, TransactionID, TransactionDate
FROM Numbered n
WHERE RowNum <= 5;

Gói ROW_NUMBER

Mặc dù vậy, đáng kể, kế hoạch này có một toán tử Sắp xếp đắt tiền. Hợp nhất Tham gia dường như không duy trì thứ tự các hàng trong TransactionHistory, dữ liệu phải được sử dụng để có thể tìm thấy các chủ sở hữu. Nó ít đọc hơn, nhưng việc sắp xếp chặn này có thể cảm thấy đau đớn. Khi sử dụng APPLY, Nested Loop sẽ trả về các hàng đầu tiên rất nhanh, chỉ sau vài lần đọc, nhưng với Sắp xếp, ROW_NUMBER()sẽ chỉ trả về các hàng sau khi phần lớn công việc đã hoàn thành.

Thật thú vị, nếu ROW_NUMBER()truy vấn sử dụng INNER JOINthay vì LEFT JOIN, thì một kế hoạch khác sẽ xuất hiện.

ROW_NUMBER () với INNER THAM GIA

Kế hoạch này sử dụng một Nested Loop, giống như với APPLY. Nhưng không có nhà điều hành hàng đầu, do đó, nó kéo tất cả các giao dịch cho từng sản phẩm và sử dụng nhiều lần đọc hơn trước - 492 lần đọc so với TransactionHistory. Không có lý do chính đáng nào để nó không chọn tùy chọn Hợp nhất tham gia ở đây, vì vậy tôi đoán rằng kế hoạch được coi là "Đủ tốt". Tuy nhiên - nó không chặn, điều này thật tuyệt - chỉ là không đẹp bằng APPLY.

Các PARTITION BYcột mà tôi sử dụng cho ROW_NUMBER()h.ProductIDtrong cả hai trường hợp, bởi vì tôi đã muốn cung cấp cho các Qo tùy chọn của sản xuất giá trị ROWNUM trước khi gia nhập vào bảng Product. Nếu tôi sử dụng p.ProductID, chúng ta sẽ thấy kế hoạch hình dạng tương tự như với INNER JOINbiến thể.

WITH Numbered AS
(
SELECT p.Name, p.ProductID, h.TransactionID, h.TransactionDate, ROW_NUMBER() OVER (PARTITION BY p.ProductID ORDER BY h.TransactionDate DESC) AS RowNum
FROM Production.Product p
LEFT JOIN Production.TransactionHistory h ON h.ProductID = p.ProductID
WHERE p.Name >= 'M' AND p.Name < 'S'
)
SELECT Name, ProductID, TransactionID, TransactionDate
FROM Numbered n
WHERE RowNum <= 5;

Nhưng nhà điều hành Tham gia nói 'Tham gia bên ngoài bên trái' thay vì 'Tham gia bên trong'. Số lần đọc vẫn chỉ dưới 500 lần đọc so với bảng TransactionHistory.

THAM GIA B onNG trên p. ProducttID thay vì h. ProducttID

Dù sao đi nữa - trở lại câu hỏi trong tầm tay ...

Chúng tôi đã trả lời câu hỏi 1 , với hai tùy chọn mà bạn có thể chọn và chọn. Cá nhân, tôi thích các APPLYtùy chọn.

Để mở rộng điều này để sử dụng một số biến ( câu hỏi 2 ), 5chỉ cần thay đổi cho phù hợp. Ồ, và tôi đã thêm một chỉ mục khác, để có một chỉ mục trên Production.Product.Nameđó bao gồm DaysToManufacturecột.

WITH Numbered AS
(
SELECT p.Name, p.ProductID, p.DaysToManufacture, h.TransactionID, h.TransactionDate, ROW_NUMBER() OVER (PARTITION BY h.ProductID ORDER BY h.TransactionDate DESC) AS RowNum
FROM Production.Product p
LEFT JOIN Production.TransactionHistory h ON h.ProductID = p.ProductID
WHERE p.Name >= 'M' AND p.Name < 'S'
)
SELECT Name, ProductID, TransactionID, TransactionDate
FROM Numbered n
WHERE RowNum <= 5 * DaysToManufacture;

SELECT p.Name, p.ProductID, t.TransactionID, t.TransactionDate
FROM 
Production.Product p
OUTER APPLY (
    SELECT TOP (5 * p.DaysToManufacture) h.TransactionID, h.ProductID, h.TransactionDate
    FROM Production.TransactionHistory h
    WHERE h.ProductID = p.ProductID
    ORDER BY TransactionDate DESC
) t
WHERE p.Name >= 'M' AND p.Name < 'S';

Và cả hai kế hoạch gần như giống hệt với những gì trước đây!

Hàng biến

Một lần nữa, bỏ qua các chi phí ước tính - nhưng tôi vẫn thích kịch bản TOP, vì nó đơn giản hơn rất nhiều và kế hoạch không có toán tử chặn. Các bài đọc ít hơn trên TransactionHistory vì số lượng số 0 rất cao DaysToManufacture, nhưng trong thực tế, tôi nghi ngờ chúng ta sẽ chọn cột đó. ;)

Một cách để tránh khối là đưa ra một kế hoạch xử lý ROW_NUMBER()bit ở bên phải (trong kế hoạch) của phép nối. Chúng tôi có thể thuyết phục điều này xảy ra bằng cách tham gia bên ngoài CTE.

WITH Numbered AS
(
SELECT h.TransactionID, h.ProductID, h.TransactionDate, ROW_NUMBER() OVER (PARTITION BY ProductID ORDER BY TransactionDate DESC) AS RowNum
FROM Production.TransactionHistory h
)
SELECT p.Name, p.ProductID, t.TransactionID, t.TransactionDate
FROM Production.Product p
LEFT JOIN Numbered t ON t.ProductID = p.ProductID
    AND t.RowNum <= 5 * p.DaysToManufacture
WHERE p.Name >= 'M' AND p.Name < 'S';

Kế hoạch ở đây có vẻ đơn giản hơn - nó không chặn, nhưng có một mối nguy hiểm tiềm ẩn.

Tham gia bên ngoài CTE

Lưu ý tính toán vô hướng kéo dữ liệu từ bảng Sản phẩm. Đây là làm việc ra 5 * p.DaysToManufacturegiá trị. Giá trị này không được truyền vào chi nhánh lấy dữ liệu từ bảng TransactionHistory, nó được sử dụng trong Hợp nhất Tham gia. Là một người còn lại.

Lén lút còn sót lại!

Vì vậy, Hợp nhất Tham gia đang tiêu thụ TẤT CẢ các hàng, không chỉ là hàng đầu tiên rất cần thiết, mà là tất cả chúng và sau đó thực hiện kiểm tra dư. Điều này là nguy hiểm khi số lượng giao dịch tăng lên. Tôi không phải là người hâm mộ kịch bản này - các vị từ còn lại trong Hợp nhất có thể nhanh chóng leo thang. Một lý do khác tại sao tôi thích APPLY/TOPkịch bản.

Trong trường hợp đặc biệt trong đó chính xác là một hàng, đối với câu hỏi 3 , rõ ràng chúng ta có thể sử dụng cùng một truy vấn, nhưng 1thay vì 5. Nhưng sau đó chúng ta có một tùy chọn bổ sung, đó là sử dụng tổng hợp thông thường.

SELECT ProductID, MAX(TransactionDate)
FROM Production.TransactionHistory
GROUP BY ProductID;

Một truy vấn như thế này sẽ là một khởi đầu hữu ích và chúng tôi có thể dễ dàng sửa đổi nó để rút ra Giao dịch cũng như cho các mục đích hòa vốn (sử dụng phép nối mà sau đó sẽ bị phá vỡ), nhưng chúng tôi sẽ xem xét toàn bộ chỉ mục hoặc chúng tôi đi sâu vào từng sản phẩm và chúng tôi không thực sự có được sự cải thiện lớn về những gì chúng tôi có trước đây trong kịch bản này.

Nhưng tôi nên chỉ ra rằng chúng ta đang xem xét một kịch bản cụ thể ở đây. Với dữ liệu thực và với chiến lược lập chỉ mục có thể không lý tưởng, số dặm có thể thay đổi đáng kể. Mặc dù thực tế là chúng ta đã thấy rằng APPLYnó mạnh ở đây, nhưng nó có thể chậm hơn trong một số tình huống. Mặc dù vậy, nó hiếm khi chặn, vì nó có xu hướng sử dụng Vòng lặp Nested, điều mà nhiều người (bao gồm cả tôi) thấy rất hấp dẫn.

Tôi đã không cố gắng khám phá sự song song ở đây, hoặc đã rất chăm chỉ vào câu hỏi 3, mà tôi thấy đó là một trường hợp đặc biệt mà mọi người hiếm khi muốn dựa trên sự phức tạp của việc ghép và tách. Điều chính cần xem xét ở đây là hai tùy chọn này đều rất mạnh.

Tôi thích APPLY. Rõ ràng, nó sử dụng toán tử Top tốt và hiếm khi gây ra chặn.


44

Cách điển hình để thực hiện điều này trong SQL Server 2005 trở lên là sử dụng CTE và các chức năng cửa sổ. Đối với top n trên mỗi nhóm, bạn có thể chỉ cần sử dụng ROW_NUMBER()với một PARTITIONmệnh đề và lọc theo điều đó trong truy vấn bên ngoài. Vì vậy, ví dụ, 5 đơn hàng hàng đầu gần đây nhất cho mỗi khách hàng có thể được hiển thị theo cách này:

DECLARE @top INT;
SET @top = 5;

;WITH grp AS 
(
   SELECT CustomerID, OrderID, OrderDate,
     rn = ROW_NUMBER() OVER
     (PARTITION BY CustomerID ORDER BY OrderDate DESC)
   FROM dbo.Orders
)
SELECT CustomerID, OrderID, OrderDate
  FROM grp
  WHERE rn <= @top
  ORDER BY CustomerID, OrderDate DESC;

Bạn cũng có thể làm điều này với CROSS APPLY:

DECLARE @top INT;
SET @top = 5;

SELECT c.CustomerID, o.OrderID, o.OrderDate
FROM dbo.Customers AS c
CROSS APPLY 
(
    SELECT TOP (@top) OrderID, OrderDate 
    FROM dbo.Orders AS o
    WHERE CustomerID = c.CustomerID
    ORDER BY OrderDate DESC
) AS o
ORDER BY c.CustomerID, o.OrderDate DESC;

Với tùy chọn bổ sung mà Paul đã chỉ định, giả sử bảng Khách hàng có một cột cho biết số lượng hàng cần bao gồm cho mỗi khách hàng:

;WITH grp AS 
(
   SELECT CustomerID, OrderID, OrderDate,
     rn = ROW_NUMBER() OVER
     (PARTITION BY CustomerID ORDER BY OrderDate DESC)
   FROM dbo.Orders
)
SELECT c.CustomerID, grp.OrderID, grp.OrderDate
  FROM grp 
  INNER JOIN dbo.Customers AS c
  ON grp.CustomerID = c.CustomerID
  AND grp.rn <= c.Number_of_Recent_Orders_to_Show
  ORDER BY c.CustomerID, grp.OrderDate DESC;

Và một lần nữa, bằng cách sử dụng CROSS APPLYvà kết hợp tùy chọn đã thêm rằng số lượng hàng cho một khách hàng sẽ được quyết định bởi một số cột trong bảng khách hàng:

SELECT c.CustomerID, o.OrderID, o.OrderDate
FROM dbo.Customers AS c
CROSS APPLY 
(
    SELECT TOP (c.Number_of_Recent_Orders_to_Show) OrderID, OrderDate 
    FROM dbo.Orders AS o
    WHERE CustomerID = c.CustomerID
    ORDER BY OrderDate DESC
) AS o
ORDER BY c.CustomerID, o.OrderDate DESC;

Lưu ý rằng những điều này sẽ thực hiện khác nhau tùy thuộc vào phân phối dữ liệu và tính khả dụng của các chỉ mục hỗ trợ, vì vậy tối ưu hóa hiệu suất và có được kế hoạch tốt nhất sẽ thực sự phụ thuộc vào các yếu tố địa phương.

Cá nhân, tôi thích các giải pháp CTE và cửa sổ hơn CROSS APPLY/ TOPvì chúng tách logic tốt hơn và trực quan hơn (với tôi). Nói chung (cả trong trường hợp này và theo kinh nghiệm chung của tôi), phương pháp CTE tạo ra các kế hoạch hiệu quả hơn (ví dụ bên dưới), nhưng điều này không nên được coi là một sự thật phổ quát - bạn nên luôn kiểm tra các kịch bản của mình, đặc biệt là nếu các chỉ mục đã thay đổi hoặc dữ liệu đã sai lệch đáng kể.


Ví dụ AdventureWorks - không có bất kỳ thay đổi nào

  1. Liệt kê năm ngày giao dịch gần đây nhất và ID từ TransactionHistorybảng, cho mỗi sản phẩm bắt đầu bằng một chữ cái từ M đến R.
-- CTE / OVER()

;WITH History AS
(
  SELECT p.ProductID, p.Name, t.TransactionID, t.TransactionDate,
    rn = ROW_NUMBER() OVER 
    (PARTITION BY t.ProductID ORDER BY t.TransactionDate DESC)
  FROM Production.Product AS p
  INNER JOIN Production.TransactionHistory AS t
  ON p.ProductID = t.ProductID
  WHERE p.Name >= N'M' AND p.Name < N'S'
)
SELECT ProductID, Name, TransactionID, TransactionDate
FROM History 
WHERE rn <= 5;

-- CROSS APPLY

SELECT p.ProductID, p.Name, t.TransactionID, t.TransactionDate
FROM Production.Product AS p
CROSS APPLY
(
  SELECT TOP (5) TransactionID, TransactionDate
  FROM Production.TransactionHistory
  WHERE ProductID = p.ProductID
  ORDER BY TransactionDate DESC
) AS t
WHERE p.Name >= N'M' AND p.Name < N'S';

So sánh hai số liệu này trong thời gian chạy:

nhập mô tả hình ảnh ở đây

CTE / OVER()kế hoạch:

nhập mô tả hình ảnh ở đây

CROSS APPLY kế hoạch:

nhập mô tả hình ảnh ở đây

Kế hoạch CTE có vẻ phức tạp hơn, nhưng nó thực sự hiệu quả hơn nhiều. Ít chú ý đến các con số% chi phí ước tính, nhưng tập trung vào các quan sát thực tế quan trọng hơn , chẳng hạn như số lần đọc ít hơn và thời lượng thấp hơn nhiều. Tôi cũng chạy những thứ này mà không có sự song song, và đây không phải là sự khác biệt. Số liệu thời gian chạy và kế hoạch CTE ( CROSS APPLYkế hoạch vẫn giữ nguyên):

nhập mô tả hình ảnh ở đây

nhập mô tả hình ảnh ở đây

  1. Tương tự một lần nữa, nhưng với ncác dòng lịch sử trên mỗi sản phẩm, trong đó nnăm lần DaysToManufacturethuộc tính Sản phẩm.

Những thay đổi rất nhỏ cần thiết ở đây. Đối với CTE, chúng ta có thể thêm một cột vào truy vấn bên trong và bộ lọc trên truy vấn bên ngoài; đối với CROSS APPLY, chúng ta có thể thực hiện tính toán bên trong tương quan TOP. Bạn sẽ nghĩ rằng điều này sẽ cho vay một số hiệu quả cho CROSS APPLYgiải pháp, nhưng điều đó không xảy ra trong trường hợp này. Truy vấn:

-- CTE / OVER()

;WITH History AS
(
  SELECT p.ProductID, p.Name, p.DaysToManufacture, t.TransactionID, t.TransactionDate,
    rn = ROW_NUMBER() OVER 
    (PARTITION BY t.ProductID ORDER BY t.TransactionDate DESC)
  FROM Production.Product AS p
  INNER JOIN Production.TransactionHistory AS t
  ON p.ProductID = t.ProductID
  WHERE p.Name >= N'M' AND p.Name < N'S'
)
SELECT ProductID, Name, TransactionID, TransactionDate
FROM History 
WHERE rn <= (5 * DaysToManufacture);

-- CROSS APPLY

SELECT p.ProductID, p.Name, t.TransactionID, t.TransactionDate
FROM Production.Product AS p
CROSS APPLY
(
  SELECT TOP (5 * p.DaysToManufacture) TransactionID, TransactionDate
  FROM Production.TransactionHistory
  WHERE ProductID = p.ProductID
  ORDER BY TransactionDate DESC
) AS t
WHERE p.Name >= N'M' AND p.Name < N'S';

Kết quả thời gian chạy:

nhập mô tả hình ảnh ở đây

CTE song song / OVER()kế hoạch:

nhập mô tả hình ảnh ở đây

CTE đơn luồng / OVER()kế hoạch:

nhập mô tả hình ảnh ở đây

CROSS APPLY kế hoạch:

nhập mô tả hình ảnh ở đây

  1. Tương tự, đối với trường hợp đặc biệt yêu cầu chính xác một dòng lịch sử cho mỗi sản phẩm (mục nhập gần đây nhất bằng cách TransactionDatekết hợp TransactionID.

Một lần nữa, những thay đổi nhỏ ở đây. Trong giải pháp CTE, chúng tôi thêm TransactionIDvào OVER()mệnh đề và thay đổi bộ lọc bên ngoài thành rn = 1. Đối với CROSS APPLY, chúng tôi thay đổi TOPthành TOP (1), và thêm TransactionIDvào bên trong ORDER BY.

-- CTE / OVER()

;WITH History AS
(
  SELECT p.ProductID, p.Name, t.TransactionID, t.TransactionDate,
    rn = ROW_NUMBER() OVER 
    (PARTITION BY t.ProductID ORDER BY t.TransactionDate DESC, TransactionID DESC)
  FROM Production.Product AS p
  INNER JOIN Production.TransactionHistory AS t
  ON p.ProductID = t.ProductID
  WHERE p.Name >= N'M' AND p.Name < N'S'
)
SELECT ProductID, Name, TransactionID, TransactionDate
FROM History 
WHERE rn = 1;

-- CROSS APPLY

SELECT p.ProductID, p.Name, t.TransactionID, t.TransactionDate
FROM Production.Product AS p
CROSS APPLY
(
  SELECT TOP (1) TransactionID, TransactionDate
  FROM Production.TransactionHistory
  WHERE ProductID = p.ProductID
  ORDER BY TransactionDate DESC, TransactionID DESC
) AS t
WHERE p.Name >= N'M' AND p.Name < N'S';

Kết quả thời gian chạy:

nhập mô tả hình ảnh ở đây

CTE song song / OVER()kế hoạch:

nhập mô tả hình ảnh ở đây

Gói CTE / OVER () đơn luồng:

nhập mô tả hình ảnh ở đây

CROSS APPLY kế hoạch:

nhập mô tả hình ảnh ở đây

Các hàm cửa sổ không phải luôn luôn là lựa chọn thay thế tốt nhất (có một lúc COUNT(*) OVER()) và đây không phải là hai cách tiếp cận duy nhất để giải quyết n hàng trên mỗi vấn đề của nhóm, nhưng trong trường hợp cụ thể này - đưa ra lược đồ, chỉ mục hiện có và phân phối dữ liệu - CTE đã hoạt động tốt hơn bởi tất cả các tài khoản có ý nghĩa.


Ví dụ AdventureWorks - với tính linh hoạt để thêm chỉ mục

Tuy nhiên, nếu bạn thêm một chỉ mục hỗ trợ, tương tự như chỉ số mà Paul đã đề cập trong một nhận xét nhưng với cột thứ 2 và thứ 3 được yêu cầu DESC:

CREATE UNIQUE NONCLUSTERED INDEX UQ3 ON Production.TransactionHistory 
  (ProductID, TransactionDate DESC, TransactionID DESC);

Bạn thực sự sẽ nhận được nhiều kế hoạch thuận lợi hơn xung quanh và các số liệu sẽ được sử dụng để CROSS APPLYtiếp cận trong cả ba trường hợp:

nhập mô tả hình ảnh ở đây

Nếu đây là môi trường sản xuất của tôi, có lẽ tôi sẽ hài lòng với thời lượng trong trường hợp này và sẽ không bận tâm đến việc tối ưu hóa hơn nữa.


Đây là tất cả xấu hơn nhiều trong SQL Server 2000, không hỗ trợ APPLYhoặc OVER()mệnh đề.


24

Trong DBMS, như MySQL, không có chức năng cửa sổ hoặc CROSS APPLY, cách để làm điều này sẽ là sử dụng SQL chuẩn (89). Cách chậm sẽ là một tam giác chéo tham gia với tổng hợp. Cách nhanh hơn (nhưng vẫn và có thể không hiệu quả như sử dụng ứng dụng chéo hoặc hàm row_number) sẽ là cái mà tôi gọi là "người nghèo CROSS APPLY" . Sẽ rất thú vị khi so sánh truy vấn này với các truy vấn khác:

Giả định: Orders (CustomerID, OrderDate)có một UNIQUEràng buộc:

DECLARE @top INT;
SET @top = 5;

SELECT o.CustomerID, o.OrderID, o.OrderDate
  FROM dbo.Customers AS c
    JOIN dbo.Orders AS o
      ON  o.CustomerID = c.CustomerID
      AND o.OrderID IN
          ( SELECT TOP (@top) oi.OrderID
            FROM dbo.Orders AS oi
            WHERE oi.CustomerID = c.CustomerID
            ORDER BY oi.OrderDate DESC
          )
  ORDER BY CustomerID, OrderDate DESC ;

Đối với vấn đề thêm của các hàng đầu tùy chỉnh cho mỗi nhóm:

SELECT o.CustomerID, o.OrderID, o.OrderDate
  FROM dbo.Customers AS c
    JOIN dbo.Orders AS o
      ON  o.CustomerID = c.CustomerID
      AND o.OrderID IN
          ( SELECT TOP (c.Number_of_Recent_Orders_to_Show) oi.OrderID
            FROM dbo.Orders AS oi
            WHERE oi.CustomerID = c.CustomerID
            ORDER BY oi.OrderDate DESC
          )
  ORDER BY CustomerID, OrderDate DESC ;

Lưu ý: Trong MySQL, thay vì AND o.OrderID IN (SELECT TOP(@top) oi.OrderID ...)người ta sẽ sử dụng AND o.OrderDate >= (SELECT oi.OrderDate ... LIMIT 1 OFFSET (@top - 1)). FETCH / OFFSETCú pháp SQL-Server được thêm vào trong phiên bản 2012. Các truy vấn ở đây đã được điều chỉnh IN (TOP...)để làm việc với các phiên bản trước đó.


21

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:

  1. Bộ đầu tiên chạy không có sửa đổi DB
  2. Bộ thứ hai chạy sau khi một chỉ mục được tạo để hỗ trợ TransactionDatecác truy vấn dựa trên Production.TransactionHistory.
  3. 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 AdventureWorks2012trê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, RowCountsdườ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.Productvà nhận lại 161 hàng, sau đó nó sử dụng cho các truy vấn Production.TransactionHistory. Do đó, các RowCountgiá 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 Kết quả kiểm tra 1 - không có chỉ số
Aaron của CTE rõ ràng là người chiến thắng ở đây.

Thử nghiệm 2 Kiểm tra 2 Kết quả - không có chỉ số
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 Kiểm tra 3 kết quả - không có chỉ số
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.TransactionDatevì 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, TransactionDatenên trường được đặt hàng DESC, vì vậy tôi chỉ cần lấy CREATE INDEXcâ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 Kiểm tra 1 Kết quả - với chỉ số hỗ trợ
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 APPLYphươ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 Kiểm tra 2 Kết quả - với chỉ số hỗ trợ
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 Kiểm tra 3 Kết quả - với chỉ số hỗ trợ
Ở đâ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.Productcho 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 DaysToManufacturechỉ đượ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 Kiểm tra 1 Kết quả - với chỉ mục hỗ trợ VÀ bộ nhớ đệm
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 Kiểm tra 2 Kết quả - với chỉ mục hỗ trợ VÀ bộ nhớ đệm
Ở đâ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 Kiểm tra 3 Kết quả - với chỉ mục hỗ trợ VÀ bộ nhớ đệm
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 ProductIDs và trong một trường hợp cũng là DaysToManufacture, dựa trên Namebắt đầu bằng một số chữ cái) từ các truy vấn "chi tiết" (tức là lấy TransactionIDs và TransactionDates). 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 ProductIDtrong 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 DaysToManufacturethuộc tính Sản phẩm". Phương thức phụ này có nghĩa là mỗi phương thức ProductIDsẽ 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@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 ProductIDdướ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 ProductIDgiá 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.Productbả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ỳ ProductIDs nào thậm chí không có trong TransactionHistorybả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 DaysToManufacturelĩ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à TransactionHistorybả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, NameDaysToManufacture), 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 staticDanh sách chung (nghĩa là _GlobalProductstrong mã bên dưới). Dường như thêm vào bộ sưu tập không vi phạm các readonlytùy chọn, do đó mã này hoạt động khi lắp ráp có PERMISSON_SETcủ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 DESCcuố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 DESCvà thấy rằng CROSS APPLYphươ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 TransactionIdvà 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:

Kiểm tra 3 Kết quả - với chỉ số hỗ trợ (với TransactionId DESC)
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).

Kiểm tra 3 Kết quả - với chỉ mục hỗ trợ (với TransactionId DESC) VÀ bộ nhớ đệm
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 APPLYvẫ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

  1. 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, Productbảng chỉ có 504 hàng và TransactionHistorychỉ 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.

  2. 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.

  3. 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.


2
Xem chỉnh sửa của tôi để trả lời của Mikael vì lý do cho các lần đọc logic thêm liên quan đến áp dụng.
Paul White

18

APPLY TOPhay ROW_NUMBER()? Điều gì có thể có thể có nhiều hơn để nói về vấn đề đó?

Một bản tóm tắt ngắn về sự khác biệt và để thực sự ngắn gọn, tôi sẽ chỉ hiển thị các kế hoạch cho tùy chọn 2 và tôi đã thêm chỉ mục vào Production.TransactionHistory.

create index IX_TransactionHistoryX on 
  Production.TransactionHistory(ProductID, TransactionDate)

Các row_number()truy vấn :.

with C as
(
  select T.TransactionID,
         T.TransactionDate,
         P.DaysToManufacture,
         row_number() over(partition by P.ProductID order by T.TransactionDate desc) as rn
  from Production.Product as P
    inner join Production.TransactionHistory as T
      on P.ProductID = T.ProductID
  where P.Name >= N'M' and
        P.Name < N'S'
)
select C.TransactionID,
       C.TransactionDate
from C
where C.rn <= 5 * C.DaysToManufacture;

nhập mô tả hình ảnh ở đây

Các apply topphiên bản:

select T.TransactionID, 
       T.TransactionDate
from Production.Product as P
  cross apply (
              select top(cast(5 * P.DaysToManufacture as bigint))
                T.TransactionID,
                T.TransactionDate
              from Production.TransactionHistory as T
              where P.ProductID = T.ProductID
              order by T.TransactionDate desc
              ) as T
where P.Name >= N'M' and
      P.Name < N'S';

nhập mô tả hình ảnh ở đây

Sự khác biệt chính giữa những điều này là apply topcác bộ lọc trên biểu thức trên cùng bên dưới các vòng lặp lồng nhau tham gia nơi row_numbercác bộ lọc phiên bản sau khi nối. Điều đó có nghĩa là có nhiều đọc từ Production.TransactionHistoryhơn thực sự là cần thiết.

Nếu chỉ tồn tại một cách để đẩy các toán tử chịu trách nhiệm liệt kê các hàng xuống nhánh thấp hơn trước khi nối thì row_numberphiên bản có thể làm tốt hơn.

Vì vậy, nhập apply row_number()phiên bản.

select T.TransactionID, 
       T.TransactionDate
from Production.Product as P
  cross apply (
              select T.TransactionID,
                     T.TransactionDate
              from (
                   select T.TransactionID,
                          T.TransactionDate,
                          row_number() over(order by T.TransactionDate desc) as rn
                   from Production.TransactionHistory as T
                   where P.ProductID = T.ProductID
                   ) as T
              where T.rn <= cast(5 * P.DaysToManufacture as bigint)
              ) as T
where P.Name >= N'M' and
      P.Name < N'S';

nhập mô tả hình ảnh ở đây

Như bạn có thể thấy apply row_number()là khá nhiều tương tự như apply topchỉ phức tạp hơn một chút. Thời gian thực hiện cũng chậm hơn hoặc chậm hơn một chút.

Vậy tại sao tôi lại bận tâm đưa ra một câu trả lời không tốt hơn những gì chúng ta đã có? Vâng, bạn có một điều nữa để thử trong thế giới thực và thực sự có một sự khác biệt trong việc đọc. Một cái mà tôi không có lời giải thích cho *.

APPLY - ROW_NUMBER
(961 row(s) affected)
Table 'TransactionHistory'. Scan count 115, logical reads 230, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Product'. Scan count 1, logical reads 15, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

APPLY - TOP
(961 row(s) affected)
Table 'TransactionHistory'. Scan count 115, logical reads 268, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Product'. Scan count 1, logical reads 15, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

Trong khi tôi đang ở đó, tôi cũng có thể ném row_number()phiên bản thứ hai mà trong một số trường hợp nhất định có thể là con đường để đi. Những trường hợp nhất định sẽ là khi bạn mong đợi bạn thực sự cần hầu hết các hàng từ Production.TransactionHistoryvì ở đây bạn có một phép nối hợp nhất giữa Production.Productvà liệt kê Production.TransactionHistory.

with C as
(
  select T.TransactionID,
         T.TransactionDate,
         T.ProductID,
         row_number() over(partition by T.ProductID order by T.TransactionDate desc) as rn
  from Production.TransactionHistory as T
)
select C.TransactionID,
       C.TransactionDate
from C
 inner join Production.Product as P
      on P.ProductID = C.ProductID
where P.Name >= N'M' and
      P.Name < N'S' and
      C.rn <= 5 * P.DaysToManufacture;

nhập mô tả hình ảnh ở đây

Để có được hình dạng trên mà không có toán tử sắp xếp, bạn cũng phải thay đổi chỉ mục hỗ trợ để đặt hàng bằng cách TransactionDategiảm dần.

create index IX_TransactionHistoryX on 
  Production.TransactionHistory(ProductID, TransactionDate desc)

* Chỉnh sửa: Các lần đọc logic bổ sung là do các vòng lặp lồng nhau được tìm nạp trước được sử dụng với ứng dụng hàng đầu. Bạn có thể vô hiệu hóa điều này với TF 8744 (và / hoặc 9115 trên các phiên bản mới hơn) để có cùng số lần đọc logic. Tìm nạp trước có thể là một lợi thế của lựa chọn thay thế hàng đầu trong các trường hợp phù hợp. - Paul White


11

Tôi thường sử dụng kết hợp các CTE và các chức năng cửa sổ. Bạn có thể đạt được câu trả lời này bằng cách sử dụng một cái gì đó như sau:

;WITH GiveMeCounts
AS (
    SELECT CustomerID
        ,OrderDate
        ,TotalAmt

        ,ROW_NUMBER() OVER (
            PARTITION BY CustomerID ORDER BY 
            --You can change the following field or sort order to whatever you'd like to order by.
            TotalAmt desc
            ) AS MySeqNum
    )
SELECT CustomerID, OrderDate, TotalAmt
FROM GiveMeCounts
--Set n per group here
where MySeqNum <= 10

Đối với phần tín dụng bổ sung, nơi các nhóm khác nhau có thể muốn trả về số lượng hàng khác nhau, bạn có thể sử dụng một bảng riêng. Hãy nói rằng sử dụng các tiêu chí địa lý như nhà nước:

+-------+-----------+
| State | MaxSeqnum |
+-------+-----------+
| AK    |        10 |
| NY    |         5 |
| NC    |        23 |
+-------+-----------+

Để đạt được điều này khi các giá trị có thể khác nhau, bạn cần tham gia CTE của mình vào bảng Trạng thái tương tự như sau:

SELECT [CustomerID]
    ,[OrderDate]
    ,[TotalAmt]
    ,[State]
FROM GiveMeCounts gmc
INNER JOIN StateTable st ON gmc.[State] = st.[State]
    AND gmc.MySeqNum <= st.MaxSeqNum
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.