Ottieni il conteggio delle serie e il tipo di serie dai dati di vincita vincita


15

Ho creato un violino SQL per questa domanda se ciò rende le cose più facili per chiunque.

Ho una specie di database di sport di fantasia e quello che sto cercando di capire è come elaborare i dati della "striscia corrente" (come "W2" se la squadra ha vinto i suoi ultimi 2 matchup, o "L1" se ha perso il loro ultimo matchup dopo aver vinto il matchup precedente - o 'T1' se hanno legato il matchup più recente).

Ecco il mio schema di 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
)

Un valore NULLnella winning_team_idcolonna indica un pareggio per quella corrispondenza.

Ecco una dichiarazione DML di esempio con alcuni dati di esempio per 6 squadre e 3 settimane di matchup:

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

Ecco un esempio dell'output desiderato (basato sul DML sopra) che sto riscontrando problemi anche iniziando a capire come derivare:

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

Ho provato vari metodi usando subquery e CTE ma non riesco a metterli insieme. Vorrei evitare di usare un cursore in quanto potrei avere un set di dati di grandi dimensioni per eseguire questo in futuro. Sento che potrebbe esserci un modo per coinvolgere le variabili di tabella che uniscono questi dati a se stessi in qualche modo, ma ci sto ancora lavorando.

Informazioni aggiuntive: potrebbe esserci un numero variabile di squadre (qualsiasi numero pari compreso tra 6 e 10) e gli incontri totali aumenteranno di 1 per ogni squadra ogni settimana. Qualche idea su come dovrei farlo?


2
Per inciso, tutti questi schemi che ho mai visto usano una colonna tristata (ad es. 1 2 3 che significa vittoria in casa / pareggio / vittoria in trasferta) per il risultato della partita, piuttosto che il tuo vincitore_team_id con valore id / NULL / id. Un vincolo in meno per il DB da verificare.
AakashM,

Quindi stai dicendo che il design che ho impostato è "buono"?
jamauss,

1
Bene, se mi chiedessero commenti direi: 1) perché "fantasy" in così tanti nomi 2) perché bigintper così tante colonne dove intprobabilmente farebbe 3) perché tutti gli _s ?! 4) Preferisco che i nomi delle tabelle siano singolari ma riconosco che non tutti sono d'accordo con me // ma quelli a parte ciò che ci hai mostrato qui sembrano coerenti, sì
AakashM

Risposte:


17

Dato che sei su SQL Server 2012, puoi utilizzare un paio di nuove funzioni per finestre.

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

C1calcola il streak_typeper ogni squadra e partita.

C2trova il precedente streak_typeordinato da match_id desc.

C3genera una somma corrente streak_sumordinata match_id descmantenendo 0un valore lungostreak_type è uguale all'ultimo valore.

Le principali somme di query su per le striature in cui streak_sumè 0.


4
+1 per l'uso di LEAD(). Non abbastanza persone conoscono le nuove funzioni di finestre nel 2012
Mark Sinkinson,

4
+1, mi piace il trucco di usare l'ordine decrescente in GAL per determinare in seguito l'ultima sequenza, molto pulita! Tra l'altro, dal momento che l'OP vuole solo gli ID di squadra, si potrebbe sostituire FantasyTeams JOIN FantasyMatchescon FantasyMatches CROSS APPLY (VALUES (home_fantasy_team_id), (away_fantasy_team_id))e quindi, potenzialmente, migliorare le prestazioni.
Andriy M,

@AndriyM Buona cattura !! Aggiornerò la risposta con quello. Se hai bisogno di altre colonne da FantasyTeamsesso, probabilmente è meglio unirti alla query principale.
Mikael Eriksson

Grazie per questo esempio di codice - Proverò questo e lo riporterò un po 'più tardi dopo che avrò finito le riunioni ...>: - \
jamauss

