Obtenez le nombre de séquences et le type de séquence à partir des données gagnant-perdant-égalisées

15

J'ai fait un SQL Fiddle pour cette question si cela facilite les choses pour n'importe qui.

J'ai en quelque sorte une base de données sur les sports fantastiques et ce que j'essaie de comprendre, c'est comment trouver des données de "séquence actuelle" (comme 'W2' si l'équipe a remporté ses 2 derniers affrontements, ou 'L1' si elle a perdu leur dernier match après avoir remporté le match précédent - ou «T1» s'ils ont égalé leur dernier match).

Voici mon schéma de base:

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
)

Une valeur de NULLdans la winning_team_idcolonne indique une égalité pour cette correspondance.

Voici un exemple de déclaration DML avec des exemples de données pour 6 équipes et 3 semaines de matchs:

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

Voici un exemple de la sortie souhaitée (basée sur le DML ci-dessus) que j'ai du mal à même commencer à comprendre comment dériver:

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

J'ai essayé différentes méthodes en utilisant des sous-requêtes et des CTE mais je ne peux pas les assembler. Je voudrais éviter d'utiliser un curseur car je pourrais avoir un grand ensemble de données pour exécuter cela contre à l'avenir. J'ai l'impression qu'il pourrait y avoir un moyen impliquant des variables de table qui joignent ces données à lui-même, mais je travaille toujours dessus.

Informations supplémentaires: Il pourrait y avoir un nombre variable d'équipes (n'importe quel nombre pair entre 6 et 10) et le total des affrontements augmentera de 1 pour chaque équipe chaque semaine. Avez-vous des idées sur la façon de procéder?

jamauss
la source
2
Soit dit en passant, tous ces schémas que j'ai jamais vus utilisent une colonne à trois états (par exemple, 1 2 3 signifiant victoire à domicile / égalité / victoire à l'extérieur) pour le résultat du match, plutôt que votre gagnant_équipe_id avec la valeur id / NULL / id. Une contrainte de moins à vérifier pour la base de données.
AakashM
Alors, dites-vous que la conception que j'ai configurée est "bonne"?
jamauss
1
Eh bien, si on me demandait des commentaires, je dirais: 1) pourquoi «fantaisie» dans tant de noms 2) pourquoi bigintpour autant de colonnes où intferait probablement 3) pourquoi tous les _s?! 4) Je préfère que les noms de table soient singuliers, mais je reconnais que tout le monde n'est pas d'accord avec moi // mais ceux à part ce que vous nous avez montré ici semblent cohérents, oui
AakashM

Réponses:

17

Puisque vous êtes sur SQL Server 2012, vous pouvez utiliser quelques-unes des nouvelles fonctions de fenêtrage.

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 Fiddle

C1calcule le streak_typepour chaque équipe et match.

C2trouve le précédent streak_typeordonné par match_id desc.

C3génère une somme cumulée streak_sumordonnée parmatch_id desc gardant a 0aussi longtemps que la streak_typeest la même que la dernière valeur.

La requête principale résume les séquences où se streak_sumtrouve 0.

Mikael Eriksson
la source
4
+1 pour l'utilisation de LEAD(). Pas assez de gens connaissent les nouvelles fonctions de fenêtrage en 2012
Mark Sinkinson
4
+1, j'aime l'astuce d'utiliser l'ordre décroissant dans LAG pour déterminer plus tard la dernière séquence, très nette! Soit dit en passant, puisque l'OP veut que les ID de l' équipe, vous pouvez remplacer FantasyTeams JOIN FantasyMatchesavec FantasyMatches CROSS APPLY (VALUES (home_fantasy_team_id), (away_fantasy_team_id))et donc potentiellement améliorer les performances.
Andriy M
@AndriyM Bonne prise !! Je mettrai à jour la réponse avec cela. Si vous avez besoin d'autres colonnes, FantasyTeamsil est probablement préférable de rejoindre la requête principale à la place.
Mikael Eriksson
Merci pour cet exemple de code - je vais essayer et je vous ferai rapport un peu plus tard après que je sois hors des réunions ...>: - \
jamauss
@MikaelEriksson - Cela fonctionne très bien - merci! Question rapide - J'ai besoin d'utiliser cet ensemble de résultats pour mettre à jour les lignes existantes (rejoindre sur FantasyTeams.team_id) - Comment recommanderiez-vous de transformer cela en une instruction UPDATE? J'ai commencé à essayer de simplement changer le SELECT en UPDATE mais je ne peux pas utiliser le GROUP BY dans une UPDATE. Diriez-vous que je devrais simplement jeter l'ensemble de résultats dans une table temporaire et le joindre à UPDATE ou autre chose? Merci!
jamauss
10

