Obter contagem e tipo de sequência a partir de dados de empate / derrota

15

Eu criei um SQL Fiddle para essa pergunta, se isso facilitar as coisas para qualquer pessoa.

Eu tenho um tipo de banco de dados de esportes de fantasia e o que estou tentando descobrir é como criar dados de "sequência atual" (como 'W2' se a equipe venceu seus últimos 2 confrontos, ou 'L1' se eles perderam sua última luta depois de vencer a luta anterior - ou 'T1' se eles empataram a luta mais recente).

Aqui está o meu esquema básico:

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
)

Um valor de NULLna winning_team_idcoluna indica um empate para essa correspondência.

Aqui está um exemplo de instrução DML com alguns dados de exemplo para 6 equipes e 3 semanas de confrontos:

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

Aqui está um exemplo da saída desejada (com base no DML acima) que eu estou tendo problemas para começar a descobrir como derivar:

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

Eu tentei vários métodos usando subconsultas e CTE, mas não consigo montá-lo. Gostaria de evitar o uso de um cursor, pois poderia ter um grande conjunto de dados para executar isso no futuro. Eu sinto que pode haver uma maneira de envolver variáveis ​​de tabela que associam esses dados a si mesmos de alguma forma, mas ainda estou trabalhando nisso.

Informações adicionais: pode haver um número variável de equipes (qualquer número par entre 6 e 10) e o total de confrontos aumentará em 1 para cada equipe a cada semana. Alguma idéia de como devo fazer isso?

jamauss
fonte
2
Aliás, todos esses esquemas que eu já vi usam uma coluna tristate (por exemplo, 1 2 3 significa vitória em casa / empate / vitória fora) para o resultado da partida, em vez de sua win_team_id com o valor id / NULL / id. Uma restrição a menos para o banco de dados precisar verificar.
AakashM
Então você está dizendo que o design que eu configuro é "bom"?
jamauss
1
Bem, se me pedissem comentários, eu diria: 1) por que 'fantasia' em tantos nomes 2) por que bigintpara tantas colunas onde intprovavelmente faria 3) por que todos os _s ?! 4) Eu prefiro nomes de tabela a ser singular, mas reconhecem nem todos concordam comigo // mas aqueles de lado o que você mostrou-nos aqui parece coerente, sim
AakashM

Respostas:

17

Como você está no SQL Server 2012, você pode usar algumas das novas funções de janelas.

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

C1calcula o streak_typepara cada equipe e partida.

C2encontra o anterior streak_typeordenado por match_id desc.

C3gera uma soma em execução streak_sumordenada, match_id descmantendo um 0a enquanto o streak_typeé o mesmo que o último valor.

A consulta principal resume as faixas onde streak_sumestá 0.

Mikael Eriksson
fonte
4
+1 para o uso de LEAD(). Não basta as pessoas sabem sobre as novas funções de janela, em 2012
Mark Sinkinson
4
+1, eu gosto do truque de usar a ordem decrescente no GAL para determinar mais tarde a última sequência, muito elegante! A propósito, como o OP quer apenas IDs de equipe, você pode substituir FantasyTeams JOIN FantasyMatchespor FantasyMatches CROSS APPLY (VALUES (home_fantasy_team_id), (away_fantasy_team_id))e, potencialmente, melhorar o desempenho.
Andriy M
@AndriyM Good catch !! Vou atualizar a resposta com isso. Se você precisar de outras colunas FantasyTeams, provavelmente será melhor ingressar na consulta principal.
Mikael Eriksson
Obrigado por este exemplo de código - vou tentar fazer isso e relatarei um pouco mais tarde depois que estiver fora das reuniões ...>: - \
jamauss
@MikaelEriksson - Isso funciona muito bem - obrigado! Pergunta rápida - Preciso usar esse conjunto de resultados para atualizar as linhas existentes (ingressando no FantasyTeams.team_id) - Como você recomendaria transformar isso em uma instrução UPDATE? Comecei a tentar apenas alterar o SELECT em um UPDATE, mas não posso usar o GROUP BY em um UPDATE. Você diria que eu deveria apenas lançar o conjunto de resultados em uma tabela temporária e me juntar a isso em UPDATE ou algo mais? Obrigado!
jamauss
10

Uma abordagem intuitiva para resolver esse problema é:

  1. Encontre o resultado mais recente para cada equipe
  2. Verifique a correspondência anterior e adicione uma à contagem de sequências se o tipo de resultado corresponder
  3. Repita a etapa 2, mas pare assim que o primeiro resultado diferente for encontrado

Essa estratégia pode vencer a solução da função de janela (que executa uma varredura completa dos dados) à medida que a tabela aumenta, assumindo que a estratégia recursiva seja implementada com eficiência. A chave para o sucesso é fornecer índices eficientes para localizar linhas rapidamente (usando pesquisas) e evitar classificações. Os índices necessários são:

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

Para ajudar na otimização de consultas, usarei uma tabela temporária para manter as linhas identificadas como parte de uma sequência atual. Se as faixas são geralmente curtas (como é verdade para as equipes que sigo, infelizmente), esta tabela deve ser bem pequena:

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

Minha solução de consulta recursiva é a seguinte ( SQL Fiddle aqui ):

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

O texto T-SQL é bastante longo, mas cada seção da consulta corresponde ao esboço geral do processo fornecido no início desta resposta. A consulta é prolongada pela necessidade de usar certos truques para evitar classificações e produzir um TOPna parte recursiva da consulta (o que normalmente não é permitido).

O plano de execução é relativamente pequeno e simples em comparação com a consulta. Sombrei a região da âncora em amarelo e a parte recursiva em verde na captura de tela abaixo:

Plano de execução recursiva

Com as linhas de sequência capturadas em uma tabela temporária, é fácil obter os resultados resumidos necessários. (O uso de uma tabela temporária também evita um derramamento de classificação que pode ocorrer se a consulta abaixo for combinada com a consulta recursiva principal)

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

Plano básico de execução de consultas

A mesma consulta pode ser usada como base para atualizar a FantasyTeamstabela:

-- 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, se você preferir 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;

Qualquer uma das abordagens produz um plano de execução eficiente (com base no número conhecido de linhas na tabela temporária):

Atualizar plano de execução

Finalmente, como o método recursivo inclui naturalmente o match_idprocesso, é fácil adicionar uma lista dos match_ids que formam cada sequência à saída:

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;

Resultado:

Lista de jogos incluída

Plano de execução:

Plano de execução da lista de correspondências

Paul White restabelece Monica
fonte
2
Impressionante! Existe uma razão específica para o uso de WHERE da sua parte recursiva, em EXISTS (... INTERSECT ...)vez de apenas Streaks.streak_type = CASE ...? Eu sei que o primeiro método pode ser útil quando você precisa corresponder nulos em ambos os lados, bem como os valores, mas não é como se a peça certa pode produzir quaisquer nulos, neste caso, então ...
Andriy M
2
@AndriyM Sim, existe. O código é escrito com muito cuidado em vários locais e maneiras de produzir um plano sem classificação. Quando CASEé usado, o otimizador não pode usar uma concatenação de mesclagem (que preserva a ordem das chaves de união) e usa uma concatenação mais classificações.
Paul White Reinstate Monica
8

Outra maneira de obter o resultado é por meio de uma CTE recursiva

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

Demonstração do SQLFiddle

Serpiton
fonte
obrigado por esta resposta, é bom ver mais de uma solução para o problema e poder comparar o desempenho entre as duas.
jamauss