Obtenga el recuento de rachas y el tipo de racha a partir de los datos de empate ganar-perder

15

Hice un violín de SQL para esta pregunta si eso facilita las cosas a cualquiera.

Tengo una especie de base de datos de deportes de fantasía y lo que estoy tratando de descubrir es cómo obtener datos de "racha actual" (como 'W2' si el equipo ha ganado sus últimos 2 enfrentamientos, o 'L1' si perdieron su último enfrentamiento después de ganar el enfrentamiento anterior, o 'T1' si empataron su enfrentamiento más reciente).

Aquí está mi 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
)

Un valor de NULLen la winning_team_idcolumna indica un empate para esa coincidencia.

Aquí hay una declaración DML de muestra con algunos datos de muestra para 6 equipos y 3 semanas de enfrentamientos:

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

Aquí hay un ejemplo de la salida deseada (basada en el DML anterior) que tengo problemas incluso para comenzar a descubrir cómo 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 |

He probado varios métodos usando subconsultas y CTE, pero no puedo armarlo. Me gustaría evitar usar un cursor ya que podría tener un gran conjunto de datos para ejecutar esto en el futuro. Siento que podría haber una forma de involucrar variables de tabla que unan estos datos de alguna manera, pero todavía estoy trabajando en ello.

Información adicional: podría haber un número variable de equipos (cualquier número par entre 6 y 10) y el total de enfrentamientos aumentará en 1 para cada equipo cada semana. ¿Alguna idea sobre cómo debo hacer esto?

jamauss
fuente
2
Por cierto, todos los esquemas que he visto utilizan una columna tristada (por ejemplo, 1 2 3 que significa victoria en casa / empate / visitante) para el resultado del partido, en lugar de su ganador_team_id con valor id / NULL / id. Una restricción menos para que el DB tenga que verificar.
AakashM
Entonces, ¿estás diciendo que el diseño que configuré es "bueno"?
jamauss
1
Bueno, si me piden comentarios, diría: 1) ¿por qué 'fantasía' en tantos nombres 2) por qué bigintpara tantas columnas donde intprobablemente harían 3) por qué todos los _s ?! 4) Prefiero que los nombres de las tablas sean singulares, pero reconozco que no todos están de acuerdo conmigo // pero aparte de eso, lo que nos han mostrado aquí parece coherente, sí
AakashM

Respuestas:

17

Como está en SQL Server 2012, puede usar un par de nuevas funciones de ventanas.

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;

Violín de SQL

C1calcula el streak_typepara cada equipo y partido.

C2encuentra el anterior streak_typeordenado por match_id desc.

C3genera una suma acumulada streak_sumordenada match_id descmanteniendo un valor 0siempre que streak_typesea ​​el mismo que el último valor.

La consulta principal resume las rayas donde streak_sumestá 0.

Mikael Eriksson
fuente
44
1 para el uso de LEAD(). No hay suficientes personas que conozcan las nuevas funciones de ventanas en 2012
Mark Sinkinson,
44
+1, me gusta el truco de usar el orden descendente en el LAG para luego determinar la última racha, ¡muy bien! Por cierto, ya que el OP sólo quiere identificadores de equipo, se puede reemplazar FantasyTeams JOIN FantasyMatchescon FantasyMatches CROSS APPLY (VALUES (home_fantasy_team_id), (away_fantasy_team_id))y por lo tanto mejorar potencialmente el rendimiento.
Andriy M
@AndriyM ¡Buena captura! Actualizaré la respuesta con eso. Si necesita otras columnas FantasyTeams, probablemente sea mejor unirse a la consulta principal.
Mikael Eriksson
Gracias por este ejemplo de código. Voy a intentarlo e informaré un poco más tarde, después de que no esté en las reuniones ...>: - \
jamauss
@MikaelEriksson - Esto funciona muy bien, ¡gracias! Pregunta rápida: necesito usar este conjunto de resultados para actualizar las filas existentes (unirme en FantasyTeams.team_id). ¿Cómo recomendaría convertir esto en una instrucción UPDATE? Empecé a tratar de cambiar el SELECCIONAR a una ACTUALIZACIÓN pero no puedo usar GROUP BY en una ACTUALIZACIÓN. ¿Diría que debería arrojar el conjunto de resultados en una tabla temporal y unirme a eso para ACTUALIZAR o algo más? ¡Gracias!
jamauss
10

