win-loss-tie 데이터에서 연속 카운트 및 연속 유형 가져 오기


15

내가 만든 SQL 바이올린을 그 누구보다 쉽게 일을 만드는 경우이 질문에 대해.

나는 일종의 판타지 스포츠 데이터베이스를 가지고 있고 내가 알아 내려고하는 것은 "현재 행진"데이터를 만드는 방법입니다 (팀이 마지막 2 경기에서 승리 한 경우 'W2'또는 잃어버린 경우 'L1') 이전 경기에서 승리 한 후 마지막 경기-또는 최근 경기에 묶인 경우 'T1')

기본 스키마는 다음과 같습니다.

CREATE TABLE FantasyTeams (
  team_id BIGINT NOT NULL
)

CREATE TABLE FantasyMatches(
    match_id BIGINT NOT NULL,
    home_fantasy_team_id BIGINT NOT NULL,
    away_fantasy_team_id BIGINT NOT NULL,
    fantasy_season_id BIGINT NOT NULL,
    fantasy_league_id BIGINT NOT NULL,
    fantasy_week_id BIGINT NOT NULL,
    winning_team_id BIGINT NULL
)

NULLwinning_team_id열은 일치 넥타이를 나타낸다.

다음은 6 개 팀과 3 주 분량의 매치업에 대한 샘플 데이터가 포함 된 샘플 DML 문입니다.

INSERT INTO FantasyTeams
SELECT 1
UNION
SELECT 2
UNION
SELECT 3
UNION
SELECT 4
UNION
SELECT 5
UNION
SELECT 6

INSERT INTO FantasyMatches
SELECT 1, 2, 1, 2, 4, 44, 2
UNION
SELECT 2, 5, 4, 2, 4, 44, 5
UNION
SELECT 3, 6, 3, 2, 4, 44, 3
UNION
SELECT 4, 2, 4, 2, 4, 45, 2
UNION
SELECT 5, 3, 1, 2, 4, 45, 3
UNION
SELECT 6, 6, 5, 2, 4, 45, 6
UNION
SELECT 7, 2, 6, 2, 4, 46, 2
UNION
SELECT 8, 3, 5, 2, 4, 46, 3
UNION
SELECT 9, 4, 1, 2, 4, 46, NULL

GO

다음은 원하는 출력의 예입니다 (위의 DML을 기반으로 함). 유도 방법을 알아 내기조차 시작하지 못했습니다.

| TEAM_ID | STEAK_TYPE | STREAK_COUNT |
|---------|------------|--------------|
|       1 |          T |            1 |
|       2 |          W |            3 |
|       3 |          W |            3 |
|       4 |          T |            1 |
|       5 |          L |            2 |
|       6 |          L |            1 |

하위 쿼리와 CTE를 사용하여 다양한 방법을 시도했지만 함께 사용할 수 없습니다. 나중에 이것을 실행하기 위해 큰 데이터 세트를 가질 수 있으므로 커서 사용을 피하고 싶습니다. 어떻게 든이 데이터를 조인하는 테이블 변수와 관련된 방법이있을 수 있지만 여전히 작업 중입니다.

추가 정보 : 다양한 수의 팀이있을 수 있으며 (6과 10 사이의 짝수) 매 경기마다 총 경기가 1 씩 증가합니다. 내가 어떻게 해야하는지에 대한 아이디어가 있습니까?


2
우연히도, 내가 본 모든 스키마는 id / NULL / id 값을 가진 winning_team_id 대신 일치 결과에 3 가지 상태 (예 : Home Win / Tie / Away Win을 의미하는 1 2 3) 열을 사용합니다. DB가 확인해야 할 제약이 하나 줄었습니다.
AakashM

내가 설정 한 디자인이 "좋다"고 말하는가?
jamauss

1
글쎄, 내가 의견을 묻는다면 나는 1) 왜 많은 이름에서 '판타지'를 말할 것입니까? 2) 왜 bigint그렇게 많은 열을 int가질까요? 3) 왜 모든 것 _입니까?! 4) 나는 테이블 이름이 단수 인 것을 선호하지만 모든 사람이 나와 동의하지는 않는다는 것을 인정하지만 // 여기에 우리가 보여준 것을 옆에 둔 사람들은 일관된 것처럼 보입니다.
AakashM

답변:


17

SQL Server 2012를 사용하므로 몇 가지 새로운 윈도우 기능을 사용할 수 있습니다.

