Отримайте підрахунок смуг та тип смуги з даних про виграш-збиток


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
Між іншим, у всіх таких схемах, які я коли-небудь бачив, для результату поєдинку використовується стовпець tristate (наприклад, 1 2 3, що означає «Домашній виграш / Tie / Away Win»), а не ваш win_team_id зі значенням id / NULL / id. Одне менш обмеження для перевірки БД.
AakashM

Так ви кажете, що дизайн, який я налаштував, "хороший"?
джамаусс

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;

SQL Fiddle

C1розраховує суму streak_typeдля кожної команди та матч.

C2знаходить попередній streak_typeупорядкований match_id desc.

C3генерує поточну суму, streak_sumвпорядковану, match_id descзберігаючи 0довгий, оскільки значення streak_typeє таким, як і останнє значення.

Основний запит підсумовує смуги, де streak_sumє 0.


4
+1 для використання LEAD(). Мало людей знає про нові функції вікон у 2012 році
Марк Сінкінсон

4
+1, мені подобається фокус використання порядку зменшення в LAG, щоб пізніше визначити останню смугу, дуже акуратною! До речі, так як OP хоче тільки ідентифікатори команд, ви можете замінити FantasyTeams JOIN FantasyMatchesз FantasyMatches CROSS APPLY (VALUES (home_fantasy_team_id), (away_fantasy_team_id))і , таким чином , потенційно підвищити продуктивність.
Андрій М

@AndriyM Добрий улов !! Я оновлю відповідь з цим. Якщо вам потрібні інші стовпці FantasyTeams, можливо, краще замість цього приєднатися до основного запиту.
Мікаель Ерікссон

Дякую за цей приклад коду - я спробую це спробувати, і я
звітую

@MikaelEriksson - Це чудово працює - дякую! Швидке запитання - мені потрібно використовувати цей набір результатів для оновлення існуючих рядків (приєднання до FantasyTeams.team_id) - Як ви рекомендуєте перетворити це на оператор UPDATE? Я почав намагатися просто змінити SELECT на ОНОВЛЕННЯ, але я не можу використовувати GROUP BY в ОНОВЛЕННІ. Ви б сказали, що я повинен просто кинути набір результатів у темп-таблицю і приєднатись до цього до ОНОВЛЕННЯ чи чогось іншого? Спасибі!
Джамаусс

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_id, що утворюють кожну смужку до виходу:

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 в цьому випадку, тому ...
Андрій М

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


спасибі за цю відповідь, приємно бачити більше, ніж одне рішення проблеми та мати можливість порівняти продуктивність між ними.
джамаусс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.