Ermitteln Sie die Streifenanzahl und den Streifentyp anhand der Daten zum Gewinn-Verlust-Gleichstand

15

Ich habe eine SQL-Geige für diese Frage erstellt, wenn dies für jemanden einfacher ist.

Ich habe eine Art Fantasy-Sportdatenbank und ich versuche herauszufinden, wie ich mit "aktuellen Streak" -Daten aufwarten kann (wie "W2", wenn die Mannschaft die letzten zwei Spiele gewonnen hat, oder "L1", wenn sie verloren hat ihre letzte Begegnung nach dem Gewinn der vorherigen Begegnung (oder 'T1', wenn sie ihre letzte Begegnung verloren haben).

Hier ist mein Grundschema:

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
)

Ein Wert NULLin der winning_team_idSpalte zeigt ein Unentschieden für diese Übereinstimmung an.

Hier ist eine Beispiel-DML-Anweisung mit einigen Beispieldaten für 6 Teams und Matchups im Wert von 3 Wochen:

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

Hier ist ein Beispiel für die gewünschte Ausgabe (basierend auf der obigen DML), bei der ich Probleme habe, selbst wenn ich anfange herauszufinden, wie ich sie ableiten soll:

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

Ich habe verschiedene Methoden mit Unterabfragen und CTE's ausprobiert, aber ich kann es nicht zusammensetzen. Ich möchte die Verwendung eines Cursors vermeiden, da ich in Zukunft ein großes Dataset haben könnte, mit dem dies ausgeführt werden kann. Ich habe das Gefühl, dass es eine Möglichkeit geben könnte, Tabellenvariablen einzubeziehen, die diese Daten irgendwie mit sich selbst verbinden, aber ich arbeite noch daran.

Zusätzliche Informationen: Es kann eine unterschiedliche Anzahl von Teams geben (jede gerade Anzahl zwischen 6 und 10), und die Gesamtzahl der Matchups erhöht sich jede Woche um 1 pro Team. Irgendwelche Ideen, wie ich das machen soll?

Marmeladen
quelle
2
Übrigens verwenden alle diese Schemata, die ich je gesehen habe, eine Tristate-Spalte (z. B. 1 2 3 für Heimsieg / Unentschieden / Auswärtssieg) für das Match-Ergebnis und nicht Ihre winning_team_id mit dem Wert id / NULL / id. Eine Einschränkung weniger, die der DB prüfen muss.
AakashM
Wollen Sie damit sagen, dass das von mir eingerichtete Design "gut" ist?
Jamauss
1
Nun, wenn ich um Kommentare gebeten werde, würde ich sagen: 1) warum "Fantasie" in so vielen Namen 2) warum bigintfür so viele Spalten, wo intwürde das wohl gehen 3) warum all die _s ?! 4) Ich bevorzuge Tabellennamen als Singular,
nehme

Antworten:

17

Da Sie mit SQL Server 2012 arbeiten, können Sie einige der neuen Fensterfunktionen verwenden.

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

C1berechnet die streak_typefür jede Mannschaft und jedes Spiel.

C2findet die vorherige streak_typegeordnet nach match_id desc.

C3Erzeugt eine laufende Summe, streak_sumdie durch match_id descBeibehalten eines 0langen Zeichens, das streak_typedem letzten Wert entspricht, bestellt wird.

Die Hauptabfrage fasst die Streifen zusammen, in denen sie sich streak_sumbefinden 0.

Mikael Eriksson
quelle
4
+1 für die Verwendung von LEAD(). Nicht genug Leute wissen über die neuen
Fensterfunktionen
4
+1, ich mag den Trick, die absteigende Reihenfolge in der LAG zu verwenden, um später den letzten Streifen zu bestimmen, sehr ordentlich! By the way, da nur die OP - Team - IDs will, könnten Sie ersetzen FantasyTeams JOIN FantasyMatchesmit FantasyMatches CROSS APPLY (VALUES (home_fantasy_team_id), (away_fantasy_team_id))und damit potenziell die Leistung verbessern.
Andriy M
@AndriyM Guter Fang !! Ich werde die Antwort damit aktualisieren. Wenn Sie andere Spalten benötigen FantasyTeams, ist es wahrscheinlich besser, stattdessen an der Hauptabfrage teilzunehmen.
Mikael Eriksson
Vielen Dank für dieses Codebeispiel. Ich werde es versuchen und etwas später berichten,
wenn
@MikaelEriksson - Das funktioniert super - danke! Kurze Frage - Ich muss diese Ergebnismenge verwenden, um vorhandene Zeilen zu aktualisieren (Beitritt zu FantasyTeams.team_id). Wie würden Sie empfehlen, dies in eine UPDATE-Anweisung umzuwandeln? Ich habe versucht, SELECT einfach in UPDATE zu ändern, aber ich kann GROUP BY nicht in UPDATE verwenden. Würden Sie sagen, ich sollte die Ergebnismenge einfach in eine temporäre Tabelle werfen und dagegen UPDATE oder etwas anderes hinzufügen? Vielen Dank!
Jamauss
10