with C1 as
(
  select T.team_id,
         case
           when M.winning_team_id is null then 'T'
           when M.winning_team_id = T.team_id then 'W'
           else 'L'
         end as streak_type,
         M.match_id
  from FantasyMatches as M
    cross apply (values(M.home_fantasy_team_id),
                       (M.away_fantasy_team_id)) as T(team_id)
), C2 as
(
  select C1.team_id,
         C1.streak_type,
         C1.match_id,
         lag(C1.streak_type, 1, C1.streak_type) 
           over(partition by C1.team_id 
                order by C1.match_id desc) as lag_streak_type
  from C1
), C3 as
(
  select C2.team_id,
         C2.streak_type,
         sum(case when C2.lag_streak_type = C2.streak_type then 0 else 1 end) 
           over(partition by C2.team_id 
                order by C2.match_id desc rows unbounded preceding) as streak_sum
  from C2
)
select C3.team_id,
       C3.streak_type,
       count(*) as streak_count
from C3
where C3.streak_sum = 0
group by C3.team_id,
         C3.streak_type
order by C3.team_id;

SQL 바이올린

C1streak_type각 팀과 경기를 계산합니다 .

C2로 이전 streak_type주문을 찾습니다 match_id desc.

C3가 마지막 값과 같으면 long streak_summatch_id desc유지하여 순서대로 누계를 생성 합니다.0streak_type

줄무늬까지 메인 쿼리의 합계 streak_sum입니다 0.


4
의 사용에 대한 일 LEAD(). 충분한 사람들은 2012 년 새로운 윈도우의 기능에 대해 알고하지 않습니다
마크 Sinkinson

4
+1, LAG에서 내림차순을 사용하여 나중에 마지막 행진을 결정하는 트릭이 매우 좋습니다! 그런데 OP는 팀 ID 만 원 하므로 대체 FantasyTeams JOIN FantasyMatches하여 FantasyMatches CROSS APPLY (VALUES (home_fantasy_team_id), (away_fantasy_team_id))성능을 향상시킬 수 있습니다.
Andriy M

@AndriyM 잘 잡아라! 그 대답을 업데이트하겠습니다. 다른 열이 필요한 경우 FantasyTeams주 쿼리에 조인하는 것이 좋습니다.
Mikael Eriksson

이 코드 예제에 감사드립니다-이 기능을 사용 해보고 회의가 끝난 후 조금 후에 다시보고 할 것입니다 ...> :-\
jamauss

@MikaelEriksson-이것은 잘 작동합니다-감사합니다! 빠른 질문-이 결과 집합을 사용하여 기존 행을 업데이트해야합니다 (FantasyTeams.team_id에 참여)-이것을 UPDATE 문으로 바꾸는 것이 좋습니다? SELECT를 UPDATE로 변경하려고 시도했지만 UPDATE에서 GROUP BY를 사용할 수 없습니다. 결과 세트를 임시 테이블에 던져서 UPDATE 또는 다른 것에 조인해야한다고 말 하시겠습니까? 감사!
jamauss

10

이 문제를 해결하기위한 직관적 인 접근 방식은 다음과 같습니다.

  1. 각 팀의 최신 결과 찾기
  2. 결과 유형이 일치하면 이전 일치 항목을 확인하고 연속 수에 하나를 추가하십시오.
  3. 2 단계를 반복하되 첫 번째 결과가 나오면 즉시 중지

이 전략은 재귀 전략이 효율적으로 구현된다고 가정 할 때 테이블이 커질수록 창 함수 솔루션 (데이터의 전체 스캔을 수행)을 능가 할 수 있습니다. 성공의 열쇠는 효율적으로 색인을 제공하여 행을 빠르게 찾고 (시도를 사용하여) 정렬을 피하는 것입니다. 필요한 색인은 다음과 같습니다.

-- New index #1
CREATE UNIQUE INDEX uq1 ON dbo.FantasyMatches 
    (home_fantasy_team_id, match_id) 
INCLUDE (winning_team_id);

-- New index #2
CREATE UNIQUE INDEX uq2 ON dbo.FantasyMatches 
    (away_fantasy_team_id, match_id) 
INCLUDE (winning_team_id);

쿼리 최적화를 돕기 위해 임시 테이블을 사용하여 현재 행진의 일부를 형성하는 것으로 식별 된 행을 보유합니다. 줄무늬가 일반적으로 짧으면 (따라서 내가 따르는 팀의 경우와 마찬가지로)이 표는 매우 작아야합니다.

-- Table to hold just the rows that form streaks
CREATE TABLE #StreakData
(
    team_id bigint NOT NULL,
    match_id bigint NOT NULL,
    streak_type char(1) NOT NULL,
    streak_length integer NOT NULL,
);

-- Temporary table unique clustered index
CREATE UNIQUE CLUSTERED INDEX cuq ON #StreakData (team_id, match_id);

내 재귀 쿼리 솔루션은 다음과 같습니다 ( SQL Fiddle here ).

