How to organise rows in a union - sql

Having a little issue with my results where I am trying to create a fixture list for each team, playing home and away. I’ve almost got it working but the problem I have is displayed in the following results:
I have 2 teams playing each other in week 1 home and away and the same two teams playing each other home and away in week for.
What this should display when I insert the data and read it is week 1, team 4 is at home playing team 18 who are way, and then the reverse fixture is played in week 4.
In other words row 2 and 3 are incorrect and should only show rows 1 and 4. What do I need to change in the code below to get this to work?
INSERT INTO dbo.Fixture (WeekNumber, HomeTeamID, AwayTeamID, FixtureDate, LeagueID)
SELECT
ROW_NUMBER() OVER (PARTITION BY h.teamID ORDER BY h.TeamID, a.TeamID, h.LeagueID) AS WeekNumber,
h.TeamID,
a.TeamID,
DATEADD(day,(ROW_NUMBER() OVER (ORDER BY h.LeagueID)-1)*7,#StartFixtureWeek) AS FixtureWeek,
h.LeagueID
FROM dbo.Team h
CROSS JOIN dbo.Team a
WHERE h.TeamID <> a.TeamID
AND h.LeagueID = a.LeagueID
UNION
SELECT
ROW_NUMBER() OVER (PARTITION BY a.teamID ORDER BY h.TeamID, a.TeamID, h.LeagueID) AS WeekNumber,
h.TeamID,
a.TeamID,
DATEADD(day,(ROW_NUMBER() OVER (ORDER BY a.LeagueID)-1)*7,#StartFixtureWeek) AS FixtureWeek,
h.LeagueID
FROM dbo.Team h
CROSS JOIN dbo.Team a
WHERE h.TeamID <> a.TeamID
AND h.LeagueID = a.LeagueID
select * from dbo.Fixture
WHERE (HomeTeamID = 4 AND AwayTeamID = 18) OR (HomeTeamID = 18 AND AwayTeamID = 4)
UPDATE:
Below is an explanation and design of the desired output:
WeekNumber HomeTeamID AwayTeamID FixtureWeek LeagueID
1 1 4 10-06-2016 1
2 1 3 17-06-2016 1
3 1 2 24-06-2016 1
4 4 1 30-06-2016 1
5 3 1 06-07-2016 1
6 2 1 13-07-2016 1
1 2 3 10-06-2016 1
2 2 4 17-06-2016 1
3 3 4 24-06-2016 1
4 3 2 30-06-2016 1
5 4 2 06-07-2016 1
6 4 3 13-07-2016 1
1 5 8 10-06-2016 2
2 5 7 17-06-2016 2
3 5 6 24-06-2016 2
4 8 5 30-06-2016 2
5 7 5 06-07-2016 2
6 6 5 13-07-2016 2
1 6 7 10-06-2016 2
2 6 8 17-06-2016 2
3 7 8 24-06-2016 2
4 7 6 30-06-2016 2
5 8 6 06-07-2016 2
6 8 7 13-07-2016 2
Ok so have two leagues (LeagueID 1 and LeagueID 2)
In League 1 there are 4 teams (TeamID) – 1, 2, 3, 4 – They play each other home and away but they can’t play two games within the same week.
In League 2 there are 4 teams (TeamID) – 5, 6, 7, 8 – They play each other home and away but they can’t play two games within the same week.
Both Leagues start on the same day and add 7 days for every game (or in other words every week)
The output doesn’t show it but preferable if each team can play their home game one week and then the next week play away, then play home, then away etc. But if we can get the above output to display first and then fiddle about to order them home and away then that’s fine.

CROSS JOIN with h.TeamID <> a.TeamID generates all pairs, i.e. (1,2) and (2,1). I think it is easier if we separate them, so WHERE filter would become h.TeamID > a.TeamID or h.TeamID < a.TeamID.
Week numbers should be assigned for each League, so it should be partitioned by LeagueID. Then it is a matter of sorting teams in some way, and then in opposite direction.
In the result below you can see how a pattern of team pairs for 6 weeks repeat in two opposite directions.
Most likely it is possible to achieve the same or similar result with one CROSS JOIN that uses <> without UNION and a more complicated expression in ROW_NUMBER.
Side note: don't use UNION if you don't really need it. Use UNION ALL.
Side note 2: it doesn't matter if you use CROSS JOIN or INNER JOIN here. Optimizer would generate identical execution plans. I think CROSS JOIN here is more readable, shows more clearly the intention to generate a Cartesian product - set of all pairs.
Sample data
DECLARE #T TABLE (TeamID int, LeagueID int);
INSERT INTO #T (TeamID, LeagueID) VALUES
(1, 1),
(2, 1),
(3, 1),
(4, 1),
(5, 2),
(6, 2),
(7, 2),
(8, 2);
Query
SELECT
h.TeamID AS HomeTeamID
,a.TeamID AS AwayTeamID
,h.LeagueID
,ROW_NUMBER() OVER
(PARTITION BY h.LeagueID ORDER BY h.TeamID, a.TeamID) AS WeekNumber
,1 AS SortOrder
FROM
#T AS h
CROSS JOIN #T AS a
WHERE
h.LeagueID = a.LeagueID
AND h.TeamID < a.TeamID
UNION ALL
SELECT
h.TeamID AS HomeTeamID
,a.TeamID AS AwayTeamID
,h.LeagueID
,ROW_NUMBER() OVER
(PARTITION BY h.LeagueID ORDER BY a.TeamID DESC, h.TeamID DESC) AS WeekNumber
,2 AS SortOrder
FROM
#T AS h
CROSS JOIN #T AS a
WHERE
h.LeagueID = a.LeagueID
AND h.TeamID > a.TeamID
ORDER BY
LeagueID
,SortOrder
,WeekNumber
;
Result
+------------+------------+----------+------------+-----------+
| HomeTeamID | AwayTeamID | LeagueID | WeekNumber | SortOrder |
+------------+------------+----------+------------+-----------+
| 1 | 2 | 1 | 1 | 1 |
| 1 | 3 | 1 | 2 | 1 |
| 1 | 4 | 1 | 3 | 1 |
| 2 | 3 | 1 | 4 | 1 |
| 2 | 4 | 1 | 5 | 1 |
| 3 | 4 | 1 | 6 | 1 |
| 4 | 3 | 1 | 1 | 2 |
| 4 | 2 | 1 | 2 | 2 |
| 3 | 2 | 1 | 3 | 2 |
| 4 | 1 | 1 | 4 | 2 |
| 3 | 1 | 1 | 5 | 2 |
| 2 | 1 | 1 | 6 | 2 |
| 5 | 6 | 2 | 1 | 1 |
| 5 | 7 | 2 | 2 | 1 |
| 5 | 8 | 2 | 3 | 1 |
| 6 | 7 | 2 | 4 | 1 |
| 6 | 8 | 2 | 5 | 1 |
| 7 | 8 | 2 | 6 | 1 |
| 8 | 7 | 2 | 1 | 2 |
| 8 | 6 | 2 | 2 | 2 |
| 7 | 6 | 2 | 3 | 2 |
| 8 | 5 | 2 | 4 | 2 |
| 7 | 5 | 2 | 5 | 2 |
| 6 | 5 | 2 | 6 | 2 |
+------------+------------+----------+------------+-----------+

Related

How to build an intercalated ranking from two tables using BigQuery

I have two tables in BigQuery with records ordered by a ranking. Given a ratio of integers, I want to be able to join both tables, keeping the order of the ranking and the proportions of the ratio of integers.
For example:
Table A
Name
Ranking A
Kevin
1
Jack
2
Kate
3
Randall
4
Beck
5
Table B:
Name
Ranking B
William
1
Laurel
2
Sophie
3
Tess
4
Deja
5
Toby
6
Nick
7
Given a ratio 2:3 where 2 corresponds with Table A, and 3 corresponds with Table B, the expected result would be:
Name
Ranking A
Ranking B
Final Rank
Kevin
1
1
Jack
2
2
William
1
3
Laurel
2
4
Sophie
3
5
Kate
3
6
Randall
4
7
Tess
4
8
Deja
5
9
Toby
6
10
Beck
5
11
Nick
7
12
Any ideas?
You can solve this problem with some math trick here. In both tables you have to compute a running sum and skip 2 (for second table) or 3 (for first table) values according to the ranking value you're currently placing. Basically you're making two gapped running sum, where the gaps will be filled by the other one's ranking values.
SELECT Name,
SUM(CASE WHEN MOD(RankingA,2) = 1 THEN 4 ELSE 1 END) OVER(ORDER BY RankingA)-3 AS rn
FROM tableA
UNION ALL
SELECT Name,
SUM(CASE WHEN MOD(RankingB,3) = 1 THEN 3 ELSE 1 END) OVER(ORDER BY RankingB) AS rn
FROM tableB
ORDER BY rn
Then you just apply the UNION ALL operation and ORDER BY on the just generated ranking.
Another approach would be:
SELECT name,
IF(tbl = 1, rank, NULL) AS rankingA,
IF(tbl = 2, rank, NULL) AS rankingB,
ROW_NUMBER() OVER (ORDER BY rank_grp, tbl, rank) final_rank
FROM (
SELECT name, rankingA AS rank, DIV(rankingA - 1, 2) AS rank_grp, 1 AS tbl FROM tableA
UNION ALL
SELECT name, rankingB, DIV(rankingB - 1, 3), 2 FROM tableB
);
+---------+----------+----------+------------+
| name | rankingA | rankingB | final_rank |
+---------+----------+----------+------------+
| Kevin | 1 | | 1 |
| Jack | 2 | | 2 |
| William | | 1 | 3 |
| Laurel | | 2 | 4 |
| Sophie | | 3 | 5 |
| Kate | 3 | | 6 |
| Randall | 4 | | 7 |
| Tess | | 4 | 8 |
| Deja | | 5 | 9 |
| Toby | | 6 | 10 |
| Beck | 5 | | 11 |
| Nick | | 7 | 12 |
+---------+----------+----------+------------+
You may try the following:
select Name, RankingA, RankingB,
rank() over (order by NewRank) FinalRank
from
(
select Name, cast(RankingA as string) as RankingA , '' as RankingB,
RankingA + floor((RankingA-1)/2)*3 as NewRank
from TableA
union all
select Name, '', cast(RankingB as string),
RankingB + ceiling((RankingB)/3)*2
from TableB
) T
RankingA + floor((RankingA-1)/2)*3: shifts each two consecutive RankingA values (1,2 and 3,4 and 5,6 ...) by n * 3 where n starts from 0.
RankingB + ceiling((RankingB)/3)*2: shifts each three consecutive RankingB values (1,2,3 and 4,5,6 ...) by n * 2 where n starts from 1.

Query to show top 3 records per user where the user has submitted a minimum of 3?

I have in a table in MS SQL with multiple entries per user. I am trying to get the top 3 entries by date for each user. I have a query that returns returns the maximum top 3 entries per user but is also returning users which have submitted 2 or 1 entries. I have a join with another table only to get the email address. I would like it to return only the entries by john and dave as they have 3 entries. If they have more than 3 just return the top 3 by submitmonth.
select * from (
select m.Email, q.submitmonth, q.A2, q.A7, q.C7, q.C8, q.C16, q.F9, q.F10, q.G4, q.H1, q.H2, q.J2, q.J13, q.K18, q.N1, q.P6,
row_number() over (partition by q.userid order by q.submitmonth desc) as Submitted
from dbo.submission q
left join dbo.users m
on q.UserId = m.UserId ) ranks
where Submitted < 4
this returns
| Email | submitmonth | A2 | A7 | Submitted
| | | | |
| john#yahoo.com | 01/08/2020 | 2 | 4 | 1
| john#yahoo.com | 01/07/2020 | 8 | 8 | 2
| john#yahoo.com | 01/06/2020 | 2 | 1 | 3
| bob#gmail.com | 01/08/2020 | 1 | 3 | 1
| bob#gmail.com | 01/07/2020 | 9 | 7 | 2
| pete#yahoo.co.uk | 01/08/2020 | 8 | 5 | 1
| dave#gmail.com | 01/06/2020 | 3 | 6 | 1
| dave#gmail.com | 01/04/2020 | 5 | 6 | 2
| dave#gmail.com | 01/02/2020 | 1 | 6 | 3
Thanks for your help.
Add the count window function and then filter on it.
select *
from (
select m.Email, q.submitmonth, q.A2, q.A7, q.C7, q.C8, q.C16, q.F9, q.F10, q.G4, q.H1, q.H2, q.J2, q.J13, q.K18, q.N1, q.P6
, row_number() over (partition by q.userid order by q.submitmonth desc) as Submitted
, count(*) over (partition by q.userid) TotalSubmitted
from dbo.submission q
left join dbo.users m on q.UserId = m.UserId
) ranks
where Submitted < 4 and TotalSubmitted >= 3

How to create column for every single integer within a range in SQLite?

Here's some sample data from my table:
day_number daily_users_count
1 1
3 1
6 1
7 1
9 2
10 2
I need all day_number values, from 1 to max(day_number), and I want daily_users_count to be zero if it isn't mentioned in this table.
It should look something like this:
day_number daily_users_count
1 1
2 0
3 1
4 0
5 0
6 1
7 1
8 0
9 2
10 2
I think a left join with a table which has a number column with all integers from 1 to max(day_number) would work, if I put a default value for daily_users_count as 0.
What I don't get is how to create such a table where all integers within a certain range are present. Any alternate solutions or any ways to do this would be much appreciated.
You can do it with a recursive CTE which will return all the day_numbers including the missing ones and then a LEFT join to the table:
with cte as (
select min(day_number) day_number from tablename
union all
select day_number + 1 from cte
where day_number < (select max(day_number) from tablename)
)
select c.day_number,
coalesce(t.daily_users_count, 0) daily_users_count
from cte c left join tablename t
on t.day_number = c.day_number
See the demo.
Results:
| day_number | daily_users_count |
| ---------- | ----------------- |
| 1 | 1 |
| 2 | 0 |
| 3 | 1 |
| 4 | 0 |
| 5 | 0 |
| 6 | 1 |
| 7 | 1 |
| 8 | 0 |
| 9 | 2 |
| 10 | 2 |

Recursive join with SUM

I have data in the following format:
FromStateID ToStateID Seconds
1 2 10
2 3 20
3 4 15
4 5 5
I need the following output
FromStateID ToStateID Seconds
1 2 10
2 3 20
3 4 15
4 5 5
1 3 10+20
1 4 10+20+15
1 5 10+20+15+5
2 4 20+15
2 5 20+15+5
3 5 15+5
This output shows the total time taken FromStateId to ToStateId in every combination in chronological order.
Please help.
I think this is a recursive CTE that follows the links:
with cte as (
select FromStateID, ToStateID, Seconds
from t
union all
select cte.FromStateId, t.ToStateId, cte.Seconds + t.Seconds
from cte join
t
on cte.toStateId = t.FromStateId
)
select *
from cte;
Here is a db<>fiddle.
#Gordon LinOff is the better solution. Below is another option to achieve the same.
You can achieve this using CROSS JOIN and GROUP BY
DECLARE #table table(FromStateId int, ToStateId int, seconds int)
insert into #table
values
(1 ,2 ,10),
(2 ,3 ,20),
(3 ,4 ,15),
(4 ,5 ,5 );
;with cte_fromToCombination as
(select f.fromStateId, t.tostateId
from
(select distinct fromStateId from #table) as f
cross join
(select distinct toStateId from #table) as t
)
select c.FromStateId, c.ToStateId, t.sumseconds as Total_seconds
from cte_fromToCombination as c
CROSS APPLY
(SELECT sum(t.seconds)
from
#table as t
WHERE t.ToStateId <= c.ToStateId
) as t(sumseconds)
where c.tostateId > c.fromStateId
order by FromStateId,ToStateId
+-------------+-----------+---------------+
| FromStateId | ToStateId | Total_seconds |
+-------------+-----------+---------------+
| 1 | 2 | 10 |
| 1 | 3 | 30 |
| 1 | 4 | 45 |
| 1 | 5 | 50 |
| 2 | 3 | 30 |
| 2 | 4 | 45 |
| 2 | 5 | 50 |
| 3 | 4 | 45 |
| 3 | 5 | 50 |
| 4 | 5 | 50 |
+-------------+-----------+---------------+

get data from same table in sql using join

I have a table [dbo].[UserImages] where user uploads their photos after every 6 day, total 18 records for user 3. 9 records of day 1 and 9 records of day 6. There are 4 columns In this table
[Id, UserId, Image, Day]
Id UserId Image Day
1 3 3_20200408_1.png 1
2 3 3_20200408_2.png 1
3 3 3_20200408_3.png 1
4 3 3_20200408_4.png 1
5 3 3_20200408_5.png 1
6 3 3_20200408_6.png 1
7 3 3_20200408_7.png 1
8 3 3_20200408_8.png 1
9 3 3_20200408_9.png 1
10 3 3_20200410_9.png 6
11 3 3_20200410_2.png 6
12 3 3_20200410_3.png 6
13 3 3_20200410_4.png 6
14 3 3_20200410_5.png 6
15 3 3_20200410_6.png 6
16 3 3_20200410_7.png 6
17 3 3_20200410_8.png 6
18 3 3_20200410_9.png 6
I need something like that
ImgCount UserId ImageDay1 ImageDay6
1 3 3_20200408_1.png 3_20200408_1.png
2 3 3_20200408_2.png 3_20200408_2.png
3 3 3_20200408_3.png 3_20200408_3.png
4 3 3_20200408_4.png 3_20200408_4.png
5 3 3_20200408_5.png 3_20200408_5.png
6 3 3_20200408_6.png 3_20200408_6.png
7 3 3_20200408_7.png 3_20200408_7.png
8 3 3_20200408_8.png 3_20200408_8.png
9 3 3_20200408_9.png 3_20200408_9.png
What should I do for this
You can use row_number() and aggregation:
select
imgCount,
userId,
max(case when day = 1 then image end) ImageDay1,
max(case when day = 6 then image end) ImageDay6
from (
select t.*, row_number() over(partition by userId, day order by image) imgCount
from mytable t
where day in (1, 6)
) t
group by userId, imgCount
order by ImgCount
Demo on DB Fiddle:
ImgCount | userId | ImageDay1 | ImageDay6
:------- | -----: | :--------------- | :---------------
1 | 3 | 3_20200408_1.png | 3_20200410_1.png
2 | 3 | 3_20200408_2.png | 3_20200410_2.png
3 | 3 | 3_20200408_3.png | 3_20200410_3.png
4 | 3 | 3_20200408_4.png | 3_20200410_4.png
5 | 3 | 3_20200408_5.png | 3_20200410_5.png
6 | 3 | 3_20200408_6.png | 3_20200410_6.png
7 | 3 | 3_20200408_7.png | 3_20200410_7.png
8 | 3 | 3_20200408_8.png | 3_20200410_8.png
9 | 3 | 3_20200408_9.png | 3_20200410_9.png