Получите количество полос и тип полос на основе данных побед-поражений

15

Я сделал SQL-скрипку для этого вопроса, если это облегчает жизнь кому-либо.

У меня есть своего рода база данных по фэнтези-спорту, и я пытаюсь выяснить, как получить данные о «текущей серии» (например, «W2», если команда выиграла последние 2 матча, или «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
)

Значение NULLвwinning_team_id столбце указывает на связь для этого совпадения.

Вот пример заявления DML с некоторыми примерами данных для 6 команд и матчей за 3 недели:

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 для каждой команды каждую неделю. Есть идеи, как мне это сделать?

jamauss
источник
2
Между прочим, все такие схемы, которые я когда-либо видел, используют столбец с тремя состояниями (например, 1 2 3, означающий Home Win / Tie / Away Win) для результата матча, а не ваш win_team_id со значением id / NULL / id. Еще одно ограничение для БД, которое нужно проверить.
AakashM
Так вы говорите, что я настроил дизайн "хорошо"?
Джамаусс
1
Ну, если бы меня попросили прокомментировать, я бы сказал: 1) почему «фэнтези» в столь многих именах 2) почему bigintдля такого количества столбцов, где int, вероятно, будет 3), почему все _s ?! 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 Fiddle

C1 рассчитывает streak_type для каждой команды и матча.

C2находит предыдущий streak_typeзаказанныйmatch_id desc .

C3генерирует промежуточную сумму, streak_sumупорядоченную, match_id descсохраняя a 0long, поскольку streak_typeзначение совпадает с последним значением.

Основной запрос суммирует полосы, где streak_sumесть 0.

Микаэль Эрикссон
источник
4
+1 за использование LEAD(). Недостаточно людей знают о новых оконных функциях в 2012 году
Марк Синкинсон
4
+1, мне нравится хитрость использования нисходящего порядка в LAG, чтобы позже определить последнюю серию, очень аккуратно! Кстати, так как OP хочет только идентификаторы команд, вы можете заменить FantasyTeams JOIN FantasyMatchesс FantasyMatches CROSS APPLY (VALUES (home_fantasy_team_id), (away_fantasy_team_id))и , таким образом , потенциально повысить производительность.
Андрей М
@AndriyM Хороший улов !! Я обновлю ответ с этим. Если вам нужны другие столбцы, FantasyTeamsлучше присоединиться к основному запросу.
Микаэль Эрикссон
Спасибо за этот пример кода - я собираюсь дать ему попытку и
сообщу
@MikaelEriksson - Это прекрасно работает - спасибо! Быстрый вопрос - мне нужно использовать этот набор результатов для обновления существующих строк (присоединение к FantasyTeams.team_id). Как бы вы порекомендовали превратить это в оператор UPDATE? Я начал пытаться просто изменить SELECT на UPDATE, но я не могу использовать GROUP BY в UPDATE. Вы сказали бы, что я должен просто выбросить результирующий набор во временную таблицу и объединиться с этим в ОБНОВЛЕНИЕ или что-то еще? Благодарность!
Джамаусс
10

Один интуитивный подход к решению этой проблемы:

  1. Найти самый последний результат для каждой команды
  2. Проверьте предыдущее совпадение и добавьте его к числу строк, если тип результата соответствует
  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 здесь ):

-- 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_ids, которые формируют каждую последовательность, в вывод:

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
Впечатляет! Есть ли конкретная причина, почему ваша рекурсивная часть ГДЕ использует EXISTS (... INTERSECT ...)вместо просто Streaks.streak_type = CASE ...? Я знаю, что первый метод может быть полезен, когда вам нужно сопоставить значения NULL на обеих сторонах, а также значения, но это не так, как если бы правая часть могла генерировать любые значения NULL в этом случае, так что ...
Андрей М
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 demo

Serpiton
источник
спасибо за этот ответ, приятно видеть более одного решения проблемы и иметь возможность сравнить производительность между ними.
Джамаусс