-- Solution query
WITH Streaks AS
(
    -- Anchor: most recent match for each team
    SELECT 
        FT.team_id, 
        CA.match_id, 
        CA.streak_type, 
        streak_length = 1
    FROM dbo.FantasyTeams AS FT
    CROSS APPLY
    (
        -- Most recent match
        SELECT
            T.match_id,
            T.streak_type
        FROM 
        (
            SELECT 
                FM.match_id, 
                streak_type =
                    CASE 
                        WHEN FM.winning_team_id = FM.home_fantasy_team_id
                            THEN CONVERT(char(1), 'W')
                        WHEN FM.winning_team_id IS NULL
                            THEN CONVERT(char(1), 'T')
                        ELSE CONVERT(char(1), 'L')
                    END
            FROM dbo.FantasyMatches AS FM
            WHERE 
                FT.team_id = FM.home_fantasy_team_id
            UNION ALL
            SELECT 
                FM.match_id, 
                streak_type =
                    CASE 
                        WHEN FM.winning_team_id = FM.away_fantasy_team_id
                            THEN CONVERT(char(1), 'W')
                        WHEN FM.winning_team_id IS NULL
                            THEN CONVERT(char(1), 'T')
                        ELSE CONVERT(char(1), 'L')
                    END
            FROM dbo.FantasyMatches AS FM
            WHERE
                FT.team_id = FM.away_fantasy_team_id
        ) AS T
        ORDER BY 
            T.match_id DESC
            OFFSET 0 ROWS 
            FETCH FIRST 1 ROW ONLY
    ) AS CA
    UNION ALL
    -- Recursive part: prior match with the same streak type
    SELECT 
        Streaks.team_id, 
        LastMatch.match_id, 
        Streaks.streak_type, 
        Streaks.streak_length + 1
    FROM Streaks
    CROSS APPLY
    (
        -- Most recent prior match
        SELECT 
            Numbered.match_id, 
            Numbered.winning_team_id, 
            Numbered.team_id
        FROM
        (
            -- Assign a row number
            SELECT
                PreviousMatches.match_id,
                PreviousMatches.winning_team_id,
                PreviousMatches.team_id, 
                rn = ROW_NUMBER() OVER (
                    ORDER BY PreviousMatches.match_id DESC)
            FROM
            (
                -- Prior match as home or away team
                SELECT 
                    FM.match_id, 
                    FM.winning_team_id, 
                    team_id = FM.home_fantasy_team_id
                FROM dbo.FantasyMatches AS FM
                WHERE 
                    FM.home_fantasy_team_id = Streaks.team_id
                    AND FM.match_id < Streaks.match_id
                UNION ALL
                SELECT 
                    FM.match_id, 
                    FM.winning_team_id, 
                    team_id = FM.away_fantasy_team_id
                FROM dbo.FantasyMatches AS FM
                WHERE 
                    FM.away_fantasy_team_id = Streaks.team_id
                    AND FM.match_id < Streaks.match_id
            ) AS PreviousMatches
        ) AS Numbered
        -- Most recent
        WHERE 
            Numbered.rn = 1
    ) AS LastMatch
    -- Check the streak type matches
    WHERE EXISTS
    (
        SELECT 
            Streaks.streak_type
        INTERSECT
        SELECT 
            CASE 
                WHEN LastMatch.winning_team_id IS NULL THEN 'T' 
                WHEN LastMatch.winning_team_id = LastMatch.team_id THEN 'W' 
                ELSE 'L' 
            END
    )
)
INSERT #StreakData
    (team_id, match_id, streak_type, streak_length)
SELECT
    team_id,
    match_id,
    streak_type,
    streak_length
FROM Streaks
OPTION (MAXRECURSION 0);

T-SQL 텍스트는 상당히 길지만 쿼리의 각 섹션은이 답변의 시작 부분에 제공된 광범위한 프로세스 개요와 밀접한 관련이 있습니다. 정렬을 피하고 TOP쿼리의 재귀 부분 을 생성하기 위해 특정 트릭을 사용해야하므로 쿼리가 더 오래 걸립니다 (일반적으로 허용되지 않음).

실행 계획은 쿼리와 비교하여 비교적 작고 간단합니다. 아래의 스크린 샷에서 앵커 영역을 노란색으로 표시하고 재귀 부분을 녹색으로 표시했습니다.

재귀 실행 계획

임시 테이블에서 행 행을 캡처하면 필요한 요약 결과를 쉽게 얻을 수 있습니다. (임시 테이블을 사용하면 아래 쿼리가 기본 재귀 쿼리와 결합 된 경우 발생할 수있는 정렬 유출이 방지됩니다)

-- Basic results
SELECT
    SD.team_id,
    StreakType = MAX(SD.streak_type),
    StreakLength = MAX(SD.streak_length)