Un enfoque intuitivo para resolver este problema es:

  1. Encuentra el resultado más reciente para cada equipo
  2. Verifique la coincidencia anterior y agregue una al conteo de rachas si el tipo de resultado coincide
  3. Repita el paso 2 pero pare tan pronto como se encuentre el primer resultado diferente

Esta estrategia podría vencer a la solución de función de ventana (que realiza un análisis completo de los datos) a medida que la tabla se hace más grande, suponiendo que la estrategia recursiva se implemente de manera eficiente. La clave del éxito es proporcionar índices eficientes para ubicar filas rápidamente (usando búsquedas) y evitar géneros. Los índices necesarios son:

-- 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 ayudar en la optimización de consultas, usaré una tabla temporal para mantener las filas identificadas como parte de una racha actual. Si las rachas son típicamente cortas (como es cierto para los equipos que sigo, lamentablemente) esta tabla debería ser bastante pequeña:

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

Mi solución de consulta recursiva es la siguiente ( SQL Fiddle aquí ):

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

El texto T-SQL es bastante largo, pero cada sección de la consulta se corresponde estrechamente con el esquema general del proceso dado al comienzo de esta respuesta. La consulta se hace más larga por la necesidad de usar ciertos trucos para evitar géneros y producir un TOPen la parte recursiva de la consulta (que normalmente no está permitido).

El plan de ejecución es relativamente pequeño y simple en comparación con la consulta. He sombreado la región de anclaje en amarillo y la parte recursiva en verde en la captura de pantalla a continuación:

Plan de ejecución recursiva

Con las filas seguidas capturadas en una tabla temporal, es fácil obtener los resultados de resumen que necesita. (El uso de una tabla temporal también evita un derrame de clasificación que podría ocurrir si la consulta a continuación se combinara con la 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;

Plan básico de ejecución de consultas

La misma consulta se puede utilizar como base para actualizar la FantasyTeamstabla:

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

O, si lo prefieres 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;

Cualquiera de los dos enfoques produce un plan de ejecución eficiente (basado en el número conocido de filas en la tabla temporal):

Actualizar plan de ejecución

Finalmente, debido a que el método recursivo naturalmente incluye el match_iden su procesamiento, es fácil agregar una lista de los match_ids que forman cada racha a la salida:

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;

Salida:

Lista de coincidencias incluida

Plan de ejecución:

Plan de ejecución de la lista de coincidencias

Paul White reinstala a Monica
fuente
2
¡Impresionante! ¿Hay alguna razón particular por la cual la parte recursiva de WHERE se está usando en EXISTS (... INTERSECT ...)lugar de solo Streaks.streak_type = CASE ...? Sé que el método anterior puede ser útil cuando necesita unir NULL en ambos lados, así como valores, pero no es como si la parte correcta pudiera producir NULL en este caso, así que ...
Andriy M
2
@AndriyM Sí lo hay. El código está escrito con mucho cuidado en varios lugares y formas de producir un plan sin ningún tipo. Cuando CASEse utiliza, el optimizador no puede utilizar una concatenación de fusión (que conserva el orden de las claves de unión) y utiliza una concatenación más clases en su lugar.
Paul White reinstala a Monica
8

Otra forma de obtener el resultado es mediante un CTE recursivo.

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

Demostración de SQLFiddle

Serpiton
fuente
Gracias por esta respuesta, es bueno ver más de una solución al problema y poder comparar el rendimiento entre los dos.
jamauss