Kazan-kaybet-kravat verilerinden çizgi sayısını ve çizgi türünü alın


15

Herkes için işleri kolaylaştırırsa bu soru için bir SQL Fiddle yaptım .

Bir tür fantezi spor veri tabanım var ve anlamaya çalıştığım şey "güncel çizgi" verilerini (takımın son 2 eşleşmesini kazanması durumunda 'W2') ya da kaybederse 'L1' önceki eşleşmeyi kazandıktan sonra son eşleşmeleri - veya en son eşleşmelerini bağlarsa 'T1').

İşte benim temel şeması:

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
)

Değeri NULLolarakwinning_team_id sütunun maç için bir bağ olduğunu gösterir.

6 takım ve 3 haftalık eşleşmeler için bazı örnek verileri içeren örnek bir DML ifadesi:

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

İşte nasıl elde etmek için anlamaya başladım bile sorun yaşıyorum (yukarıdaki DML dayalı) istenen çıktı bir örnek:

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

Alt sorguları ve CTE'leri kullanarak çeşitli yöntemler denedim ama bir araya getiremem. İleride bunu çalıştırmak için büyük bir veri kümesine sahip olabileceğim için imleç kullanmaktan kaçınmak istiyorum. Bir şekilde kendisine bu verileri birleştiren tablo değişkenleri içeren bir yol olabilir gibi hissediyorum ama hala üzerinde çalışıyorum.

Ek Bilgi: Farklı sayıda takım olabilir (6 ile 10 arasında herhangi bir çift sayı) ve toplam eşleşmeler her takım için her hafta 1 artacaktır. Bunu nasıl yapmam gerektiğine dair bir fikrin var mı?


2
Bu arada, şimdiye kadar gördüğüm tüm bu şemalar, id / NULL / id değerine sahip winning_team_id değeriniz yerine, maç sonucu için bir tristat (örn. DB'nin kontrol etmesi gereken daha az kısıtlama.
AakashM

Yani benim kurduğum tasarımın "iyi" olduğunu mu söylüyorsun?
jamauss

1
Peki, yorum istenirse şunu söyleyebilirim: 1) neden birçok isimde 'fantezi' 2) neden muhtemelen bigintbu kadar çok sütun intiçin 3) neden tüm _s ?! 4) Tablo adlarının tekil olmasını tercih ediyorum, ancak herkesin benimle aynı fikirde olmadığını kabul ediyorum // ama burada bize gösterdikleriniz bir yana tutarlı görünüyor, evet
AakashM

Yanıtlar:


17

SQL Server 2012'de olduğunuz için birkaç yeni pencereleme işlevini kullanabilirsiniz.

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 Keman

C1streak_typeher takım ve maç için hesaplar .

C2streak_typetarafından önceki sıralamayı bulur match_id desc.

C3, son değerle aynı olduğu sürece bir uzun tutarak streak_sumsipariş edilen bir toplam oluşturur .match_id desc0streak_type

Ana sorgu nerede streak_sumolduğu çizgileri toplar 0.


4
+1 kullanımı için LEAD(). 2012'de yeni pencereleme fonksiyonları hakkında yeterli insan bilmiyor
Mark Sinkinson

4
+1, daha sonra son çizgiyi belirlemek için LAG'da azalan düzeni kullanma hilesini çok seviyorum! OP sadece takım kimliklerini istediği beri arada, sen yerini alabilecek FantasyTeams JOIN FantasyMatchesolan FantasyMatches CROSS APPLY (VALUES (home_fantasy_team_id), (away_fantasy_team_id))ve böylece potansiyel olarak performansını artırmak.
Andriy M

@AndriyM İyi yakala !! Cevabı bununla güncelleyeceğim. Diğer sütunlara ihtiyacınız varsa FantasyTeamsbunun yerine ana sorguya katılmak daha iyidir.
Mikael Eriksson

Bu kod örneği için teşekkürler - Bunu bir deneyeceğim ve toplantılar bittikten sonra biraz sonra geri rapor edeceğim ...>: - \
jamauss

