Uzyskaj liczbę i rodzaj serii z danych wygranych remis-remis

15

Zrobiłem skrzypek SQL dla tego pytania, jeśli to ułatwia każdemu.

Mam bazę danych o sportach fantasy i próbuję dowiedzieć się, jak wymyślić dane o „bieżącej serii” (np. „W2”, jeśli drużyna wygrała ostatnie 2 pojedynki, lub „L1”, jeśli przegrali) ich ostatni pojedynek po wygraniu poprzedniego pojedynku - lub „T1”, jeśli remisowali ostatni mecz).

Oto mój podstawowy schemat:

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
)

Wartość NULLw winning_team_idkolumnie wskazuje remis dla tego dopasowania.

Oto przykładowa instrukcja DML z niektórymi przykładowymi danymi dla 6 drużyn i 3 tygodniowych pojedynków:

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

Oto przykład pożądanego wyniku (na podstawie powyższego DML), z którym mam problem, nawet zaczynam wymyślać, jak uzyskać:

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

Próbowałem różnych metod przy użyciu podkwerend i CTE, ale nie mogę tego połączyć. Chciałbym uniknąć używania kursora, ponieważ mógłbym mieć duży zestaw danych, aby uruchomić to w przyszłości. Wydaje mi się, że może istnieć sposób angażowania zmiennych tabeli, które w jakiś sposób łączą te dane z sobą, ale wciąż nad tym pracuję.

Informacje dodatkowe: Może być różna liczba drużyn (dowolna liczba parzysta od 6 do 10), a łączna liczba pojedynków wzrośnie o 1 dla każdej drużyny co tydzień. Wszelkie pomysły, jak to zrobić?

jamauss
źródło
2
Nawiasem mówiąc, wszystkie takie schematy, jakie kiedykolwiek widziałem, używają kolumny wyniku trójki (np. 1 2 3, oznaczającej wygraną gospodarzy / remis / wygraną gości) zamiast wyniku meczu, a nie twojej_numeru_grupy_wysokiej o wartości id / NULL / id. Jedno mniejsze ograniczenie do sprawdzenia przez DB.
AakashM
Mówisz, że projekt, który konfiguruję, jest „dobry”?
jamauss
1
Cóż, jeśli poproszę o komentarze, powiedziałbym: 1) dlaczego „fantazja” w tak wielu nazwach 2) dlaczego bigintdla tylu kolumn, gdzie intprawdopodobnie by to zrobił 3) dlaczego wszystkie te _?! 4) Wolę, aby nazwy tabel były pojedyncze, ale potwierdzam, że nie wszyscy się ze mną zgadzają // ale te poza tym, co tu pokazałeś, wyglądają spójnie, tak
AakashM

Odpowiedzi:

17

Ponieważ korzystasz z programu SQL Server 2012, możesz użyć kilku nowych funkcji okienkowania.

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

C1oblicza streak_typedla każdej drużyny i meczu.

C2znajduje poprzednie streak_typeuporządkowane przez match_id desc.

C3generuje sumę bieżącą streak_sumuporządkowaną, match_id desczachowując 0długi, ponieważ streak_typejest taki sam jak ostatnia wartość.

Główne zapytanie podsumowuje serie, gdzie streak_sumjest 0.

Mikael Eriksson
źródło
4
+1 za użycie LEAD(). Niewiele osób wie o nowych funkcjach okienkowania w 2012 r.
Mark Sinkinson
4
+1, podoba mi się sztuczka polegająca na użyciu malejącej kolejności w LGD, aby później ustalić ostatnią serię, bardzo porządnie! Nawiasem mówiąc, ponieważ PO chce tylko identyfikatory zespołu, można wymienić FantasyTeams JOIN FantasyMatchesz FantasyMatches CROSS APPLY (VALUES (home_fantasy_team_id), (away_fantasy_team_id))a tym samym potencjalnie zwiększyć wydajność.
Andriy M
@AndriyM Dobry połów !! Zaktualizuję odpowiedź o to. Jeśli potrzebujesz innych kolumn FantasyTeams, prawdopodobnie lepiej zamiast tego dołączyć do głównego zapytania.
Mikael Eriksson
Dziękuję za ten przykład kodu - spróbuję i poinformuję o tym później, kiedy nie będę już miał spotkań ...>: - \
jamauss
@MikaelEriksson - To działa świetnie - dzięki! Szybkie pytanie - muszę użyć tego zestawu wyników, aby zaktualizować istniejące wiersze (dołączenie do FantasyTeams.team_id) - Jak poleciłbyś zamienić to w instrukcję UPDATE? Zacząłem próbować po prostu zmienić SELECT na UPDATE, ale nie mogę użyć GROUP BY w UPDATE. Czy powiedziałbyś, że powinienem po prostu rzucić zestaw wyników do tabeli tymczasowej i dołączyć do tego, aby zaktualizować lub coś innego? Dzięki!
jamauss
10

