8

I have an SQL query that returns the following table about a single team:

date         gameid     pointsfor     pointsagainst

2011-03-20   15         1             10
2011-03-27   17         7             3
2011-04-03   23         6             5
2011-04-10   30         5             4
2011-04-17   35         4             8
2011-05-01   38         8             1
2011-05-08   43         3             7
2011-05-15   48         6             2
2011-05-22   56         10            2
2011-05-29   59         4             5
2011-06-05   65         2             3
2011-06-19   71         5             6
2011-06-19   74         12            2
2011-06-19   77         5             2
2011-06-19   80         5             4

From this table, could anybody please help me calculate what the longest winning and losing streaks are?

I've had a look at a few other examples on here but have struggled to follow them as they are not quite the same as mine. Any help would be greatly appreciated. Thanks!

  • could you provide the schema and data? – nawfal May 07 '12 at 07:41
  • the schema is quite complicated so I thought that my current select query returned all the required data (shown above). is there any other data that would be helpful? my desired output is: - the longest winning streak this particular team has, e.g this team won where gameid = 17, 23 and 30, so three would be the winning streak - the longest losing streak i don't mind if these are separate queries – Graeme Cowbeller May 07 '12 at 08:15
  • I want to +9999 @johntotetwoo comment ツ – Michael Buen May 07 '12 at 12:41
  • my desired output was originally just something very basic like: `winstreak 6' but now that i've seen your query Michael, i really like that it gives the dates and a list of the games. – Graeme Cowbeller May 09 '12 at 11:23

7 Answers7

2

You'll have to implement some MySQL variables to help handle this more efficiently than multiple query/join/group by. This has a single pass through all the records, then once more getting the max per type of win/loss (or tie). I'm assuming the data you've provided is for a single time, and the date is the obvious order of the games... Points For is the team you are interested in, and points against is whoever the opponent is. That said, my "alias" name will be "YourResultSingleTeam".

The inner query will pre-determine the status of the game as "W"in or "L"oss. Then, see if that value is the same as the previous instance for the team. If so, add 1 to the existing win/loss counter. If not, set the counter back to 1. Then, preserve the status of the current game back into the "LastStatus" value for comparison to the next game.

After that is done, its a simple game result, max() grouped by the game result status

select
      StreakSet.GameResult,
      MAX( StreakSet.WinLossStreak ) as MaxStreak
   from
      ( select YR.Date,
               @CurStatus := if( YR.PointsFor > YR.PointsAgainst, 'W', 'L' ) as GameResult,
               @WinLossSeq := if( @CurStatus = @LastStatus, @WinLossSeq +1, 1 ) as WinLossStreak,
               @LastStatus := @CurStatus as carryOverForNextRecord
            from 
               YourResultSingleTeam YR,
               ( select @CurStatus := '',
                        @LastStatus := '',
                        @WinLossSeq := 0 ) sqlvars
            order by
               YR.Date ) StreakSet
   group by
      StreakSet.GameResult

As offered by Nikola, if you want to consider "tie" games, we can adjust by just changing the @CurStatus to the case/when condition to

@CurStatus := case when YR.PointsFor > YR.PointsAgainst then 'W'
                   when YR.PointsFor < YR.PointsAgainst then 'L'
                   else 'T' end as GameResult,
DRapp
  • 47,638
  • 12
  • 72
  • 142
  • Thanks for helping DRapp. Unfortunately when I use your query, the resulting table always shows up as: L 1 W 1 The only thing I changed was replacing YourResultSingleTeam with my query that returns the table I specified in my question. (SELECT date, gameid, pointsfor, pointsagainst FROM result WHERE teamid = 6 AND bye = 0 AND COMPLETED = 1 AND seasonid > 7 AND roundwd = 0 AND (pointsfor != 0 OR pointsagainst != 0) AND gametype != 'Friendly' ORDER BY date ASC) AS YR Have I done anything wrong? Thanks again! – Graeme Cowbeller May 09 '12 at 10:53
  • @GraemeCowbeller, my bad.. forgot the +1 for the IF() on WinLossStreak... Definitely more efficient though had I +1 earlier :) – DRapp May 09 '12 at 11:23
1

There is a solution but I don't think you are going to like it because it requires a self-join and your table is not a table but query.

Inner query will transform dates into ranges - that it, for each date in a table it will find first date having different outcame, or, in case of last game, the date of this game. This data will be aggregated by first date of different streak to flatten and count streaks; outer query then finds extremes by outcome.

select case Outcome 
            when -1 then 'Losses'
            when 1 then 'Wins'
            else 'Undecided'
        end Title
      , max(Streak) Streak
from
(
  select min(date) date, date_to, Outcome, count(*) Streak
  from
  (
    select t1.date, 
           sign (t1.pointsfor - t1.pointsagainst) Outcome, 
           ifnull (min(t2.date), t1.date) date_to
     from table1 t1
     left join table1 t2
       on t1.date < t2.date
      and sign (t1.pointsfor - t1.pointsagainst) 
       <> sign (t2.pointsfor - t2.pointsagainst)
    group by t1.date, sign (t1.pointsfor - t1.pointsagainst)
  ) a
  group by date_to, Outcome
) a
group by Outcome

To circumvent the need to replace table1 with - probably - cumbersome query you might use temporary table, or have the data already in appropriate format in auxiliary table. There is live test at Sql fiddle, along with another, subquery-driven version that might perform better - you should try them both.

Nikola Markovinović
  • 18,963
  • 5
  • 46
  • 51
  • Oh my god, I think this is how DBAs guarantee job security. – Asherah May 07 '12 at 12:58
  • @Len, respectfully, the query does work... however, for MySQL, it is very hard to follow... doing a join to itself is going to implement duplication of processing comparing 1 to ????, then 2 to ????, then 3 to ????, etc... Once that's done, select group by again, just to do another group by for the max streak. – DRapp May 07 '12 at 13:16
  • Your query is similar to running total, some problems are not amenable to set-based approach http://sqlblog.com/blogs/adam_machanic/archive/2006/07/12/running-sums-redux.aspx – Michael Buen May 07 '12 at 14:37
  • @MichaelBuen Yes, I know. This is the very reason we have window functions, specifically lag and lead in Oracle. I don't use MySql, but had to answer as nobody else did for four hours, so I offered pure SQL solution. But thank you for your interest. – Nikola Markovinović May 07 '12 at 14:51
  • Just a gentle reminder :-) This query isn't exactly suited to production; if there's 1,000 rows in table, your query will traverse rows 500,500 times. Just like what Adam Machanic explained with running total – Michael Buen May 07 '12 at 15:20
  • Sometimes pure SQL or common denominator won't cut it. You have to tap the particular database's strength. For that, I use http://sqlfiddle.com. I don't have MySQL on my machine, reclaiming disk space from MySQL is a fail :-) – Michael Buen May 07 '12 at 15:36
  • @MichaelBuen Didn't you notice Sql Fiddle link in my answer? And yes, you are absolutely right. – Nikola Markovinović May 07 '12 at 15:51
  • Heheh I didn't notice, stackoverflow's blue link on answer look so subtle, blends with black :-) It took me at least three pass to see your sqlfiddle link. When I post a link in answer, I put them on their own line; and not embedded in text, raw url stands out – Michael Buen May 07 '12 at 16:18
1

MySQL don't have CTE nor windowing function (e.g. SUM OVER, ROW_NUMBER OVER, etc). But it has one redeeming factor. Variables!

Use this:

select 
   min(date) as start_date,
   max(date) as end_date,
   count(date) as streak,
   group_concat(gameid) as gameid_list
from
( 
  select *,      
    IF(
        pointsfor > pointsagainst 
        and 
        @pointsfor > @pointsagainst, 
           @gn, @gn := @gn + 1)                
    as group_number,

    @date as old_date, @gameid as old_gameid, 
    @pointsfor as old_pointsfor,
    @pointsagainst as old_pointsagainst,

    @date := date, @gameid := gameid, 
    @pointsfor := pointsfor, @pointsagainst := pointsagainst      
  from tbl
  cross join 
  (
    select 
      @date := CAST(null as date) as xa,
      @gameid := null + 0 as xb, -- why CAST(NULL AS INT) doesn't work?
      @pointsfor := null + 0 as xc, @pointsagainst := null + 0 as xd, @gn := 0
  ) x
  order by date
) as y
group by group_number
order by streak desc;

Output:

START_DATE                    END_DATE                      STREAK  GAMEID_LIST
March, 27 2011 08:00:00-0700  April, 10 2011 08:00:00-0700  3       17,23,30
June, 19 2011 08:00:00-0700   June, 19 2011 08:00:00-0700   3       74,77,80
May, 15 2011 08:00:00-0700    May, 22 2011 08:00:00-0700    2       48,56
March, 20 2011 08:00:00-0700  March, 20 2011 08:00:00-0700  1       15
April, 17 2011 08:00:00-0700  April, 17 2011 08:00:00-0700  1       35
May, 01 2011 08:00:00-0700    May, 01 2011 08:00:00-0700    1       38
May, 08 2011 08:00:00-0700    May, 08 2011 08:00:00-0700    1       43
May, 29 2011 08:00:00-0700    May, 29 2011 08:00:00-0700    1       59
June, 05 2011 08:00:00-0700   June, 05 2011 08:00:00-0700   1       65
June, 19 2011 08:00:00-0700   June, 19 2011 08:00:00-0700   1       71

Live test: http://www.sqlfiddle.com/#!2/bbe78/8

Note on my solution on sqlfiddle, it has two queries. 1. Simulation on top. 2. Final query below

Michael Buen
  • 38,643
  • 9
  • 94
  • 118
  • Just in case you are still wondering why CAST(NULL AS INT) didn't work: [Nested CAST not working](http://stackoverflow.com/questions/10347624/nested-cast-not-working). – Andriy M May 08 '12 at 04:56
  • I can't get it :D Anyway, it works on other database though, e.g. [Postgres](http://www.sqlfiddle.com/#!1/e93ae/4), [Oracle](http://www.sqlfiddle.com/#!4/e93ae/7), [SQL Server](http://www.sqlfiddle.com/#!3/e93ae/9). Sadly, [MySQL](http://www.sqlfiddle.com/#!2/85789/13) cannot. Fortunately there's a workaround: [select null+0 as x](http://www.sqlfiddle.com/#!2/85789/15) – Michael Buen May 08 '12 at 05:13
  • Simply put, the `CAST` and `CONVERT` functions in MySQL accept a slightly different set of type names than those used to declare variables and columns. It seems absolutely counter-intuitive to me, but that's what appears to be the case. – Andriy M May 08 '12 at 05:18
  • Hi Micahel, I'm running your query in my query browser and running into a problem. For some reason when I run it once, it returns incorrect data, but if I click run again, it return correct data. Would you happen to know why this is the case? Thanks again! – Graeme Cowbeller May 09 '12 at 11:02
  • also, in case it matters, I have replaced "tbl" with my query that returns the table: `(SELECT date, gameid, pointsfor, pointsagainst FROM result WHERE teamid = 6 AND bye = 0 AND COMPLETED = 1 AND seasonid > 7 AND roundwd = 0 AND (pointsfor != 0 OR pointsagainst != 0) AND gametype != 'Friendly' ORDER BY date ASC) AS tbl` – Graeme Cowbeller May 09 '12 at 11:06
  • Hmm.. how can be MySql that flaky? Did you run my exact query? Or your already modified one? If you can reproduce the error, take a screenshot, post the screenshot url here; and paste the query in sqlfiddle, then post the sqlfiddle url here, I'd be glad to analyze – Michael Buen May 10 '12 at 01:52
  • Wow I used this in my own table and it works great! @MichaelBuen. Question my table is a view that already has Win, Draw, Lose column but it also has the Team Name. Where would I add a group level for each team? Right now I have a "where" clause after "...) x" e.g.: "...) x where team_name = 'Team 1' order by date" but I would need 150 queries for all teams. How could I do this by team as well? – Matias Diez Feb 17 '19 at 19:01
  • @MatiasDiez On latest version of MySQL, it's easier to do that query. Use `partition by team_name` on CTE, and then `group by team_name` on final query. Use and adjust this query to your requirement: https://stackoverflow.com/questions/10478295/mysql-winning-streak-for-one-team/55121403#55121403 – Michael Buen Mar 12 '19 at 12:28
1

Latest version of MySQL has CTE and is windowing-capable.

Here's a solution.

First step, group winning and losing by assigning them their own streak_group number:

with t as 
(
    select
        *,      
        pointsfor - pointsagainst > 0 is_winner,
        case when pointsfor - pointsagainst > 0 
            and lag(pointsfor) over(order by date, pointsfor - pointsagainst desc) 
                - lag(pointsagainst) over(order by date, pointsfor - pointsagainst desc) > 0 
        then
            0
        else
            1
        end as is_new_group
    from tbl
)
select *, sum(is_new_group) over(order by date, pointsfor - pointsagainst desc) as streak_group
from t

Output:

date                |gameid |pointsfor |pointsagainst |is_winner |is_new_group |streak_group |
--------------------|-------|----------|--------------|----------|-------------|-------------|
2011-03-20 15:00:00 |15     |1         |10            |0         |1            |1            |
2011-03-27 15:00:00 |17     |7         |3             |1         |1            |2            |
2011-04-03 15:00:00 |23     |6         |5             |1         |0            |2            |
2011-04-10 15:00:00 |30     |5         |4             |1         |0            |2            |
2011-04-17 15:00:00 |35     |4         |8             |0         |1            |3            |
2011-05-01 15:00:00 |38     |8         |1             |1         |1            |4            |
2011-05-08 15:00:00 |43     |3         |7             |0         |1            |5            |
2011-05-15 15:00:00 |48     |6         |2             |1         |1            |6            |
2011-05-22 15:00:00 |56     |10        |2             |1         |0            |6            |
2011-05-29 15:00:00 |59     |4         |5             |0         |1            |7            |
2011-06-05 15:00:00 |65     |2         |3             |0         |1            |8            |
2011-06-19 15:00:00 |74     |12        |2             |1         |1            |9            |
2011-06-19 15:00:00 |77     |5         |2             |1         |0            |9            |
2011-06-19 15:00:00 |80     |5         |4             |1         |0            |9            |
2011-06-19 15:00:00 |71     |5         |6             |0         |1            |10           |

Final query. Count the streak of winning:

with t as 
(
    select
        *,      
        pointsfor - pointsagainst > 0 is_winner,
        case when pointsfor - pointsagainst > 0 
            and lag(pointsfor) over(order by date, pointsfor - pointsagainst desc) 
                - lag(pointsagainst) over(order by date, pointsfor - pointsagainst desc) > 0 
        then
            0
        else
            1
        end as is_new_group
    from tbl
)
, streak_grouping as
(
    select
        *, sum(is_new_group) over(order by date, pointsfor - pointsagainst desc) as streak_group
    from t
)
select 
    min(date) as start_date,
    max(date) as end_date,
    count(*) as streak,
    group_concat(gameid order by gameid) as gameid_list
from streak_grouping
group by streak_group
order by streak desc, start_date

Output:

start_date          |end_date            |streak |gameid_list |
--------------------|--------------------|-------|------------|
2011-03-27 15:00:00 |2011-04-10 15:00:00 |3      |17,23,30    |
2011-06-19 15:00:00 |2011-06-19 15:00:00 |3      |74,77,80    |
2011-05-15 15:00:00 |2011-05-22 15:00:00 |2      |48,56       |
2011-03-20 15:00:00 |2011-03-20 15:00:00 |1      |15          |
2011-04-17 15:00:00 |2011-04-17 15:00:00 |1      |35          |
2011-05-01 15:00:00 |2011-05-01 15:00:00 |1      |38          |
2011-05-08 15:00:00 |2011-05-08 15:00:00 |1      |43          |
2011-05-29 15:00:00 |2011-05-29 15:00:00 |1      |59          |
2011-06-05 15:00:00 |2011-06-05 15:00:00 |1      |65          |
2011-06-19 15:00:00 |2011-06-19 15:00:00 |1      |71          |
Michael Buen
  • 38,643
  • 9
  • 94
  • 118
0

You are dealing here is to track trends of wins and loses, which needs to be computed with a loop of some sort with a running counter, not in SQL. SQL queries deal with individual rows, grouping, sorting etc; You are trying to use a language which is not meant to be solving such problems.

Reza S
  • 9,480
  • 3
  • 54
  • 84
  • Not anymore. ANSI SQL 2008-compliant databases are equally capable of solving that problem now, this can be solved by CTE and windowing function combo. See another example of this problem: http://stackoverflow.com/questions/10448024/sql-issue-calculate-max-days-sequence/10449751#10449751 – Michael Buen May 07 '12 at 14:42
0

You will have to create a cursor, read all rows, compute the data ... every time you want to get the longest streaks...

I suggest a workaround that will make things easier. You add a column in your table "streakFor". every time you insert a row :

//pseudo code
if pointsFor > pointsAgainst
    if last_streakFor > 0 
        then streakFor++ 
        else streakFor = 1

else
    if last_streakFor > 0 
        then streakFor = -1 
        else streakFor--

last_streakFor is streakFor in the last inserted row
then you insert the row with column streakFor

Now you can

  • select max(streakFor) from yourTable where yourConditions that will give you both the longest winning streak for "pointsFor" and the longest losing streak for "pointsAgainst"
  • select min(streakFor) from yourTable where yourConditions will give you longest winning streak for "pointsAgainst" and longest losing streak for "pointsFor"
jazzytomato
  • 6,994
  • 2
  • 31
  • 44
0

thanks for all the helps guys. i ended up using php to loop through as suggested. in case anyone is wondering, this is my code:

$streakSQL = "SELECT date, gameid, pointsfor, pointsagainst FROM result WHERE teamid = ".$_GET['teamid']." AND bye = 0 AND COMPLETED = 1 AND seasonid > 7 AND roundwd = 0 AND (pointsfor != 0 OR pointsagainst != 0)";
            $streak = mysql_query($streakSQL);

            $winstreak = 0;
            $maxwinstreak = 0;
            $losestreak = 0;
            $maxlosestreak = 0;
            while($streakRow = mysql_fetch_array($streak))
            {
                //calculate winning streak
                if($streakRow['pointsfor'] > $streakRow['pointsagainst'])
                { 
                    $winstreak++; 
                    if($winstreak > $maxwinstreak)
                    {
                        $maxwinstreak = $winstreak;
                    }
                }
                else{ $winstreak = 0; }
                //calculate losing streak
                if($streakRow['pointsfor'] < $streakRow['pointsagainst'])
                { 
                    $losestreak++; 
                    if($losestreak > $maxlosestreak)
                    {
                        $maxlosestreak = $losestreak;
                    }
                }
                else{ $losestreak = 0; }
            }
            echo "Biggest Winning Streak: ".$maxwinstreak;
            echo "<br />Biggest Losing Streak: ".$maxlosestreak;