Nhận số lượng và loại chuỗi từ dữ liệu thắng-thua


15

Tôi đã tạo một SQL Fiddle cho câu hỏi này nếu điều đó giúp mọi người dễ dàng hơn.

Tôi có một cơ sở dữ liệu thể thao giả tưởng và những gì tôi đang cố gắng tìm ra là làm thế nào để đưa ra dữ liệu "vệt hiện tại" (như 'W2' nếu đội thắng 2 trận đấu cuối cùng của họ, hoặc 'L1' nếu họ thua trận đấu cuối cùng của họ sau khi chiến thắng trận đấu trước đó - hoặc 'T1' nếu họ kết thúc trận đấu gần đây nhất của họ).

Đây là lược đồ cơ bản của tôi:

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
)

Giá trị NULLtrong winning_team_idcột biểu thị một ràng buộc cho trận đấu đó.

Đây là một tuyên bố DML mẫu với một số dữ liệu mẫu cho 6 đội và các trận đấu có giá trị trong 3 tuần:

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

Dưới đây là một ví dụ về đầu ra mong muốn (dựa trên DML ở trên) mà tôi gặp khó khăn ngay cả khi bắt đầu tìm ra cách lấy:

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

Tôi đã thử các phương pháp khác nhau bằng cách sử dụng các truy vấn con và CTE nhưng tôi không thể kết hợp nó. Tôi muốn tránh sử dụng một con trỏ vì tôi có thể có một bộ dữ liệu lớn để chạy nó trong tương lai. Tôi cảm thấy có thể có một cách liên quan đến các biến bảng tham gia dữ liệu này vào chính nó bằng cách nào đó nhưng tôi vẫn đang làm việc với nó.

Thông tin bổ sung: Có thể có một số lượng đội khác nhau (bất kỳ số chẵn nào trong khoảng từ 6 đến 10) và tổng số trận đấu sẽ tăng thêm 1 cho mỗi đội mỗi tuần. Bất cứ ý tưởng về cách tôi nên làm điều này?


2
Ngẫu nhiên, tất cả các lược đồ như vậy tôi từng thấy đều sử dụng cột tristate (ví dụ 1 2 3 có nghĩa là Win Win / Tie / Away Win) cho kết quả trận đấu, thay vì win_team_id của bạn với giá trị id / NULL / id. Một ràng buộc ít hơn cho DB phải kiểm tra.
AakashM

Vì vậy, bạn đang nói thiết kế tôi thiết lập là "tốt"?
jamauss

1
Chà, nếu tôi được hỏi ý kiến ​​tôi sẽ nói: 1) tại sao 'fantasy' trong rất nhiều tên 2) tại sao bigintcho rất nhiều cột intcó thể sẽ làm 3) tại sao tất cả các _?! 4) Tôi thích tên bảng là số ít nhưng thừa nhận không phải ai cũng đồng ý với tôi // nhưng những cái mà bạn đã cho chúng tôi thấy ở đây có vẻ mạch lạc, vâng
AakashM

Câu trả lời:


17

Vì bạn đang ở trên SQL Server 2012, bạn có thể sử dụng một vài chức năng cửa sổ mới.

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;

Câu đố SQL

C1 tính toán streak_type cho mỗi đội và trận đấu.

C2tìm thấy thứ tự trước streak_typebởimatch_id desc .

C3tạo ra một tổng chạy được streak_sumsắp xếp bằng cách match_id descgiữ 0một khoảng thời gian streak_typegiống như giá trị cuối cùng.

Khoản tiền truy vấn chính lên những vệt nơi streak_sum0.


4
+1 cho việc sử dụng LEAD(). Không đủ người biết về các chức năng cửa sổ mới trong năm 2012
Mark Sinkinson

4
+1, tôi thích thủ thuật sử dụng thứ tự giảm dần trong LAG để sau đó xác định vệt cuối cùng, rất gọn gàng! Nhân tiện, vì OP chỉ muốn ID nhóm, bạn có thể thay thế FantasyTeams JOIN FantasyMatchesbằng FantasyMatches CROSS APPLY (VALUES (home_fantasy_team_id), (away_fantasy_team_id))và do đó có khả năng cải thiện hiệu suất.
Andriy M

@AndriyM Bắt tốt !! Tôi sẽ cập nhật câu trả lời với điều đó. Nếu bạn cần các cột khác từ FantasyTeamsnó, có lẽ tốt hơn để tham gia vào truy vấn chính thay thế.
Mikael Eriksson

Cảm ơn ví dụ về mã này - Tôi sẽ dùng thử và sẽ báo cáo lại sau một thời gian sau khi tôi ra khỏi cuộc họp ...>: - \
jamauss