FROM #StreakData AS SD
GROUP BY 
    SD.team_id
ORDER BY
    SD.team_id;

기본 쿼리 실행 계획

동일한 쿼리를 FantasyTeams테이블 업데이트의 기초로 사용할 수 있습니다 .

-- Update team summary
WITH StreakData AS
(
    SELECT
        SD.team_id,
        StreakType = MAX(SD.streak_type),
        StreakLength = MAX(SD.streak_length)
    FROM #StreakData AS SD
    GROUP BY 
        SD.team_id
)
UPDATE FT
SET streak_type = SD.StreakType,
    streak_count = SD.StreakLength
FROM StreakData AS SD
JOIN dbo.FantasyTeams AS FT
    ON FT.team_id = SD.team_id;

또는 원하는 경우 MERGE:

MERGE dbo.FantasyTeams AS FT
USING
(
    SELECT
        SD.team_id,
        StreakType = MAX(SD.streak_type),
        StreakLength = MAX(SD.streak_length)
    FROM #StreakData AS SD
    GROUP BY 
        SD.team_id
) AS StreakData
    ON StreakData.team_id = FT.team_id
WHEN MATCHED THEN UPDATE SET
    FT.streak_type = StreakData.StreakType,
    FT.streak_count = StreakData.StreakLength;

두 가지 접근 방식 중 하나는 효율적인 실행 계획을 생성합니다 (임시 테이블의 알려진 행 수 기준).

실행 계획 업데이트

마지막으로 재귀 적 인 방법 match_id은 처리에 자연스럽게 포함하기 때문에 match_id각 행진을 형성 하는 의 목록을 출력에 쉽게 추가 할 수 있습니다 .

SELECT
    S.team_id,
    streak_type = MAX(S.streak_type),
    match_id_list =
        STUFF(
        (
            SELECT ',' + CONVERT(varchar(11), S2.match_id)
            FROM #StreakData AS S2
            WHERE S2.team_id = S.team_id
            ORDER BY S2.match_id DESC
            FOR XML PATH ('')
        ), 1, 1, ''),
    streak_length = MAX(S.streak_length)
FROM #StreakData AS S
GROUP BY 
    S.team_id
ORDER BY
    S.team_id;

산출:

경기 목록 포함

실행 계획 :

경기 목록 실행 계획


2
감동적인! 재귀 부분의 WHERE가 EXISTS (... INTERSECT ...)대신 사용되는 특별한 이유가 Streaks.streak_type = CASE ...있습니까? 이전 방법은 값뿐만 아니라 양쪽에서 NULL을 일치시켜야 할 때 유용 할 수 있지만,이 경우 오른쪽 부분이 NULL을 생성 할 수있는 것처럼 보이지는 않습니다.
Andriy M

2
@AndriyM 네 있습니다. 이 코드는 여러 가지 방법으로 계획을 작성하는 여러 장소와 방법으로 매우 신중하게 작성되었습니다. 때 CASE사용, 최적화는 (노조 키 순서 유지) 대신에 연결 플러스 종류를 사용하는 병합 연결을 사용할 수 없습니다.
폴 화이트 9

8

결과를 얻는 또 다른 방법은 재귀 CTE에 의한 것입니다.

WITH TeamRes As (
SELECT FT.Team_ID
     , FM.match_id
     , Previous_Match = LAG(match_id, 1, 0) 
                        OVER (PARTITION BY FT.Team_ID ORDER BY FM.match_id)
     , Matches = Row_Number() 
                 OVER (PARTITION BY FT.Team_ID ORDER BY FM.match_id Desc)
     , Result = Case Coalesce(winning_team_id, -1)
                     When -1 Then 'T'
                     When FT.Team_ID Then 'W'
                     Else 'L'
                End 
FROM   FantasyMatches FM
       INNER JOIN FantasyTeams FT ON FT.Team_ID IN 
         (FM.home_fantasy_team_id, FM.away_fantasy_team_id)
), Streaks AS (
SELECT Team_ID, Result, 1 As Streak, Previous_Match
FROM   TeamRes
WHERE  Matches = 1
UNION ALL
SELECT tr.Team_ID, tr.Result, Streak + 1, tr.Previous_Match
FROM   TeamRes tr
       INNER JOIN Streaks s ON tr.Team_ID = s.Team_ID 
                           AND tr.Match_id = s.Previous_Match 
                           AND tr.Result = s.Result
)
Select Team_ID, Result, Max(Streak) Streak
From   Streaks
Group By Team_ID, Result
Order By Team_ID

SQLFiddle 데모


이 답변 덕분에 문제에 대한 하나 이상의 솔루션을보고 두 가지 성능을 비교할 수있는 것이 좋습니다.
jamauss
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.