รับจำนวนการตรวจสอบและจำนวนการพิมพ์จากข้อมูลการสูญเสียผลเสมอ


15

ฉันทำSQL Fiddleสำหรับคำถามนี้ถ้านั่นทำให้ทุกคนง่ายขึ้น

ฉันมีฐานข้อมูลกีฬาแนวแฟนตาซีและสิ่งที่ฉันพยายามคิดคือทำอย่างไรกับข้อมูล "แนวปัจจุบัน" (เช่น '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 สำหรับแต่ละทีมทุกสัปดาห์ มีความคิดเห็นเกี่ยวกับวิธีการที่ฉันควรทำอย่างไร


2
บังเอิญ schemas ดังกล่าวทั้งหมดที่ฉันเคยเห็นใช้คอลัมน์ tristate (เช่น 1 2 3 ความหมาย Home Win / Tie / Away Win) สำหรับผลการแข่งขันแทนที่จะเป็น winning_team_id ของคุณพร้อมรหัสมูลค่า / NULL / id ข้อ จำกัด น้อยกว่าหนึ่งสำหรับ DB ที่จะต้องตรวจสอบ
AakashM

คุณกำลังบอกว่าการออกแบบที่ฉันติดตั้งนั้น "ดี" หรือไม่?
jamauss

1
ถ้าฉันถามความคิดเห็นฉันจะพูดว่า: 1) ทำไม 'แฟนตาซี' ในชื่อมากมาย 2) ทำไมbigintคอลัมน์หลาย ๆ อันที่intน่าจะทำ 3) ทำไมต้องเป็นทั้งหมด_! 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;

ซอ Fiddle

C1คำนวณstreak_typeสำหรับแต่ละทีมและการแข่งขัน

C2พบว่าก่อนหน้านี้ได้รับคำสั่งจากstreak_typematch_id desc

C3สร้างผลรวมสะสมที่streak_sumสั่งซื้อโดยmatch_id descทำให้0ตราบใดที่streak_typeค่านั้นเท่ากับค่าสุดท้าย

เงินก้อนแบบสอบถามหลักขึ้นลายเส้นที่เป็นstreak_sum0


4
+1 LEAD()สำหรับการใช้งานของ มีคนไม่มากพอที่รู้เกี่ยวกับฟังก์ชั่นการเปิดหน้าต่างใหม่ในปี 2012
Mark Sinkinson

4
+1, ฉันชอบกลอุบายของการใช้คำสั่งจากมากไปหาน้อยใน LAG ในภายหลังเพื่อกำหนดแนวสุดท้าย, เรียบร้อยมาก! อย่างไรก็ตามเนื่องจาก OP ต้องการเพียง ID ทีมคุณจึงสามารถแทนที่FantasyTeams JOIN FantasyMatchesด้วยFantasyMatches CROSS APPLY (VALUES (home_fantasy_team_id), (away_fantasy_team_id))และอาจปรับปรุงประสิทธิภาพ
Andriy M

@AndriyM ดีจับ !! ฉันจะอัปเดตคำตอบด้วยสิ่งนั้น หากคุณต้องการคอลัมน์อื่น ๆ จากFantasyTeamsนั้นน่าจะดีกว่าที่จะเข้าร่วมในแบบสอบถามหลักแทน
Mikael Eriksson

ขอบคุณสำหรับตัวอย่างโค้ดนี้ - ฉันจะลองทำสิ่งนี้และจะรายงานกลับในภายหลังหลังจากที่ฉันออกจากการประชุม ... >: - \
jamauss

@MikaelEriksson - ใช้งานได้ดี - ขอบคุณ! คำถามด่วน - ฉันต้องใช้ชุดผลลัพธ์นี้เพื่ออัปเดตแถวที่มีอยู่ (เข้าร่วมใน FantasyTeams.team_id) - คุณจะแนะนำให้เปลี่ยนเป็นคำสั่ง UPDATE ได้อย่างไร ฉันเริ่มพยายามเปลี่ยน SELECT เป็น UPDATE แต่ฉันไม่สามารถใช้ GROUP BY ใน UPDATE ได้ คุณจะบอกว่าฉันควรโยนชุดผลลัพธ์ลงในตารางชั่วคราวและเข้าร่วมกับการปรับปรุงหรืออะไรอย่างอื่น? ขอบคุณ!
jamauss

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
ที่น่าประทับใจ! มีเหตุผลบางประการที่ว่าทำไมส่วนที่เรียกซ้ำของคุณว่า WHERE กำลังใช้งานอยู่EXISTS (... INTERSECT ...)แทนที่จะเป็นแค่Streaks.streak_type = CASE ...อะไร? ฉันรู้ว่าวิธีการแบบเก่าจะมีประโยชน์เมื่อคุณต้องการจับคู่ NULL ทั้งสองด้านเช่นเดียวกับค่า แต่ไม่ใช่ว่าส่วนที่ถูกต้องสามารถผลิต NULL ใด ๆ ในกรณีนี้ดังนั้น ...
Andriy M

2
@AndriyM ใช่มี รหัสถูกเขียนอย่างระมัดระวังในหลายสถานที่และวิธีการสร้างแผนโดยไม่แปลก เมื่อCASEมีการใช้งานเครื่องมือเพิ่มประสิทธิภาพจะไม่สามารถใช้การรวมเข้าด้วยกันผสาน (ซึ่งรักษาลำดับคีย์ยูเนี่ยน) และใช้การเรียงต่อกันพร้อมเรียงลำดับแทน
พอลไวท์ 9

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


ขอบคุณสำหรับคำตอบนี้ยินดีที่ได้เห็นมากกว่าหนึ่งวิธีในการแก้ปัญหาและสามารถเปรียบเทียบประสิทธิภาพระหว่างทั้งสองได้
jamauss
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.