@MikaelEriksson - Funziona benissimo - grazie! Domanda rapida: devo utilizzare questo set di risultati per aggiornare le righe esistenti (unendomi a FantasyTeams.team_id) - Come consiglieresti di trasformarlo in un'istruzione UPDATE? Ho iniziato a provare a cambiare SELECT in un AGGIORNAMENTO ma non riesco a utilizzare GROUP BY in un AGGIORNAMENTO. Diresti che dovrei semplicemente buttare il set di risultati in una tabella temporanea e unirmi a quello con AGGIORNAMENTO o qualcos'altro? Grazie!
jamauss

10

Un approccio intuitivo per risolvere questo problema è:

  1. Trova il risultato più recente per ogni squadra
  2. Controllare la corrispondenza precedente e aggiungerne una al conteggio delle strisce se il tipo di risultato corrisponde
  3. Ripetere il passaggio 2 ma interrompere non appena si incontra il primo risultato diverso

Questa strategia potrebbe prevalere sulla soluzione della funzione finestra (che esegue una scansione completa dei dati) man mano che la tabella diventa più grande, supponendo che la strategia ricorsiva sia implementata in modo efficiente. La chiave del successo è fornire indici efficienti per individuare rapidamente le righe (usando le ricerche) ed evitare ordinamenti. Gli indici necessari sono:

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

Per facilitare l'ottimizzazione delle query, userò una tabella temporanea per contenere le righe identificate come parte di una sequenza corrente. Se le strisce sono in genere brevi (come è vero per le squadre che seguo, purtroppo) questa tabella dovrebbe essere piuttosto piccola:

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

La mia soluzione di query ricorsiva è la seguente ( qui SQL Fiddle ):

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

Il testo T-SQL è piuttosto lungo, ma ogni sezione della query corrisponde da vicino alla struttura generale del processo fornita all'inizio di questa risposta. La query è resa più lunga dalla necessità di utilizzare alcuni trucchi per evitare ordinamenti e produrre un TOPnella parte ricorsiva della query (che normalmente non è consentita).

Il piano di esecuzione è relativamente piccolo e semplice rispetto alla query. Ho ombreggiato la regione dell'ancora in giallo e la parte ricorsiva in verde nello screenshot seguente:

Piano di esecuzione ricorsivo

Con le righe di sequenza acquisite in una tabella temporanea, è facile ottenere i risultati di riepilogo richiesti. (L'uso di una tabella temporanea evita anche una fuoriuscita di ordinamento che potrebbe verificarsi se la query seguente fosse combinata con la query ricorsiva 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;

Piano di esecuzione della query di base

La stessa query può essere utilizzata come base per l'aggiornamento della FantasyTeamstabella:

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

Oppure, se preferisci 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;

Entrambi gli approcci producono un piano di esecuzione efficiente (basato sul numero noto di righe nella tabella temporanea):

Aggiorna piano di esecuzione

Infine, poiché il metodo ricorsivo include naturalmente il match_idnella sua elaborazione, è facile aggiungere un elenco delle match_ids che formano ogni serie all'output:

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;

Produzione:

Elenco delle partite incluso

Progetto esecutivo:

Piano di esecuzione dell'elenco delle partite


2
Degno di nota! C'è un motivo particolare per cui la parte ricorsiva che DOVE sta usando EXISTS (... INTERSECT ...)invece di solo Streaks.streak_type = CASE ...? So che il primo metodo può essere utile quando è necessario abbinare NULL su entrambi i lati e valori, ma non è come se la parte giusta potesse produrre NULL in questo caso, quindi ...
Andriy M

2
@AndriyM Sì, c'è. Il codice è scritto con molta attenzione in diversi punti e modi per produrre un piano senza sorta. Quando CASEviene utilizzato, l'ottimizzatore non è in grado di utilizzare una concatenazione di tipo merge (che preserva l'ordine delle chiavi di unione) e utilizza invece una concatenazione più ordinamenti.
Paul White Ripristina Monica

8

Un altro modo per ottenere il risultato è un CTE ricorsivo

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

Demo di SQLFiddle


grazie per questa risposta, è bello vedere più di una soluzione al problema ed essere in grado di confrontare le prestazioni tra i due.
jamauss,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.