Ein intuitiver Ansatz zur Lösung dieses Problems ist:

  1. Finden Sie das aktuellste Ergebnis für jedes Team
  2. Überprüfen Sie die vorherige Übereinstimmung und addieren Sie eine zur Streifenanzahl, wenn der Ergebnistyp übereinstimmt
  3. Wiederholen Sie Schritt 2, hören Sie jedoch auf, sobald das erste unterschiedliche Ergebnis festgestellt wird

Diese Strategie könnte sich gegenüber der Fensterfunktionslösung (die einen vollständigen Scan der Daten durchführt) durchsetzen, wenn die Tabelle größer wird, vorausgesetzt, die rekursive Strategie wird effizient implementiert. Der Schlüssel zum Erfolg besteht darin, effiziente Indizes bereitzustellen, um Zeilen schnell zu finden (mithilfe von Suchen) und Sortierungen zu vermeiden. Die benötigten Indizes sind:

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

Um die Abfrageoptimierung zu unterstützen, verwende ich eine temporäre Tabelle, um Zeilen zu speichern, die als Teil eines aktuellen Strips identifiziert wurden. Wenn die Streifen normalerweise kurz sind (was leider für die Mannschaften gilt, denen ich folge), sollte diese Tabelle recht klein sein:

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

Meine Lösung für rekursive Abfragen lautet wie folgt ( SQL Fiddle hier ):

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

Der T-SQL-Text ist ziemlich lang, aber jeder Abschnitt der Abfrage entspricht genau dem allgemeinen Ablauf, der zu Beginn dieser Antwort angegeben wurde. Die Abfrage wird länger, da bestimmte Tricks verwendet werden müssen, um Sortierungen zu vermeiden und einen TOPrekursiven Teil der Abfrage zu erzeugen (was normalerweise nicht zulässig ist).

Der Ausführungsplan ist im Vergleich zur Abfrage relativ klein und einfach. Ich habe den Ankerbereich gelb und den rekursiven Teil grün im folgenden Screenshot schattiert:

Rekursiver Ausführungsplan

Mit den in einer temporären Tabelle erfassten Streifenzeilen ist es einfach, die gewünschten Zusammenfassungsergebnisse zu erhalten. (Durch die Verwendung einer temporären Tabelle wird auch ein Sortierungsverlust vermieden, der auftreten kann, wenn die folgende Abfrage mit der rekursiven Hauptabfrage kombiniert wird.)

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

Grundlegender Ausführungsplan für Abfragen

Dieselbe Abfrage kann als Grundlage für die Aktualisierung der FantasyTeamsTabelle verwendet werden:

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

Oder wenn Sie es vorziehen 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;

Bei beiden Ansätzen wird ein effizienter Ausführungsplan erstellt (basierend auf der bekannten Anzahl von Zeilen in der temporären Tabelle):

Ausführungsplan aktualisieren

Schließlich match_idist es einfach, eine Liste der match_ids, die jeden Streifen bilden, zur Ausgabe hinzuzufügen , da die rekursive Methode natürlich die in ihrer Verarbeitung enthält :

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;

Ausgabe:

Matchliste enthalten

Ausführungsplan:

Ausführungsplan der Match-Liste

Paul White Monica wieder einsetzen
quelle
2
Beeindruckend! Gibt es einen bestimmten Grund, warum das WHERE Ihres rekursiven Teils EXISTS (... INTERSECT ...)anstelle von nur verwendet wird Streaks.streak_type = CASE ...? Ich weiß, dass die erste Methode nützlich sein kann, wenn Sie NULL-Werte auf beiden Seiten sowie Werte abgleichen müssen, aber es ist nicht so, als ob der richtige Teil in diesem Fall NULL-Werte erzeugen könnte, also ...
Andriy M
2
@AndriyM Ja, das gibt es. Der Code ist sehr sorgfältig an einer Reihe von Stellen und auf verschiedene Arten geschrieben, um einen Plan ohne Sortierung zu erstellen. Wenn CASEverwendet, kann das Optimierungsprogramm keine Zusammenführungsverkettung verwenden (die die Reihenfolge der Unionsschlüssel beibehält) und verwendet stattdessen eine Verkettung mit mehreren Sortierungen.
Paul White setzt Monica wieder ein
8

Ein anderer Weg, um das Ergebnis zu erhalten, ist ein rekursiver 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
quelle
danke für diese antwort, es ist schön, mehr als eine lösung für das problem zu sehen und in der lage zu sein, die leistung zwischen den beiden zu vergleichen.
Jamauss