@MikaelEriksson - Điều này hoạt động rất tốt - cảm ơn! Câu hỏi nhanh - Tôi cần sử dụng tập kết quả này để cập nhật các hàng hiện có (tham gia trên FantasyTeams.team_id) - Bạn muốn giới thiệu biến điều này thành câu lệnh CẬP NHẬT như thế nào? Tôi đã bắt đầu cố gắng thay đổi CHỌN thành CẬP NHẬT nhưng tôi không thể sử dụng NHÓM THEO trong CẬP NHẬT. Bạn có nói rằng tôi chỉ nên ném tập kết quả vào một bảng tạm thời và tham gia vào đó để CẬP NHẬT hay cái gì khác không? Cảm ơn!
jamauss

10

Một cách tiếp cận trực quan để giải quyết vấn đề này là:

  1. Tìm kết quả gần đây nhất cho mỗi đội
  2. Kiểm tra trận đấu trước và thêm một vào số đếm nếu loại kết quả khớp
  3. Lặp lại bước 2 nhưng dừng ngay khi gặp kết quả khác nhau đầu tiên

Chiến lược này có thể chiến thắng giải pháp chức năng cửa sổ (thực hiện quét toàn bộ dữ liệu) khi bảng phát triển lớn hơn, giả sử chiến lược đệ quy được thực hiện hiệu quả. Chìa khóa thành công là cung cấp các chỉ mục hiệu quả để xác định vị trí các hàng một cách nhanh chóng (sử dụng tìm kiếm) và tránh sắp xếp. Các chỉ số cần thiết là:

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

Để hỗ trợ tối ưu hóa truy vấn, tôi sẽ sử dụng một bảng tạm thời để giữ các hàng được xác định là một phần của chuỗi hiện tại. Nếu các vệt thường ngắn (như đúng với các đội tôi theo dõi, thật đáng buồn) thì bảng này khá nhỏ:

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

Giải pháp truy vấn đệ quy của tôi như sau ( SQL Fiddle tại đây ):

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

Văn bản T-SQL khá dài, nhưng mỗi phần của truy vấn tương ứng chặt chẽ với phác thảo quy trình rộng được đưa ra khi bắt đầu câu trả lời này. Truy vấn được thực hiện lâu hơn bởi nhu cầu sử dụng một số thủ thuật nhất định để tránh sắp xếp và tạo ra một TOPphần đệ quy của truy vấn (thường không được phép).

Kế hoạch thực hiện tương đối nhỏ và đơn giản bằng cách so sánh với truy vấn. Tôi đã tô màu vùng neo màu vàng và phần đệ quy màu xanh lục trong ảnh chụp màn hình bên dưới:

Kế hoạch thực hiện đệ quy

Với các hàng sọc được ghi trong một bảng tạm thời, thật dễ dàng để có được kết quả tóm tắt mà bạn yêu cầu. (Sử dụng bảng tạm thời cũng tránh được sự cố tràn sắp xếp có thể xảy ra nếu truy vấn bên dưới được kết hợp với truy vấn đệ quy chính)

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

Kế hoạch thực hiện truy vấn cơ bản

Truy vấn tương tự có thể được sử dụng làm cơ sở để cập nhật FantasyTeamsbảng:

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

Hoặc, nếu bạn thích 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;

Cách tiếp cận tạo ra một kế hoạch thực hiện hiệu quả (dựa trên số lượng hàng đã biết trong bảng tạm thời):

Cập nhật kế hoạch thực hiện

Cuối cùng, vì phương thức đệ quy tự nhiên bao gồm match_idtrong quá trình xử lý của nó, nên dễ dàng thêm một danh sách các match_ids tạo thành từng vệt cho đầu ra:

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;

Đầu ra:

Bao gồm danh sách phù hợp

Kế hoạch thực hiện:

Kế hoạch thực hiện danh sách phù hợp


2
Ấn tượng! Có một lý do cụ thể tại sao phần đệ quy của bạn đang sử dụng EXISTS (... INTERSECT ...)thay vì chỉ Streaks.streak_type = CASE ...? Tôi biết phương pháp trước đây có thể hữu ích khi bạn cần khớp các NULL ở cả hai mặt cũng như các giá trị nhưng không phải là phần bên phải có thể tạo ra bất kỳ NULL nào trong trường hợp này, vì vậy ...
Andriy M

2
@AndriyM Vâng, có đấy. Mã được viết rất cẩn thận ở một số nơi và cách để tạo ra một kế hoạch không có loại. Khi CASEđược sử dụng, trình tối ưu hóa không thể sử dụng phép nối hợp nhất (giữ nguyên thứ tự khóa liên kết) và sử dụng phép nối cộng với các loại thay thế.
Paul White phục hồi Monica

8

Một cách khác để có được kết quả là bằng CTE đệ quy

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

Bản thử nghiệm SQLFiddle


cảm ơn vì câu trả lời này, thật tuyệt khi thấy nhiều hơn một giải pháp cho vấn đề và có thể so sánh hiệu suất giữa hai vấn đề.
jamauss
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.