@MikaelEriksson - Bu harika çalışıyor - teşekkürler! Hızlı soru - Varolan satırları güncellemek için bu sonuç kümesini kullanmam gerekiyor (FantasyTeams.team_id'e katılma) - Bunu UPDATE ifadesine dönüştürmeyi nasıl önerirsiniz? SELECT'i bir UPDATE olarak değiştirmeye çalıştım ancak GROUP BY'ı bir UPDATE içinde kullanamıyorum. Sonuç kümesini geçici bir tabloya atmam ve buna karşı GÜNCELLEME'ye veya başka bir şeye katılmam gerektiğini söyleyebilir misiniz? Teşekkürler!
jamauss

10

Bu sorunu çözmek için sezgisel bir yaklaşım:

  1. Her takım için en son sonucu bulun
  2. Önceki eşleşmeyi kontrol edin ve sonuç türü eşleşiyorsa çizgi sayısına bir tane ekleyin
  3. 2. adımı tekrarlayın, ancak ilk farklı sonuçla karşılaşıldığında durun

Yinelenen stratejinin verimli bir şekilde uygulandığı varsayılarak, tablo büyüdükçe bu strateji, pencere işlevi çözümü (veriyi tam olarak tarar) gerçekleştirir. Başarının anahtarı, satırları hızlı bir şekilde bulmak için (aramalar kullanarak) verimli dizinler sağlamak ve türlerden kaçınmaktır. Gerekli dizinler:

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

Sorgu optimizasyonuna yardımcı olmak için, geçerli bir çizginin parçası olarak tanımlanan satırları tutmak için geçici bir tablo kullanacağım. Çizgiler genellikle kısaysa (ne yazık ki takip ettiğim takımlar için geçerli olduğu gibi) bu tablo oldukça küçük olmalıdır:

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

Benim özyinelemeli sorgu çözümü aşağıdaki gibidir ( SQL Fiddle burada ):

-- 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 metni oldukça uzundur, ancak sorgunun her bölümü bu cevabın başında verilen geniş işlem ana hatlarına yakındır. Sorgu, sıralamaları önlemek ve TOPsorgunun özyinelemeli bölümünde (normalde izin verilmez) üretmek için belirli hileler kullanma gereği ile daha uzun yapılır .

Uygulama planı, sorgu ile karşılaştırıldığında nispeten küçük ve basittir. Aşağıdaki ekran görüntüsünde çapa bölgesini sarı ve özyinelemeli kısmı yeşil gölgelendirdim:

Özyinelemeli yürütme planı

Geçici bir tabloda yakalanan çizgi satırlarıyla, ihtiyacınız olan özet sonuçları elde etmek kolaydır. (Geçici bir tablo kullanmak, aşağıdaki sorgu ana özyinelemeli sorgu ile birleştirilirse oluşabilecek bir sıralama dökülmesini de önler)

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

Temel sorgu yürütme planı

Aynı sorgu, FantasyTeamstabloyu güncellemek için temel olarak kullanılabilir :

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

Veya, isterseniz 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;

Her iki yaklaşım da verimli bir yürütme planı oluşturur (geçici tablodaki bilinen satır sayısına göre):

Yürütme planını güncelle

Son olarak, özyinelemeli yöntem doğal olarak match_idişlenmesini içerdiğinden match_id, çıktıya her bir çizgiyi oluşturan s'lerin bir listesini eklemek kolaydır :

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;

Çıktı:

Maç listesi dahil

Yürütme planı:

Maç listesi yürütme planı


2
Etkileyici! Özyinelemeli parçanızın WHERE EXISTS (... INTERSECT ...)yerine adil kullanmak yerine belirli bir nedeni var mı Streaks.streak_type = CASE ...? Eski yöntemin her iki taraftaki NULL'ları ve değerleri eşleştirmeniz gerektiğinde yararlı olabileceğini biliyorum, ancak doğru kısım bu durumda herhangi bir NULL üretebilir gibi değil, bu yüzden ...
Andriy M

2
@AndriyM Evet var. Kod, çok çeşitli yerlerde ve çeşitsiz bir plan üretmek için çok dikkatli bir şekilde yazılmıştır. Ne zaman CASEkullanılır, iyileştirici (sendika anahtar düzeni korur) ve bunun yerine bir birleştirme artı türlü kullanan bir birleştirme birleştirme kullanamıyor.
Paul White 9

8

Sonuç almanın başka bir yolu da özyinelemeli bir CTE'dir.

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 demosu


Bu cevap için teşekkürler, soruna birden fazla çözüm görmek ve ikisi arasındaki performansı karşılaştırabilmek güzel.
jamauss
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.