Dapatkan jumlah coretan dan jenis coretan dari data win-loss-tie


15

Saya membuat SQL Fiddle untuk pertanyaan ini jika itu membuat segalanya lebih mudah bagi siapa pun.

Saya memiliki semacam database olahraga fantasi dan apa yang saya coba cari tahu adalah bagaimana menghasilkan data "arus beruntun" (seperti 'W2' jika tim telah memenangkan 2 pertarungan terakhir mereka, atau 'L1' jika mereka kalah pertarungan terakhir mereka setelah memenangkan pertarungan sebelumnya - atau 'T1' jika mereka mengikat pertarungan terbaru mereka).

Ini skema dasar saya:

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
)

Nilai NULLdi winning_team_idkolom menunjukkan dasi untuk pertandingan itu.

Berikut ini contoh pernyataan DML dengan beberapa data sampel untuk 6 tim dan 3 minggu pertandingan:

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

Berikut adalah contoh dari output yang diinginkan (berdasarkan DML di atas) yang saya mengalami kesulitan bahkan mulai mencari cara untuk mendapatkan:

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

Saya sudah mencoba berbagai metode menggunakan subqueries dan CTE tetapi saya tidak bisa menggabungkannya. Saya ingin menghindari menggunakan kursor karena saya dapat memiliki dataset besar untuk menjalankan ini melawan di masa depan. Saya merasa mungkin ada cara yang melibatkan variabel tabel yang menggabungkan data ini ke dirinya sendiri, tetapi saya masih mengerjakannya.

Info Tambahan: Mungkin ada jumlah tim yang bervariasi (angka genap antara 6 dan 10) dan total pertarungan akan meningkat sebesar 1 untuk setiap tim setiap minggu. Ada ide tentang bagaimana saya harus melakukan ini?


2
Secara kebetulan, semua skema seperti yang pernah saya lihat menggunakan kolom tristate (mis. 1 2 3 yang berarti Kemenangan Rumah / Menang / Tandang Menang) untuk hasil pertandingan, alih-alih win_team_id Anda dengan nilai id / NULL / id. Satu kendala yang kurang untuk DB harus memeriksa.
AakashM

Jadi maksud Anda desain yang saya setup "bagus"?
jamauss

1
Nah, jika saya diminta komentar saya akan mengatakan: 1) mengapa 'fantasi' dalam begitu banyak nama 2) mengapa bigintuntuk begitu banyak kolom di mana intmungkin akan melakukan 3) mengapa semua _s ?! 4) Saya lebih suka nama tabel tunggal tetapi mengakui tidak semua orang setuju dengan saya // tapi selain itu yang Anda tunjukkan di sini terlihat masuk akal, ya
AakashM

Jawaban:


17

Karena Anda menggunakan SQL Server 2012, Anda dapat menggunakan beberapa fungsi windowing baru.

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

C1menghitung streak_typeuntuk setiap tim dan pertandingan.

C2menemukan yang sebelumnya streak_typedipesan oleh match_id desc.

C3menghasilkan jumlah berjalan yang streak_sumdipesan dengan match_id descmempertahankan 0selama streak_typesama dengan nilai terakhir.

Permintaan jumlah utama sampai garis-garis di mana streak_sumadalah 0.


4
+1 untuk penggunaan LEAD(). Tidak cukup banyak orang tahu tentang fungsi-fungsi windowing baru pada 2012
Mark Sinkinson

4
+1, saya suka trik menggunakan urutan menurun di LAG untuk nanti menentukan goresan terakhir, sangat rapi! Ngomong-ngomong, karena OP hanya menginginkan ID tim, Anda dapat menggantinya dan FantasyTeams JOIN FantasyMatchesdengan FantasyMatches CROSS APPLY (VALUES (home_fantasy_team_id), (away_fantasy_team_id))demikian berpotensi meningkatkan kinerja.
Andriy M

@ AndriyM Tangkapan yang bagus !! Saya akan memperbarui jawabannya dengan itu. Jika Anda membutuhkan kolom lain dari FantasyTeamsitu, mungkin lebih baik bergabung dalam permintaan utama.
Mikael Eriksson

Terima kasih untuk contoh kode ini - Saya akan mencoba ini dan akan melaporkan kembali nanti setelah saya keluar dari rapat ...>: - \
jamauss