Une approche intuitive pour résoudre ce problème est la suivante:

  1. Trouvez le résultat le plus récent pour chaque équipe
  2. Vérifiez la correspondance précédente et ajoutez-en une au nombre de séquences si le type de résultat correspond
  3. Répétez l'étape 2 mais arrêtez dès que le premier résultat différent est rencontré

Cette stratégie pourrait l'emporter sur la solution de fonction de fenêtre (qui effectue une analyse complète des données) à mesure que la table s'agrandit, en supposant que la stratégie récursive est mise en œuvre efficacement. La clé du succès est de fournir des index efficaces pour localiser rapidement les lignes (à l'aide de recherches) et éviter les tris. Les index nécessaires sont:

-- 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);

Pour aider à l'optimisation des requêtes, j'utiliserai une table temporaire pour contenir les lignes identifiées comme faisant partie d'une séquence en cours. Si les séquences sont généralement courtes (comme c'est malheureusement le cas pour les équipes que je suis), ce tableau devrait être assez petit:

-- 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);

Ma solution de requête récursive est la suivante ( SQL Fiddle ici ):

-- 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);

Le texte T-SQL est assez long, mais chaque section de la requête correspond étroitement au schéma général du processus donné au début de cette réponse. La requête est allongée par la nécessité d’utiliser certaines astuces pour éviter les tris et produire unTOP dans la partie récursive de la requête (ce qui n'est normalement pas autorisé).

Le plan d'exécution est relativement petit et simple par rapport à la requête. J'ai ombré la région d'ancrage en jaune et la partie récursive en vert dans la capture d'écran ci-dessous:

Plan d'exécution récursif

Avec les lignes de séquence capturées dans un tableau temporaire, il est facile d'obtenir les résultats récapitulatifs dont vous avez besoin. (L'utilisation d'une table temporaire évite également un déversement de tri qui pourrait se produire si la requête ci-dessous était combinée avec la requête récursive principale)

-- 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;

Plan d'exécution de requête de base

La même requête peut être utilisée comme base pour la mise à jour de la FantasyTeamstable:

-- 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;

Ou, si vous préférez 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;

L'une ou l'autre approche produit un plan d'exécution efficace (basé sur le nombre connu de lignes dans la table temporaire):

Mettre à jour le plan d'exécution

Enfin, parce que la méthode récursive inclut naturellement le match_iddans son traitement, il est facile d'ajouter une liste des match_ids qui forment chaque séquence à la sortie:

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;

Production:

Liste des matchs incluse

Plan d'exécution:

Plan d'exécution de la liste de correspondance

Paul White 9
la source
2
Impressionnant! Y a-t-il une raison particulière pour laquelle la partie récursive WHERE est utilisée EXISTS (... INTERSECT ...)au lieu de juste Streaks.streak_type = CASE ...? Je sais que l'ancienne méthode peut être utile lorsque vous devez faire correspondre des valeurs NULL des deux côtés ainsi que des valeurs, mais ce n'est pas comme si la bonne partie pouvait produire des valeurs NULL dans ce cas, donc ...
Andriy M,
2
@AndriyM Oui, il y en a. Le code est très soigneusement écrit dans un certain nombre d'endroits et de façons de produire un plan sans tri. Lorsque CASEest utilisé, l'optimiseur ne peut pas utiliser une concaténation de fusion (qui préserve l'ordre des clés d'union) et utilise une concaténation plus des tris à la place.
Paul White 9
8

Une autre façon d'obtenir le résultat est par un CTE récursif

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

Démo SQLFiddle

Serpiton
la source
merci pour cette réponse, il est agréable de voir plus d'une solution au problème et de pouvoir comparer les performances entre les deux.
jamauss