두 날짜 열에 대한 SARGable WHERE 절


24

SARGability에 대한 흥미로운 질문이 있습니다. 이 경우 두 날짜 열의 차이점에 대한 술어를 사용하는 것입니다. 설정은 다음과 같습니다.

USE [tempdb]
SET NOCOUNT ON  

IF OBJECT_ID('tempdb..#sargme') IS NOT NULL
BEGIN
DROP TABLE #sargme
END

SELECT TOP 1000
IDENTITY (BIGINT, 1,1) AS ID,
CAST(DATEADD(DAY, [m].[severity] * -1, GETDATE()) AS DATE) AS [DateCol1],
CAST(DATEADD(DAY, [m].[severity], GETDATE()) AS DATE) AS [DateCol2]
INTO #sargme
FROM sys.[messages] AS [m]

ALTER TABLE [#sargme] ADD CONSTRAINT [pk_whatever] PRIMARY KEY CLUSTERED ([ID])
CREATE NONCLUSTERED INDEX [ix_dates] ON [#sargme] ([DateCol1], [DateCol2])

내가 자주 보게 될 것은 다음과 같습니다.

/*definitely not sargable*/
SELECT
    * ,
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
    [#sargme] AS [s]
WHERE
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2]) >= 48;

확실히 SARGable이 아닙니다. 인덱스 스캔이 발생하고 1000 행을 모두 읽습니다. 예상 행이 악취가납니다. 당신은 이것을 프로덕션에 넣지 않을 것입니다.

아니, 나는 그것을 좋아하지 않았다.

우리가 CTE를 구체화 할 수 있다면 좋을 것입니다. 왜냐하면 그것은 더 잘 SARGable하고 기술적으로 말하면 도움이 될 것이기 때문입니다. 그러나 아닙니다. 우리는 최상위와 동일한 실행 계획을 얻습니다.

/*would be nice if it were sargable*/
WITH    [x] AS ( SELECT
                * ,
                DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2]) AS [ddif]
               FROM
                [#sargme] AS [s])
     SELECT
        *
     FROM
        [x]
     WHERE
        [x].[ddif] >= 48;

물론 상수를 사용하지 않기 때문에이 코드는 아무것도 변경하지 않으며 SARGable의 절반도 아닙니다. 재미 없어. 동일한 실행 계획.

/*not even half sargable*/
SELECT
    * ,
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
    [#sargme] AS [s]
WHERE
    [s].[DateCol2] >= DATEADD(DAY, 48, [s].[DateCol1])

운이 좋으면 연결 문자열의 모든 ANSI SET 옵션을 준수하는 경우 계산 열을 추가하고 검색 할 수 있습니다 ...

ALTER TABLE [#sargme] ADD [ddiff] AS 
DATEDIFF(DAY, DateCol1, DateCol2) PERSISTED

CREATE NONCLUSTERED INDEX [ix_dates2] ON [#sargme] ([ddiff], [DateCol1], [DateCol2])

SELECT [s].[ID] ,
       [s].[DateCol1] ,
       [s].[DateCol2]
FROM [#sargme] AS [s]
WHERE [ddiff] >= 48

이렇게하면 세 가지 쿼리로 인덱스를 찾을 수 있습니다. 이상한 사람은 DateCol1에 48 일을 추가하는 곳입니다. 와 쿼리 DATEDIFFWHERE절은 CTE계산 된 컬럼에 대한 술어, 최종 쿼리는 모두 당신에게 훨씬 좋네요 추정치 훨씬 더 좋은 계획을주고, 모든 것을.

나는 이것과 함께 살 수 있었다.

하나의 질문 으로이 검색을 수행 할 수있는 SARGable 방법이 있습니까?

임시 테이블, 테이블 변수, 테이블 구조 변경 및 뷰가 없습니다.

자체 조인, CTE, 하위 쿼리 또는 데이터를 여러 번 통과하는 것이 좋습니다. 모든 버전의 SQL Server에서 작동 할 수 있습니다.

계산 열을 피하는 것은 인공적인 한계입니다. 다른 것보다 쿼리 솔루션에 더 관심이 있기 때문입니다.

답변:


16

이것을 빨리 추가하면 답변으로 존재합니다 (원하는 답변이 아님).

인덱스 계산 열은 일반적으로 이러한 종류의 문제에 대한 최적의 솔루션이다.

그것:

  • 술어를 색인 가능한 표현식으로 만듭니다.
  • 더 나은 카디널리티 추정을 위해 자동 통계를 작성할 수 있습니다.
  • 기본 테이블에서 공간을 차지할 필요 가 없습니다.

마지막 지점을 명확히하기 위해이 경우 계산 열을 유지 하지 않아도됩니다 .

-- Note: not PERSISTED, metadata change only
ALTER TABLE #sargme
ADD DayDiff AS DATEDIFF(DAY, DateCol1, DateCol2);

-- Index the expression
CREATE NONCLUSTERED INDEX index_name
ON #sargme (DayDiff)
INCLUDE (DateCol1, DateCol2);

이제 쿼리 :

SELECT
    S.ID,
    S.DateCol1,
    S.DateCol2,
    DATEDIFF(DAY, S.DateCol1, S.DateCol2)
FROM
    #sargme AS S
WHERE
    DATEDIFF(DAY, S.DateCol1, S.DateCol2) >= 48;

... 다음과 같은 간단한 계획을 제시합니다.

실행 계획

Martin Smith가 말했듯이 잘못된 설정 옵션을 사용하여 연결하는 경우 일반 열을 만들고 트리거를 사용하여 계산 된 값을 유지할 수 있습니다.

물론 Aaron이 대답 에서 알 수 있듯이 해결해야 할 실제 문제가있는 경우이 모든 것이 실제로 중요합니다 (코드 문제는 제외) .

이것은 생각하기에 재미 있지만 질문에 제약이 주어지면 합리적으로 원하는 것을 달성 할 수있는 방법을 모르겠습니다. 최적의 솔루션에는 어떤 유형의 새로운 데이터 구조가 필요할 것 같습니다. 가장 근접한 것은 위와 같이 비 지속적 계산 열의 인덱스에 의해 제공된 '함수 인덱스'근사치입니다.


12

SQL Server 커뮤니티에서 가장 큰 이름 중 일부에서 비웃을 위험에 처해, 나는 목을 내밀어 말할 것입니다.

쿼리가 SARGable이 되려면 기본적으로 인덱스 의 연속 행 범위 에서 시작 행을 찾아 낼 수있는 쿼리를 구성해야합니다 . index를 사용하면 및 ix_dates의 날짜 차이로 행이 정렬되지 않으므로 대상 행이 인덱스의 어느 곳에 나 퍼질 수 있습니다.DateCol1DateCol2

자체 조인, 다중 패스 등은 모두 하나 이상의 인덱스 스캔을 포함한다는 공통점을 가지고 있지만 (중첩 루프) 조인은 인덱스 탐색을 잘 사용할 수 있습니다. 그러나 스캔을 제거하는 방법을 알 수 없습니다.

보다 정확한 행 추정치를 얻으려면 날짜 차이에 대한 통계가 없습니다.

다음의 상당히 추악한 재귀 CTE 구문은 중첩 루프 조인과 잠재적으로 매우 많은 수의 인덱스 탐색을 소개하지만 전체 테이블 스캔을 기술적으로 제거합니다.

DECLARE @from date, @count int;
SELECT TOP 1 @from=DateCol1 FROM #sargme ORDER BY DateCol1;
SELECT TOP 1 @count=DATEDIFF(day, @from, DateCol1) FROM #sargme WHERE DateCol1<=DATEADD(day, -48, {d '9999-12-31'}) ORDER BY DateCol1 DESC;

WITH cte AS (
    SELECT 0 AS i UNION ALL
    SELECT i+1 FROM cte WHERE i<@count)

SELECT b.*
FROM cte AS a
INNER JOIN #sargme AS b ON
    b.DateCol1=DATEADD(day, a.i, @from) AND
    b.DateCol2>=DATEADD(day, 48+a.i, @from)
OPTION (MAXRECURSION 0);

이는 매를 포함하는 색인 생성 스풀 DateCol1표는 다음 인덱스가 그 각각 (범위 검색) 탐색 수행 DateCol1하고 DateCol2순방향있는 최소 48 일입니다.

더 많은 IO, 약간 더 긴 실행 시간, 행 추정치가 여전히 멀어지고 재귀로 인해 병렬화 가능성이 없습니다. DateCol1(탐색 수 유지).

미친 재귀 CTE 쿼리 계획


9

나는 엉뚱한 변형을 시도했지만 하나의 버전보다 더 나은 버전을 찾지 못했습니다. 주요 문제는 date1과 date2가 함께 정렬되는 방식에서 색인이 다음과 같다는 것입니다. 첫 번째 열은 멋진 선반 라인에있을 것이며 열 사이의 간격은 매우 들쭉날쭉합니다. 실제 방법보다 깔때기처럼 보이기를 원합니다.

Date1    Date2
-----    -------
*             *
*             *
*              *
 *       * 
 *        *
 *         *
  *      *
  *           *

두 점 사이의 특정 델타 (또는 델타 범위)에 대해 탐색 할 수있는 방법은 실제로 없습니다. 그리고 한 번 실행되는 단일 검색 + 모든 행에 대해 실행되는 검색이 아닌 범위 스캔을 의미합니다. 그것은 어느 시점에서 스캔 및 / 또는 정렬을 포함 할 것이고, 이들은 분명히 피하고 싶은 것들입니다. 필터링 된 인덱스에서 DATEADD/ 와 같은 표현식을 사용 DATEDIFF하거나 날짜 diff의 제품을 정렬 할 수있는 스키마 수정을 수행 할 수 없습니다 (삽입 / 업데이트시 델타 계산). 실제로 이것은 스캔이 실제로 최적의 검색 방법 인 경우 중 하나 인 것 같습니다.

이 쿼리는 재미 있지 않다고 말했지만 자세히 살펴보면 가장 좋은 쿼리입니다 (컴퓨 트 스칼라 출력을 생략하면 더 나을 것입니다).

SELECT
    * ,
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
    [#sargme] AS [s]
WHERE
    [s].[DateCol2] >= DATEADD(DAY, 48, [s].[DateCol1])

그 이유는 피하는 것입니다 DATEDIFF가능성에 대한 계산에 비해 약간의 CPU를 면도 에만 인덱스의 비를 선도하는 키 열, 또한에 성가신 암시 적 변환을 방지 datetimeoffset(7)(사람들이이 있지만, 그들은 왜 나 한테 물어하지 않습니다). DATEDIFF버전 은 다음과 같습니다 .

<조건 자>
<ScalarOperator ScalarString = "datediff (day, CONVERT_IMPLICIT (datetimeoffset (7), [splunge]. [dbo]. [sargme]. [DateCol1] as [s]. [DateCol1], 0), CONVERT_IMPLICIT (datetimeoffset ()) 7), [튀김]. [dbo]. [sargme]. [DateCol2]를 [s]. [DateCol2], 0))> = (48) ">

그리고 여기없는 것입니다 DATEDIFF:

<Predicate>
<ScalarOperator ScalarString = "[splunge]. [dbo]. [sargme]. [DateCol2] as [s]. [DateCol2]> = dateadd (day, (48), [splunge]. [dbo]. [ sargme]. [DateCol1] as [s]. [DateCol1]) ">

또한 인덱스 만 포함으로 변경했을 때의 지속 시간 측면에서 약간 더 나은 결과를 찾았습니다. DateCol2두 인덱스가 모두 존재하는 경우 SQL Server는 항상 하나의 키와 하나의 포함 열 대 다중 키를 가진 것을 선택했습니다. 이 쿼리의 경우 범위를 찾기 위해 모든 행을 스캔해야하므로 두 번째 날짜 열을 키의 일부로 사용하여 어떤 방식 으로든 정렬하면 이점이 없습니다. 그리고 우리가 여기서 구할 수 없다는 것을 알고 있지만 , 주요 키 열에 대해 계산을 강요하고 2 차 또는 포함 열에 대해서만 계산하는 기능을 방해 하지 않는 것이 본질적으로 좋은 느낌 입니다.

그것이 나 였고, sargable 솔루션을 찾는 것을 포기했다면 SQL Server가 가장 적은 양의 작업을 수행하는 델타가 거의 없어도 선택할 수있는 솔루션을 알고 있습니다. 또는 스키마 변경 등에 대한 제한을 완화하는 것이 좋습니다.

그리고 그 모든 것이 얼마나 중요합니까? 모르겠어요 나는 테이블을 천만 행으로 만들었고 위의 모든 쿼리 변형은 여전히 ​​1 초 안에 완료되었습니다. 그리고 이것은 랩톱의 VM (SSD와 함께 부여됨)에 있습니다.


3

sarg-able WHERE 절을 복잡하게 만들고 인덱스 추구를 목표로 삼는 것이 수단이 아닌 최종 목표로 생각되는 모든 방법. 그래서, 나는 그것이 (실용적으로) 가능하다고 생각하지 않습니다.

"테이블 구조 변경 없음"에 추가 인덱스가 없는지 확실하지 않았습니다. 다음은 인덱스 스캔을 완전히 피하지만 많은 개별 인덱스 탐색을 발생시키는 솔루션입니다. 즉 , 테이블의 최소 / 최대 날짜 값 범위에서 가능한 각 DateCol1 날짜 마다 하나씩입니다 . (다니엘과 달리 실제로 테이블에 나타나는 각각의 별개의 날짜를 찾는 경우가 있습니다). 이론적으로 병렬 처리 b / c의 후보이며 재귀를 피합니다. 그러나 솔직히 말해서 DATEDIFF를 스캔하고 수행하는 것보다이 작업이 더 빠른 데이터 배포를보기는 어렵습니다. (아마도 DOP가 높을까요?) 그리고 ... 코드는 못 생겼습니다. 이 노력은 "정신 운동"으로 간주됩니다.

--Add this index to avoid the scan when determining the @MaxDate value
--CREATE NONCLUSTERED INDEX [ix_dates2] ON [#sargme] ([DateCol2]);
DECLARE @MinDate DATE, @MaxDate DATE;
SELECT @MinDate=DateCol1 FROM (SELECT TOP 1 DateCol1 FROM #sargme ORDER BY DateCol1 ASC) ss;
SELECT @MaxDate=DateCol2 FROM (SELECT TOP 1 DateCol2 FROM #sargme ORDER BY DateCol2 DESC) ss;

--Used 44 just to get a few more rows to test my logic
DECLARE @DateDiffSearchValue INT = 44, 
    @MinMaxDifference INT = DATEDIFF(DAY, @MinDate, @MaxDate);

--basic data profile in the table
SELECT [MinDate] = @MinDate, 
        [MaxDate] = @MaxDate, 
        [MinMaxDifference] = @MinMaxDifference, 
        [LastDate1SearchValue] = DATEADD(DAY, 0-@DateDiffSearchValue, @MaxDate);

;WITH rn_base AS (
SELECT [col1] = 0
        UNION ALL SELECT 0
        UNION ALL SELECT 0
        UNION ALL SELECT 0
),
rn_1 AS (
    SELECT t0.col1 FROM rn_base t0
        CROSS JOIN rn_base t1
        CROSS JOIN rn_base t2
        CROSS JOIN rn_base t3
),
rn_2 AS (
    SELECT rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
    FROM rn_1 t0
        CROSS JOIN rn_1 t1
),
candidate_searches AS (
    SELECT 
        [Date1_EqualitySearch] = DATEADD(DAY, t.rn-1, @MinDate),
        [Date2_RangeSearch] = DATEADD(DAY, t.rn-1+@DateDiffSearchValue, @MinDate)
    FROM rn_2 t
    WHERE DATEADD(DAY, t.rn-1, @MinDate) <= DATEADD(DAY, 0-@DateDiffSearchValue, @MaxDate)
    /* Of course, ignore row-number values that would result in a
       Date1_EqualitySearch value that is < @DateDiffSearchValue days before @MaxDate */
)
--select * from candidate_searches

SELECT c.*, xapp.*, dd_rows = DATEDIFF(DAY, xapp.DateCol1, xapp.DateCol2)
FROM candidate_searches c
    cross apply (
        SELECT t.*
        FROM #sargme t
        WHERE t.DateCol1 = c.date1_equalitysearch
        AND t.DateCol2 >= c.date2_rangesearch
    ) xapp
ORDER BY xapp.ID asc --xapp.DateCol1, xapp.DateCol2 

3

커뮤니티 위키 답변은 원래 질문에 대한 편집으로 질문 작성자가 추가했습니다.

이것을 조금 앉아 있고, 똑똑한 사람들이 떠오른 후에, 이것에 대한 나의 초기 생각은 정확 해 보입니다. 트리거합니다.

나는 몇 가지 다른 것들을 시도했고, 읽는 사람에게 흥미 롭거나 흥미롭지 않을 수있는 다른 관찰 결과를 가지고 있습니다.

먼저 임시 테이블이 아닌 일반 테이블을 사용하여 설정을 다시 실행하십시오.

  • 나는 그들의 명성을 알고 있지만 여러 열 통계를 시도하고 싶었습니다. 그들은 쓸모가 없었습니다.
  • 사용 된 통계를보고 싶었습니다.

새로운 설정은 다음과 같습니다.

USE [tempdb]
SET NOCOUNT ON  

DBCC FREEPROCCACHE

IF OBJECT_ID('tempdb..sargme') IS NOT NULL
BEGIN
DROP TABLE sargme
END

SELECT TOP 1000
IDENTITY (BIGINT, 1,1) AS ID,
CAST(DATEADD(DAY, [m].[severity] * -1, GETDATE()) AS DATE) AS [DateCol1],
CAST(DATEADD(DAY, [m].[severity], GETDATE()) AS DATE) AS [DateCol2]
INTO sargme
FROM sys.[messages] AS [m]

ALTER TABLE [sargme] ADD CONSTRAINT [pk_whatever] PRIMARY KEY CLUSTERED ([ID])
CREATE NONCLUSTERED INDEX [ix_dates] ON [sargme] ([DateCol1], [DateCol2])

CREATE STATISTICS [s_sargme] ON [sargme] ([DateCol1], [DateCol2])

그런 다음 첫 번째 쿼리를 실행하면 ix_dates 인덱스를 사용하여 이전과 마찬가지로 스캔합니다. 여기에 변화가 없습니다. 이것은 중복 된 것처럼 보이지만 나와 붙어 있습니다.

SELECT
    * ,
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
    [sargme] AS [s]
WHERE
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2]) >= 48

CTE 쿼리를 다시 실행하십시오.

WITH    [x] AS ( SELECT
                * ,
                DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2]) AS [ddif]
               FROM
                [sargme] AS [s])
     SELECT
        *
     FROM
        [x]
     WHERE
        [x].[ddif] >= 48;

좋구나! 1/2이 아닌 Sargable 쿼리를 다시 실행하십시오.

SELECT
    * ,
    DATEDIFF(DAY, [s].[DateCol1], [s].[DateCol2])
FROM
    [sargme] AS [s]
WHERE
    [s].[DateCol2] >= DATEADD(DAY, 48, [s].[DateCol1])

이제 계산 열을 추가하고 계산 열에 맞는 쿼리와 함께 세 개 모두를 다시 실행하십시오.

ALTER TABLE [sargme] ADD [ddiff] AS 
DATEDIFF(DAY, DateCol1, DateCol2) PERSISTED

CREATE NONCLUSTERED INDEX [ix_dates2] ON [sargme] ([ddiff], [DateCol1], [DateCol2])

SELECT [s].[ID] ,
       [s].[DateCol1] ,
       [s].[DateCol2]
FROM [sargme] AS [s]
WHERE [ddiff] >= 48

당신이 여기에 붙어 있다면, 감사합니다. 이것은 게시물의 흥미로운 관찰 부분입니다.

Fabiano Amorim 이 문서화하지 않은 추적 플래그를 사용하여 쿼리를 실행하여 각 쿼리에 사용 된 통계가 매우 멋진 지 확인하십시오. 계산 열이 만들어지고 색인이 생성 될 때까지 통계 개체를 터치 한 계획없는 것을 확인했습니다 .

혈흔

도대체 계산 열에 만 도달 한 쿼리조차도 몇 번 실행하고 간단한 매개 변수화를 얻을 때까지 통계 개체를 만지지 않았습니다. 따라서 모두 처음에 ix_dates 색인을 스캔했지만 사용 가능한 통계 오브젝트 대신 하드 코딩 된 카디널리티 추정값 (테이블의 30 %)을 사용했습니다.

여기에 눈썹을 올린 또 다른 요점은 비 클러스터형 인덱스 만 추가하면 쿼리가 두 날짜 열 모두에 비 클러스터형 인덱스를 사용하지 않고 모든 HEAP를 스캔한다는 계획입니다.

응답 한 모든 사람에게 감사합니다. 당신은 모두 훌륭합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.