나는 옵션이 좋은 것이기 때문에 주로이 기술이 다른 기술과 어떻게 비교되는지 알기 위해 약간 다른 접근 방식을 취했습니다.
테스트
다양한 방법이 서로 어떻게 쌓여 있는지 살펴 보는 것으로 시작하지 않겠습니까? 세 가지 테스트를 수행했습니다.
- 첫 번째 세트는 DB 수정없이 실행되었습니다.
- 에 대한
TransactionDate
기반 쿼리 를 지원하기 위해 인덱스가 작성된 후 두 번째 세트가 실행되었습니다 Production.TransactionHistory
.
- 세 번째 세트는 약간 다른 가정을했습니다. 세 가지 테스트 모두 동일한 제품 목록에 대해 실행되었으므로 해당 목록을 캐시하면 어떻게됩니까? 내 방법은 메모리 내 캐시를 사용하는 반면 다른 방법은 동등한 임시 테이블을 사용했습니다. 두 번째 테스트 세트에 대해 작성된 지원 색인은이 테스트 세트에 여전히 존재합니다.
추가 테스트 세부 사항 :
- 테스트는
AdventureWorks2012
SQL Server 2012 SP2 (Developer Edition)에서 실행되었습니다.
- 각 테스트마다 나는 그 질문에 대한 답변과 어떤 특정 쿼리에 대한 답을 표시했습니다.
- 쿼리 옵션 | "실행 후 결과 폐기"옵션을 사용했습니다. 결과.
- 처음 두 세트의 테스트에서
RowCounts
내 방법에 대해 "끄기"로 나타납니다. 이것은 내 방법이 수행중인 작업의 수동 구현이기 CROSS APPLY
때문입니다. 초기 쿼리를 실행하고 Production.Product
161 행을 가져 와서 에 대해 쿼리에 사용합니다 Production.TransactionHistory
. 따라서 RowCount
내 항목 의 값은 항상 다른 항목보다 161 더 큽니다. 세 번째 테스트 (캐싱 사용)에서 행 수는 모든 방법에 대해 동일합니다.
- 실행 계획에 의존하는 대신 SQL Server 프로파일 러를 사용하여 통계를 캡처했습니다. Aaron과 Mikael은 이미 쿼리 계획을 보여주는 훌륭한 작업을 수행했으며 해당 정보를 재현 할 필요가 없습니다. 그리고 내 방법의 의도는 쿼리를 실제로 중요하지 않은 간단한 형태로 줄이는 것입니다. 프로파일 러를 사용하는 또 다른 이유가 있지만 나중에 설명하겠습니다.
Name >= N'M' AND Name < N'S'
구문을 사용하는 대신 을 사용하기로 선택 Name LIKE N'[M-R]%'
했으며 SQL Server는이를 동일하게 취급합니다.
결과
지지 지수 없음
이것은 기본적으로 제공되는 AdventureWorks2012입니다. 모든 경우에 내 방법은 다른 방법보다 분명히 우수하지만 상위 1 또는 2 방법만큼 좋은 것은 아닙니다.
테스트 1
Aaron의 CTE가 분명히 승자입니다.
테스트 2
Aaron의 CTE (다시)와 Mikael의 두 번째 apply row_number()
방법은 아주 가깝습니다.
테스트 3
Aaron의 CTE (다시)가 승자입니다.
결론
에 대한 지원 색인이없는 TransactionDate
경우 표준 방법보다 내 방법이 낫지 CROSS APPLY
만 여전히 CTE 방법을 사용하는 것이 좋습니다 .
지원 인덱스 포함 (캐싱 없음)
이 테스트 세트의 경우 TransactionHistory.TransactionDate
모든 쿼리가 해당 필드에서 정렬되므로 명백한 색인을 추가했습니다 . 다른 답변들도이 점에 동의하기 때문에 "명백하다"고 말합니다. 쿼리가 모두 가장 최근 날짜를 원하기 때문에 TransactionDate
필드를 정렬해야 DESC
하므로 CREATE INDEX
Mikael의 답변 맨 아래에 있는 진술을 잡고 명시 적으로 추가했습니다 FILLFACTOR
.
CREATE INDEX [IX_TransactionHistoryX]
ON Production.TransactionHistory (ProductID ASC, TransactionDate DESC)
WITH (FILLFACTOR = 100);
이 지수가 제정되면 결과가 약간 변경됩니다.
테스트 1
이번에는 적어도 논리적 읽기와 관련하여 앞서 나오는 방법입니다. CROSS APPLY
방법, 시험 1 이전에 최악의 활약은, 기간에 승리하고도 논리적 읽고에 CTE 방법을 친다.
테스트 2
이번에 apply row_number()
는 Reads를 볼 때 가장 좋은 방법 은 Mikael의 첫 번째 방법이지만, 이전에는 최악의 성능 중 하나였습니다. 그리고 이제 내 방법은 Reads를 볼 때 매우 가까운 2 위를 차지합니다. 실제로 CTE 방법 이외의 나머지는 모두 읽기 측면에서 상당히 가깝습니다.
테스트 3
여기서 CTE가 여전히 승자이지만, 이제 다른 방법들 사이의 차이는 인덱스를 만들기 전에 존재했던 과감한 차이와 비교할 때 거의 눈에 띄지 않습니다.
결론
적절한 인덱스를 사용하지 않는 것이 탄력성이 떨어지지 만 내 방법의 적용 가능성은 이제 더 분명합니다.
인덱스 및 캐싱 지원
이 테스트 세트에서 캐싱을 사용했습니다. 왜 그렇지 않습니까? 내 방법을 사용하면 다른 방법으로 액세스 할 수없는 메모리 내 캐싱을 사용할 수 있습니다. 공정하게 Product.Product
하기 위해 세 가지 테스트 모두에서 다른 방법의 모든 참조 대신 사용되는 다음 임시 테이블을 만들었습니다 . 이 DaysToManufacture
필드는 테스트 번호 2에서만 사용되지만 동일한 테이블을 사용하기 위해 SQL 스크립트에서 일관성을 유지하는 것이 더 쉬웠으며 거기에 두는 것이 아프지 않았습니다.
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);
테스트 1
모든 방법이 캐싱에서 똑같이 이익을 얻는 것처럼 보이며 제 방법은 여전히 앞서 나옵니다.
테스트 2
이제 우리의 방법이 간신히 나올 때 라인업의 차이를 볼 수 있습니다. 2 개의 읽기만 Mikael의 첫 번째 apply row_number()
방법 보다 나은 반면, 캐싱이 없으면 내 방법은 4 개의 읽기로 이루어졌습니다.
테스트 3
하단 (라인 아래)으로 업데이트를 참조하십시오 . 여기서 우리는 또 다른 차이점을 보게됩니다. 내 분석법의 "매개 변수화 된"맛은 이제 Aaron의 CROSS APPLY 방법 (캐싱없이 동일)에 비해 2 개의 읽기로 인해 거의 선두에 서 있지 않습니다. 그러나 정말 이상한 점은 캐싱에 부정적인 영향을받는 방법 인 Aaron의 CTE 방법 (처음에는 테스트 번호 3에 가장 적합 함)을 처음으로 보는 것입니다. 그러나 캐싱이 없으면 Aaron의 CTE 방법이 캐싱과 함께 사용하는 방법보다 여전히 빠르기 때문에이 특정 상황에 대한 가장 좋은 방법은 Aaron의 CTE 방법으로 보입니다.
결론 하단 (라인 아래)으로 업데이트를 참조하십시오
. 보조 쿼리의 결과를 반복적으로 사용하는 상황은 이러한 결과를 캐싱하는 데 도움이 될 수 있습니다 (항상 그런 것은 아님). 그러나 캐싱이 이점 인 경우, 상기 캐싱에 메모리를 사용하면 임시 테이블을 사용하는 것보다 몇 가지 이점이 있습니다.
방법
일반적으로
나는 "헤더"질의 분리 (즉, 점점 ProductID
들, 하나의 경우도 DaysToManufacture
,에 기초하여 Name
특정 문자로 시작하는)은 "상세"쿼리에서 (즉,이 점점 TransactionID
들과 TransactionDate
들). 개념은 매우 간단한 쿼리를 수행하고 옵티마이 저가 조인 할 때 혼동되지 않도록하는 것입니다. 분명히 이것은 최적화 프로그램이 최적화를 잘하지 못하기 때문에 항상 유리 하지는 않습니다 . 그러나 결과에서 볼 수 있듯이 쿼리 유형에 따라이 방법에는 장점이 있습니다.
이 방법의 다양한 맛의 차이점은 다음과 같습니다.
상수 : 대체 가능한 값을 매개 변수 대신 인라인 상수로 제출하십시오. 이는 ProductID
" DaysToManufacture
제품 속성의 5 배"기능이므로 테스트 2에서 반환 할 행 수와 세 가지 테스트 모두에서 언급됩니다 . 이 하위 방법은 각각 ProductID
고유 한 실행 계획을 얻게되며, 이는 데이터 배포에 다양한 변형이있는 경우 유용 할 수 있습니다 ProductID
. 그러나 데이터 배포에 변동이 거의없는 경우 추가 계획을 생성하는 비용은 그만한 가치가 없을 것입니다.
매개 변수화 : 최소한 ProductID
으로 제출하여 @ProductID
실행 계획 캐싱 및 재사용을 허용하십시오. 테스트 2에 대해 리턴 할 변수 행 수를 매개 변수로 처리하기위한 추가 테스트 옵션이 있습니다.
알 수없는 최적화 : 참조하는 경우 ProductID
로 @ProductID
, 데이터 분포의 폭 넓은 변화가있을 경우 다음 다른에 부정적인 영향이 계획 캐시 할 수 ProductID
는이 쿼리 힌트를 사용하여 어떤 도움이되는지 알고 좋은 것, 그래서 값을.
캐시 제품 :Production.Product
매번 테이블을 쿼리하지 않고 정확히 동일한 목록을 가져 와서 쿼리를 한 번만 실행하십시오 (그리고 우리가 테이블에 있지 않은 ProductID
동안 TransactionHistory
테이블에 있지 않은 모든 것을 필터링하여 낭비하지 않도록하십시오) 리소스를 찾고 해당 목록을 캐시합니다. 목록에는 DaysToManufacture
필드 가 포함되어야 합니다. 이 옵션을 사용하면 첫 번째 실행에 대한 논리적 읽기에서 초기 히트가 약간 더 높지만 그 후에 TransactionHistory
는 쿼리 된 테이블 만됩니다 .
구체적으로
그러나 CURSOR를 사용하지 않고 각 결과 세트를 임시 테이블 또는 테이블 변수에 덤프하지 않고 모든 하위 쿼리를 개별 쿼리로 발행하는 방법은 무엇입니까? CURSOR / Temp Table 방법을 명확하게 수행하면 읽기 및 쓰기에 분명히 반영됩니다. 글쎄, SQLCLR을 사용하여 :). SQLCLR 저장 프로 시저를 만들면 결과 집합을 열고 기본적으로 각 하위 쿼리의 결과를 여러 결과 집합이 아닌 연속 결과 집합으로 스트리밍 할 수있었습니다. 제품 정보 외부 (예 ProductID
: Name
, 및DaysToManufacture
) 하위 쿼리 결과는 어디에도 저장하지 않아도되며 (메모리 또는 디스크) SQLCLR 저장 프로 시저의 기본 결과 집합으로 전달되지 않습니다. 이를 통해 제품 정보를 얻기 위해 간단한 쿼리를 수행 한 다음에 대해 매우 간단한 쿼리를 수행하여 제품 정보를 살펴볼 수있었습니다 TransactionHistory
.
그리고 이것이 통계를 캡처하기 위해 SQL Server 프로파일 러를 사용해야하는 이유입니다. "실제 실행 계획 포함"쿼리 옵션을 설정하거나을 실행하여 SQLCLR 저장 프로 시저가 실행 계획을 반환하지 않았습니다 SET STATISTICS XML ON;
.
제품 정보 캐싱을 위해 readonly static
일반 목록 (예 : _GlobalProducts
아래 코드)을 사용했습니다. 이 컬렉션에 추가하는 것은 위반하지 않는 것 같다 readonly
어셈블리가있을 때 따라서이 코드가 작동, 옵션 PERMISSON_SET
의 SAFE
그 반 직관적 경우에도 :)을.
생성 된 쿼리
이 SQLCLR 저장 프로 시저에서 생성 된 쿼리는 다음과 같습니다.
제품 정보
테스트 번호 1과 3 (캐싱 없음)
SELECT prod1.ProductID, prod1.Name, 1 AS [DaysToManufacture]
FROM Production.Product prod1
WHERE prod1.Name LIKE N'[M-R]%';
테스트 번호 2 (캐싱 없음)
;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;
테스트 번호 1, 2 및 3 (캐싱)
;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;
거래 정보
테스트 번호 1과 2 (상수)
SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = 977
ORDER BY th.TransactionDate DESC;
테스트 번호 1 및 2 (파라미터 화)
SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
;
테스트 번호 1 및 2 (파라미터 + OPTIMIZE UNKNOWN)
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));
테스트 번호 2 (모두 매개 변수화 됨)
SELECT TOP (@RowsToReturn) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
;
테스트 번호 2 (모두 매개 변수화 됨 + 최적화 불가)
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));
테스트 번호 3 (상수)
SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = 977
ORDER BY th.TransactionDate DESC, th.TransactionID DESC;
테스트 번호 3 (파라미터 화)
SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC, th.TransactionID DESC
;
테스트 번호 3 (파라미터 + OPTIMIZE UNKNOWN)
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));
코드
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);
}
}
}
}
시험 쿼리
여기에 시험을 게시 할 공간이 충분하지 않아 다른 위치를 찾을 수 있습니다.
결론
특정 시나리오의 경우 SQLCLR을 사용하여 T-SQL에서 수행 할 수없는 쿼리의 특정 측면을 조작 할 수 있습니다. 임시 테이블 대신 캐싱에 메모리를 사용하는 기능이 있지만 메모리가 시스템에 자동으로 다시 릴리스되지 않으므로 신중하고 신중하게 수행해야합니다. 이 방법은 또한 임시 쿼리에 도움이되는 것은 아니지만 실행중인 쿼리의 더 많은 측면을 조정하기 위해 매개 변수를 추가하여 여기에 표시된 것보다 더 유연하게 만들 수 있습니다.
최신 정보
추가 테스트
지원 인덱스가 포함 된 원래 테스트 TransactionHistory
는 다음 정의 를 사용했습니다.
ProductID ASC, TransactionDate DESC
나는 TransactionId DESC
마지막에 포함하는 것을 포기하기로 결심했다. 테스트 번호 3 (가장 최근에 잘 TransactionId
풀리는 것을 지정하는 테스트 번호 3을 명시 할 수는 있지만 명시 적으로 언급되지 않았기 때문에 "가장 최근의"이라고 가정 함) 이 가정에 동의하기 위해서는 차이를 만들기에 충분한 유대가 없을 것입니다.
그러나 Aaron은이 세 가지 테스트 모두에서이 방법이 승자 TransactionId DESC
라는 것을 포함하는지지 지수로 다시 CROSS APPLY
테스트했습니다. 이것은 CTE 방법이 테스트 번호 3에 가장 적합하다는 것을 나타내는 내 테스트와 다릅니다 (캐싱을 사용하지 않을 때 Aaron의 테스트를 반영 함). 테스트해야 할 추가 변형이 있음이 분명했습니다.
현재 지원 색인을 제거하고으로 새로운 색인을 생성 TransactionId
하고 계획 캐시를 지 웠습니다 (확실히).
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;
테스트 번호 1을 다시 실행했으며 예상대로 결과가 동일했습니다. 그런 다음 테스트 번호 3을 다시 실행하고 결과가 실제로 변경되었습니다.
위의 결과는 표준 비 캐싱 테스트에 대한 것입니다. 이번에 CROSS APPLY
는 Aaron의 테스트에서 알 수 있듯이 CTE를 이길뿐만 아니라 SQLCLR proc이 30 Reads (woo hoo)만큼 우위를 차지했습니다.
위의 결과는 캐싱이 활성화 된 테스트에 대한 것입니다. 이번에는 CTE의 성능이 CROSS APPLY
여전히 저하되지는 않지만 성능이 저하되지는 않습니다 . 그러나 이제는 SQLCLR proc이 23 Reads (woo hoo, 다시 한 번)로 앞서고 있습니다.
핵심, 관심사
사용할 수있는 다양한 옵션이 있습니다. 그들이 각각의 강점을 가지고 있기 때문에 여러 가지를 시도하는 것이 가장 좋습니다. 여기에서 수행 된 테스트는 모든 테스트에서 최고 및 최저 성능 사이의 읽기 및 지속 시간에서 약간의 차이를 나타냅니다 (지원 인덱스 포함). 읽기의 변화는 약 350이고 지속 시간은 55ms입니다. SQLCLR proc은 1 번의 테스트 (읽기 측면에서)를 제외하고 모두 승리했지만, 몇 번의 읽기만 저장하는 것은 일반적으로 SQLCLR 경로를 유지하는 유지 관리 비용이 들지 않습니다. 그러나 AdventureWorks2012에서 Product
테이블에는 504 개의 행만 있고 TransactionHistory
113,443 개의 행만 있습니다. 이러한 방법의 성능 차이는 행 수가 증가함에 따라 더욱 두드러집니다.
이 질문은 특정 행 집합을 가져 오는 것과 관련이 있었지만 성능에서 가장 큰 단일 요소는 인덱싱이고 특정 SQL은 아니라고 간과해서는 안됩니다. 어떤 방법이 가장 적합한지를 결정하기 전에 좋은 지수를 마련해야합니다.
여기에서 가장 중요한 교훈은 CROSS APPLY vs CTE vs SQLCLR에 관한 것이 아니라 테스트에 관한 것입니다. 가정하지 마십시오. 여러 사람으로부터 아이디어를 얻고 최대한 많은 시나리오를 테스트하십시오.