Jednym z intuicyjnych sposobów rozwiązania tego problemu jest:

  1. Znajdź najnowszy wynik dla każdej drużyny
  2. Sprawdź poprzednie dopasowanie i dodaj jeden do liczby serii, jeśli typ wyniku jest zgodny
  3. Powtórz krok 2, ale zatrzymaj się, gdy tylko pojawi się pierwszy inny wynik

Strategia ta może wygrać z rozwiązaniem funkcji okna (które wykonuje pełne skanowanie danych) w miarę powiększania się tabeli, przy założeniu, że strategia rekurencyjna jest skutecznie wdrażana. Kluczem do sukcesu jest zapewnienie wydajnych indeksów w celu szybkiego zlokalizowania wierszy (za pomocą wyszukiwań) i uniknięcia sortowania. Potrzebne indeksy to:

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

Aby pomóc w optymalizacji zapytań, użyję tabeli tymczasowej do przechowywania wierszy określonych jako część bieżącej serii. Jeśli smugi są zazwyczaj krótkie (jak to jest w przypadku zespołów, które śledzę, niestety), ta tabela powinna być dość mał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);

Moje rozwiązanie do zapytań rekurencyjnych jest następujące ( tutaj Fiddle SQL ):

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

Tekst T-SQL jest dość długi, ale każda sekcja zapytania ściśle odpowiada ogólnemu zarysowi procesu podanemu na początku tej odpowiedzi. Kwerenda jest dłuższa z powodu potrzeby użycia pewnych sztuczek, aby uniknąć sortowania i wygenerowania TOPrekurencyjnej części zapytania (co zwykle nie jest dozwolone).

Plan wykonania jest stosunkowo niewielki i prosty w porównaniu z zapytaniem. Zacieniowałem obszar zakotwiczenia na żółto, a część rekurencyjną na zielonym zrzucie ekranu:

Plan wykonania rekurencyjnego

Dzięki zarejestrowaniu wierszy pasm w tabeli tymczasowej łatwo jest uzyskać wymagane podsumowanie wyników. (Użycie tabeli tymczasowej pozwala również uniknąć wycieku sortowania, który mógłby wystąpić, gdyby poniższe zapytanie zostało połączone z głównym zapytaniem rekurencyjnym)

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

Podstawowy plan wykonania zapytania

To samo zapytanie może być wykorzystane jako podstawa do aktualizacji FantasyTeamstabeli:

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

Lub, jeśli wolisz 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;

Każde z tych podejść tworzy efektywny plan wykonania (na podstawie znanej liczby wierszy w tabeli tymczasowej):

Zaktualizuj plan wykonania

Wreszcie, ponieważ metoda rekurencyjna naturalnie obejmuje match_idprzetwarzanie, łatwo jest dodać listę wyników match_idtworzących każdą serię do wyniku:

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;

Wynik:

Dołączona lista meczów

Plan wykonania:

Plan wykonania listy dopasowań

Paul White 9
źródło
2
Imponujący! Czy istnieje konkretny powód, dla którego twoja część rekurencyjna GDZIE używa EXISTS (... INTERSECT ...)zamiast po prostu Streaks.streak_type = CASE ...? Wiem, że poprzednia metoda może być przydatna, gdy trzeba dopasować wartości NULL po obu stronach, a także wartości, ale nie jest tak, jakby odpowiednia część mogła w tym przypadku wygenerować wartości NULL, więc ...
Andriy M
2
@AndriyM Tak jest. Kod jest bardzo starannie napisany w wielu miejscach i sposobach, aby stworzyć plan bez żadnych rodzajów. Gdy CASEjest używany, optymalizator nie może użyć konkatenacji scalającej (która zachowuje porządek klucza unii) i zamiast tego używa konkatenacji plus sortowania.
Paul White 9
8

Innym sposobem na uzyskanie wyniku jest rekurencyjne 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

Demo SQLFiddle

Serpiton
źródło
dzięki za tę odpowiedź, miło jest zobaczyć więcej niż jedno rozwiązanie problemu i móc porównać wydajność między nimi.
jamauss