win-loss-tieデータからストリークカウントとストリークタイプを取得する


15

誰にとっても物事が簡単になる場合は、この質問に対してSQL Fiddleを作成しました。

ある種のファンタジースポーツデータベースがあり、私が理解しようとしているのは、「現在のストリーク」データ(チームが最後の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ではなく、トライステート(例:Home Win / Tie / Away Winを意味する)列を使用します。DBがチェックする必要がある制約が1つ少なくなります。
AakashM

それで、私がセットアップしたデザインは「良い」と言っているのですか?
ジャマス

1
コメントを求められたら、1)なぜ多くの名前で「ファンタジー」なのか2)なぜbigint多くの列でintなぜ3なのか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各チームと試合を計算します。

C2streak_typeによって順序付けられた前のものを見つけmatch_id descます。

C3が最後の値と同じである限りstreak_summatch_id desc保持する0ことによって順序付けられたランニングサムを生成しstreak_typeます。

筋アップメインクエリの合計streak_sumです0


4
+1を使用する場合LEAD()。2012年の新しいウィンドウ関数について十分な知識がない人
マークシンキンソン

4
+ 1、LAGで降順を使用して、最後のストリークを後で決定するトリックがとても気に入っています!ちなみに、OPはチームIDのみを必要とするためFantasyTeams JOIN FantasyMatches、置き換えてFantasyMatches CROSS APPLY (VALUES (home_fantasy_team_id), (away_fantasy_team_id))パフォーマンスを向上させることができます。
アンドリーM

@AndriyM良いキャッチ!! それで答えを更新します。他の列が必要な場合FantasyTeamsは、代わりにメインクエリに参加することをお勧めします。
ミカエルエリクソン

このコード例に感謝します-これを試してみます。会議を終えてから少し後に報告します...>:-\
jamauss

@MikaelEriksson-これは素晴らしい作品です-ありがとう!簡単な質問-この結果セットを使用して既存の行を更新する必要があります(FantasyTeams.team_idに参加)-これをUPDATEステートメントに変換することをどのようにお勧めしますか?SELECTをUPDATEに変更しようとしましたが、UPDATEでGROUP BYを使用できません。結果セットを一時テーブルに投げて、それに対してUPDATEまたは他の何かに結合するだけでいいと言いますか?ありがとう!
ジャマス

10

この問題を解決する1つの直観的なアプローチは次のとおりです。

  1. 各チームの最新の結果を見つける
  2. 前の一致を確認し、結果のタイプが一致する場合は、ストリークカウントに1を追加します
  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各ストリークを形成するsのリストを出力に簡単に追加できます。

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 ...)justの代わりに使用している特別な理由はありますStreaks.streak_type = CASE ...か?私はあなたの両側にはNULLなどの値と一致する必要がある場合、前者の方法が有用であることを知っていますが、右の部分は、そう...この場合には任意のNULLを作り出すことができるかのようではありません
アンドリー・M

2
@AndriyMはい、あります。コードは、ソートなしで計画を作成するための多くの場所と方法で非常に慎重に書かれています。ときにCASE使用され、オプティマイザは、(組合キーの順序を保持)マージ連結を使用することができず、代わりに、連結プラスの種類を使用しています。
ポール・ホワイト・ライステート・モニカ

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デモ


この回答のおかげで、問題に対する複数の解決策を見つけて、2つのパフォーマンスを比較できてうれしいです。
ジャマス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.