@MikaelEriksson - Ini sangat bagus - terima kasih! Pertanyaan cepat - Saya perlu menggunakan set hasil ini untuk memperbarui baris yang ada (bergabung di FantasyTeams.team_id) - Bagaimana Anda akan merekomendasikan mengubah ini menjadi pernyataan UPDATE? Saya mulai mencoba mengubah SELECT menjadi UPDATE tetapi saya tidak bisa menggunakan GROUP BY dalam UPDATE. Apakah Anda mengatakan saya harus membuang hasil yang ditetapkan ke tabel temp dan bergabung menentangnya ke UPDATE atau yang lain? Terima kasih!
jamauss

10

Salah satu pendekatan intuitif untuk menyelesaikan masalah ini adalah:

  1. Temukan hasil terbaru untuk setiap tim
  2. Periksa kecocokan sebelumnya dan tambahkan satu ke jumlah coretan jika jenis hasil cocok
  3. Ulangi langkah 2 tetapi hentikan segera setelah hasil berbeda yang pertama ditemukan

Strategi ini mungkin menang atas solusi fungsi jendela (yang melakukan pemindaian penuh data) karena tabel tumbuh lebih besar, dengan asumsi strategi rekursif diterapkan secara efisien. Kunci keberhasilan adalah menyediakan indeks yang efisien untuk menemukan baris dengan cepat (menggunakan pencarian) dan menghindari pengurutan. Indeks yang dibutuhkan adalah:

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

Untuk membantu dalam optimasi kueri, saya akan menggunakan tabel sementara untuk menahan baris yang diidentifikasi sebagai bagian dari rangkaian beruntun saat ini. Jika goresan biasanya pendek (seperti halnya untuk tim yang saya ikuti, sayangnya) tabel ini harus cukup kecil:

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

Solusi kueri rekursif saya adalah sebagai berikut ( SQL Fiddle di sini ):

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

Teks T-SQL cukup panjang, tetapi setiap bagian dari kueri terkait erat dengan garis besar proses yang diberikan pada awal jawaban ini. Permintaan dibuat lebih lama dengan kebutuhan untuk menggunakan trik tertentu untuk menghindari jenis dan untuk menghasilkan TOPbagian rekursif dari permintaan (yang biasanya tidak diizinkan).

Paket eksekusi relatif kecil dan sederhana jika dibandingkan dengan kueri. Saya telah memberi warna kuning pada area jangkar, dan bagian rekursif berwarna hijau pada tangkapan layar di bawah ini:

Rencana eksekusi rekursif

Dengan deretan baris yang ditangkap dalam tabel sementara, mudah untuk mendapatkan hasil ringkasan yang Anda butuhkan. (Menggunakan tabel sementara juga menghindari tumpahan pengurutan yang mungkin terjadi jika kueri di bawah ini dikombinasikan dengan permintaan rekursif utama)

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

Rencana eksekusi permintaan dasar

Kueri yang sama dapat digunakan sebagai dasar untuk memperbarui FantasyTeams tabel:

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

Atau, jika Anda lebih suka 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;

Salah satu pendekatan menghasilkan rencana eksekusi yang efisien (berdasarkan jumlah baris yang diketahui dalam tabel sementara):

Perbarui rencana eksekusi

Akhirnya, karena metode rekursif secara alami menyertakan match_iddalam pemrosesan, mudah untuk menambahkan daftar match_ids yang membentuk setiap goresan ke output:

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;

Keluaran:

Daftar pertandingan disertakan

Rencana eksekusi:

Rencana eksekusi daftar pertandingan


2
Impresif! Apakah ada alasan khusus mengapa bagian rekursif Anda WHERE menggunakan EXISTS (... INTERSECT ...)bukan hanya Streaks.streak_type = CASE ...? Saya tahu metode mantan dapat berguna ketika Anda harus mencocokkan NULLs di kedua sisi serta nilai-nilai tapi tidak seolah-olah bagian kanan bisa menghasilkan NULLs apapun dalam hal ini, jadi ...
Andriy M

2
@ AndriyM Ya ada. Kode ditulis dengan sangat hati-hati di sejumlah tempat dan cara untuk menghasilkan rencana tanpa jenis. Saat CASEdigunakan, pengoptimal tidak dapat menggunakan gabungan penggabungan (yang mempertahankan urutan kunci gabungan) dan menggunakan penggabungan ditambah jenis sebagai gantinya.
Paul White Reinstate Monica

8

Cara lain untuk mendapatkan hasilnya adalah dengan CTE rekursif

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


terima kasih atas jawaban ini, senang melihat lebih dari satu solusi untuk masalah ini dan dapat membandingkan kinerja di antara keduanya.
jamauss
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.