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:
- Total score of all series
- Number of center hits (center hit has 10 points score)
- The next step to order by is:
- Highest score on last serie
- Highest score on next to last serie
- Highest score on next to next to last serie
- ...
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;