Home > Mobile >  Order competitors by multiple conditions
Order competitors by multiple conditions

Time:03-29

I use a concrete, but hypothetical, example.

Consider a database containing the results of a shooting competition, where each competitor made several series of shots. DB contains 3 tables: Competitors, Series and Shots.

Competitors:

id name
1 A
2 B

Series:

id competitorId
1 1
2 1
3 1
4 2
5 2
6 2

Shots:

id serieId score
1 1 8
2 1 8
3 1 8
4 2 10
5 2 7
6 2 6
7 3 10
8 3 8
9 3 6
10 4 8
11 4 8
12 4 7
13 5 7
14 5 10
15 5 7
16 6 7
17 6 10
18 6 7

(DDL with the above statements: dbfiddle)

What I need is to order competitors by multiple conditions, which are:

  1. Total score of all series
  2. Number of center hits (center hit has 10 points score)
  3. The next step to order by is:
    1. Highest score on last serie
    2. Highest score on next to last serie
    3. Highest score on next to next to last serie
    4. ...

and so on for the number of series in the competition.

The query that uses the first two order conditions is quite straightforward:

SELECT comp.name,
    SUM(shots.score) AS score,       
    SUM(IIF(shots.score = 10, 1, 0)) AS centerHits         
FROM Shots shots
INNER JOIN Series series ON series.id = shots.serieId
INNER JOIN Competitors comp ON comp.id = series.competitorId
GROUP BY comp.name
ORDER BY score DESC, centerHits DESC

It produces following results:

name score centerHits
A 71 2
B 71 2

With the 3rd order condition I expect B competitor to be above A, because both have the same total score, the same centerHits and the same score for the last serie (24), but the score of next to last serie of B is 24 while A's score is only 23.

I wonder if it's possible to make a query that uses the third and following order conditions.

CodePudding user response:

It appears you need a multi-level query, each building on the one prior.

The INNER-MOST query with alias PQ is a simple sum on a per SerieID which gets the total Center Hits and total points for each respective set. Similar to what you had for the counting.

From that, you need to know which series is the latest (most recent) and work your way backwards to the prior and again prior to that. By using the OVER / PARTITION, I am joining to the series table to get the competitor ID and name while I'm at it.

By Partitioning the data based on each competitor, and applying the order based on the SerieID DESCENDING, I am getting the row number which will put the most recent as row_number() becoming 1, 2 and 3 respectively, such that for Competitor A, who had SerieID 1, then 2, then 3 will have the final "MostRecent" column as 3, 2 and 1 respetively, so SerieID 3 = 1 -- the most recent, and SerieID 1 = 3 the OLDEST serie or the competitor.

Similarly for the second competitor B, SerieIDs 4, 5 and 6 become 3, 2, 1 respectively. So now, you have a basis to know what was the latest (1 = most recent), the next to last (2 = next most recent), and next to next to last (3...)

Now that these two parts are all set, I can sum the respective totals, center hits, and now expliitly know what the most recent (1) was for its sort, and second latest (2) and third from last (3) are. These are added to the group by.

Now, if one competitor has 6 shooting series vs another having 4 series (not that it will happen in a real competition, but to understand the context), the 6 series will have their LATEST as the MostRecent = 1, similarly with 4 series, the 4th series will be MostRecent = 1.

So the final group by at the COMPETITOR level, you can assess all the parts in question.

select
        F.Name,
        F.CompetitorID,
        sum( F.SeriesTotalScore ) TotalScore,
        sum( F.CenterHits ) CenterHits,
        sum( case when F.MostRecent = 1 
                    then F.SeriesTotalScore else 0 end ) MostRecentScore,
        sum( case when F.MostRecent = 2 
                    then F.SeriesTotalScore else 0 end ) SecondToMostRecentScore,
        sum( case when F.MostRecent = 3 
                    then F.SeriesTotalScore else 0 end ) ThirdToMostRecentScore
    from
        ( select
                c.Name,
                Se.CompetitorID,
                PQ.SerieId,
                PQ.CenterHits,
                PQ.SeriesTotalScore,
                ROW_NUMBER() OVER( PARTITION BY Se.CompetitorID 
                                    order by PQ.SerieId DESC) AS MostRecent
            from
                ( select
                        s.serieId,
                        sum( case when s.score = 10 then 1 else 0 end ) as CenterHits,
                        sum( s.Score ) SeriesTotalScore
                    from
                        Shots s
                    group by
                        s.SerieID ) PQ
                    Join Series Se
                        on PQ.SerieID = se.id
                        JOIN Competitors c
                            on Se.CompetitorID = c.id 
        ) F
    group by
        F.Name,
        F.CompetitorID
    order by
        sum( F.SeriesTotalScore ) desc,
        sum( F.CenterHits ),
        sum( case when F.MostRecent = 1 
                    then F.SeriesTotalScore else 0 end ) desc,
        sum( case when F.MostRecent = 2 
                    then F.SeriesTotalScore else 0 end ) desc,
        sum( case when F.MostRecent = 3 
                    then F.SeriesTotalScore else 0 end ) desc

CodePudding user response:

You should be able to do this pretty simply, as your requirements can be done with normal aggregation and window functions.

For each level of ordering:

  • "Total score of all series" can be satisfied by summing all scores.
  • "Number of center hits (center hit has 10 points score)" can be satisfied with a conditional count.
  • To order by each series working backwards by date, we can aggregate the total score per series (which we calculate using a window function) using STRING_AGG, ordering the aggregation by date (or id). Then if we order the final query by that aggregation, the later series will be sorted first.

    This method allows you to order by an arbitrary number of series, as opposed to the other answer.

It's unclear how you define "later" and "earlier" as you have no date column, but I've used series.id as a proxy for that.

SELECT
  comp.name,
  SUM(shots.score) as totalScore,
  COUNT(CASE WHEN shots.score = 10 THEN 1 END) AS centerHits,
  STRING_AGG(NCHAR(shots.MaxScore   65), ',') WITHIN GROUP (ORDER BY series.id DESC) as AllShots
FROM (
    SELECT *,
      SUM(shots.score) OVER (PARTITION BY shots.serieID) MaxScore
    FROM Shots shots
) shots
INNER JOIN Series series ON series.id = shots.serieId
INNER JOIN Competitors comp ON comp.id = series.competitorId
GROUP BY
  comp.id,
  comp.name
ORDER BY
  totalScore DESC,
  centerHits DESC,
  AllShots DESC;

Note that when grouping by name, you should also add in the primary key to the GROUP BY as the name might not be unique.


A similar, but slightly more complex query, is to pre-aggregate shots in the derived table. This is likely to perform better than using a window function.

SELECT
  comp.name,
  SUM(shots.totalScore) as totalScore,
  SUM(centerHits) AS centerHits,
  STRING_AGG(NCHAR(shots.totalScore   65), ',') WITHIN GROUP (ORDER BY series.id DESC) as AllShots
FROM (
    SELECT
      shots.serieId,
      SUM(shots.score) as totalScore,
      COUNT(CASE WHEN shots.score = 10 THEN 1 END) AS centerHits
    FROM Shots shots
    GROUP BY
      shots.serieId
) shots
INNER JOIN Series series ON series.id = shots.serieId
INNER JOIN Competitors comp ON comp.id = series.competitorId
GROUP BY
  comp.id,
  comp.name
ORDER BY
  totalScore DESC,
  centerHits DESC,
  AllShots DESC;

db<>fiddle

  • Related