Select one row from non unique rows based on row value - sql

I have a quiz table
id | user_id | quiz_id
--------------------------
1 | 34567 | 12334
2 | 34567 | 12334
3 | 34567 | 23455
id 1 and 2 depicts a quiz that can be assigned to the same user twice
and a quiz transaction table
id | date | status
------------------------
1 | 2014 | assigned
2 | 2014 | assigned
3 | 2014 | assigned
------------------------
1 | 2014 | completed
id is foreign key to quiz table id, the last row depicts whenever a user finished the quiz, the row in the transaction table is updated with status 'completed'
Expected Result: I want a table with a structure like
id | user_id| course_id | date | status
------------------------------------------
1 | 34567 | 12334 | 2014 | completed
2 | 32567 | 12334 | 2014 | assigned
3 | 2014 | 23455 | 2014 | assigned
My query is
SELECT q.id, q.user_id, q.course_id, qt.date, qt.status FROM quiz q
LEFT JOIN
quiz_transaction qt ON
q.id = qt.id
but it gives me extra row (as the query will)
1 | 34567 | 12334 | 2014 | assigned
I cannot use
ON qt.type = 'completed'
Because if its completed it should return a completed row and if not it should return an assigned row but not both.
So in the result I cannot have
1 | 34567 | 12334 | 2014 | completed
1 | 34567 | 12334 | 2014 | assigned
How can I do it?

How about simply using the MAX() function with GROUP BY (SQL Fiddle):
SELECT q.id, q.user_id, q.course_id, qt.date, MAX(qt.status) AS Status
FROM quiz q
LEFT JOIN quiz_transaction qt ON q.id = qt.id
GROUP BY q.id, q.user_id, q.course_id, qt.date
EDIT: If you need to order a string a certain way, you could use a CASE statement to convert the string to a number. Get the MAX value and then convert it back (SQL Fiddle):
SELECT m.id, m.user_id, m.quiz_id, MAX(m.date),
CASE WHEN MAX(m.status) = 1 THEN 'assigned'
WHEN MAX(m.status) = 2 THEN 'doing'
WHEN MAX(m.status) = 3 THEN 'completed' END AS Status
FROM
(
SELECT q.id, q.user_id, q.quiz_id, qt.date,
CASE WHEN qt.status = 'assigned' THEN 1
WHEN qt.status = 'doing' THEN 2
WHEN qt.status = 'completed' THEN 3 END AS Status
FROM quiz q
LEFT JOIN quiz_transaction qt ON q.id = qt.id
) AS m
GROUP BY m.id, m.user_id, m.quiz_id;

Depending on your release SQLnServer supports Standard SQL's "Windowed Aggregate Functions". ROW_NUMBER will give you a single row:
SELECT
q.id
,q.user_id
,q.quiz_id
,qt.date
,qt.status
FROM quiz q
JOIN
(
SELECT
id
,date
,status
,ROW_NUMBER()
OVER (PARTITION BY id
ORDER BY Status DESC) as rn
FROM quiz_transaction
) as qt
ON q.id = qt.id
WHERE rn = 1
If you got more complex ordering rules you need to use a CASE:
,ROW_NUMBER()
OVER (PARTITION BY id
ORDER BY CASE Status WHEN 'completed' THEN 1
WHEN 'doing' THEN 2
WHEN 'assigned' THEN 3
END) as rn

try this
SELECT q.id, q.user_id, q.course_id, q1.date, qt.status FROM quiz q
LEFT JOIN
(Select id , convert(varchar,max(convert(varbinary,status ))) 'Status'
from quiz_transaction
group by id
) qt ON
q.id = qt.id
left join quiz_transaction q1 on q1.id = qt.id and q1.status=qt.status

Related

Need help for MS Access Select Request using 2 tables

For a "products reservation system", I have 2 tables :
"RD", for global reservations data (fieds: ID, CustomerID, Date, ...)
"RP", for reserved products data per reservation (fields: ID, RD_ID, ProductID, Status, ...). RD_ID fits with the ID in RD table (field for joining). Status field can have these values: O, C, S.
I need to extract (with 2 Select instructions) the list of reservations and the number of reservations for which all products have status 'O' .
Data example for RP:
ID | RD_ID | ProdID | Status
----------------------------
1 | 1 | 100 | O
2 | 1 | 101 | O
3 | 1 | 102 | O
4 | 2 | 105 | O
5 | 2 | 100 | S
6 | 3 | 101 | C
7 | 3 | 102 | O
In this example, Select statement should return only RD_ID 1
For the number of ID, the following request does not work because it also includes reservations with products having different status:
SELECT COUNT(rd.ID) FROM rd INNER JOIN rp ON rp.RD_ID = rd.ID WHERE rp.Status = 'O';
Could you help me for the right Select statement?
Thank you.
SELECT rd.ID, COUNT(rd.ID) CountOfRD, status
FROM rd INNER JOIN rp ON rp.RD_ID
GROUP BY rd.ID, status
Use not exists as follows:
Select t.* from your_table t
Where t.status = 'O'
And not exists (select 1 from your_table tt
Where t.rd_id = tt.rd_id
And t.status != tt.status)
You can also use group by and having as follows:
Select rd_id
From your_table t
Group by rd_id
Having sum(case when status <> 'O' then 1 end) > 0

Count how many times a value appears in tables SQL

Here's the situation:
So, in my database, a person is "responsible" for job X and "linked" to job Y. What I want is a query that returns: name of person, his ID and he number of jobs it's linked/responsible. So far I got this:
select id_job, count(id_job) number_jobs
from
(
select responsible.id
from responsible
union all
select linked.id
from linked
GROUP BY id
) id_job
GROUP BY id_job
And it returns a table with id in the first column and number of occurrences in the second. Now, what I can't do is associate the name of person to the table. When i put that in the "select" from beginning it gives me all the possible combinations... How can I solve this? Thanks in advance!
Example data and desirable output:
| Person |
id | name
1 | John
2 | Francis
3 | Chuck
4 | Anthony
| Responsible |
process_no | id
100 | 2
200 | 2
300 | 1
400 | 4
| Linked |
process_no | id
101 | 4
201 | 1
301 | 1
401 | 2
OUTPUT:
| OUTPUT |
id | name | number_jobs
1 | John | 3
2 | Francis | 3
3 | Chuck | 0
4 | Anthony | 2
Try this way
select prs.id, prs.name, count(*) from Person prs
join(select process_no, id
from Responsible res
Union all
select process_no, id
from Linked lin ) a on a.id=prs.id
group by prs.id, prs.name
I would recommend aggregating each of the tables by the person and then joining the results back to the person table:
select p.*, coalesce(r.cnt, 0) + coalesce(l.cnt, 0) as numjobs
from person p left join
(select id, count(*) as cnt
from responsible
group by id
) r
on r.id = p.id left join
(select id, count(*) as cnt
from linked
group by id
) l
on l.id = p.id;
select id, name, count(process_no) FROM (
select pr.id, pr.name, res.process_no from Person pr
LEFT JOIN Responsible res on pr.id = res.id
UNION
select pr.id, pr.name, lin.process_no from Person pr
LEFT JOIN Linked lin on pr.id = lin.id) src
group by id, name
order by id
Query ain't tested, give it a shot, but this is the way you want to go

row counter with condition in two different columns

I have the following tables with sport results (e.g. football):
tblGoals (RowId, GameRowIdm PlayerRowId, TeamRowId, GoalMinute)
RowId | GameRowId | PlayerRowId | TeamRowId | GoalMinute
--------------------------------------------------------
1 | 1 | 1 | 1 | 25
2 | 1 | 2 | 2 | 45
3 | 1 | 3 | 1 | 66
tblPlayers (RowId, PlayerName)
RowId | PlayerName
------------------
1 | John Snow
2 | Frank Underwood
3 | Jack Bauer
tblGames (RowId, TeamHomeRowId, TeamGuestRowId)
RowId | TeamHomeRowId | TeamGuestRowId | GameDate
---------------------------------------------------
1 | 1 | 2 | 2015-01-01
Now I want get a list of all goals. The list should look like this:
GoalMinute | PlayerName | GoalsHome | GoalsGuest
-----------------------------------------------------
25 | John Snow | 1 | 0
45 | Frank Underwood | 1 | 1
66 | Jack Bauer | 2 | 1
GoalsHome and GoalsGuest should be a counter of the shot goals for the team. So e.g. if you check the last row, the result is 2:1 for home team.
To get this list of goals, I used this statement:
SELECT t_gol.GoalMinute,
t_ply.PlayerName,
CASE WHEN
t_gol.TeamRowId = t_gam.TeamHomeRowId
THEN ROW_NUMBER() OVER (PARTITION BY t_gam.TeamHomeRowId ORDER BY t_gam.TeamHomeRowId)
END AS GoalsHome,
CASE WHEN
t_gol.TeamRowId = t_gam.TeamGuestRowId
THEN ROW_NUMBER() OVER (PARTITION BY t_gam.TeamGuestRowId ORDER BY t_gam.TeamGuestRowId)
END AS GoalsGuest
FROM dbo.tblGoalsFussball AS t_gol
LEFT JOIN dbo.tblPlayersFussball AS t_ply ON (t_ply.RowId = t_gol.PlayerRowId)
LEFT JOIN dbo.tblGames AS t_gam ON (t_gam.RowId = t_gol.GameRowId)
WHERE t_gol.GameRowId = #match_row
But what I get is this here:
GoalMinute | PlayerName | GoalsHome | GoalsGuest
-----------------------------------------------------
25 | John Snow | 1 | NULL
45 | Frank Underwood | NULL | 2
66 | Jack Bauer | 3 | NULL
Maybe ROW_NUMBER() is the wrong approach?
I would do the running total using sum() as a windowed aggregate function with the over ... clause, which works in SQL Server 2012+.
select
g.RowId, g.GameDate, t.GoalMinute, p.PlayerName,
GoalsHome = COALESCE(SUM(case when TeamRowId = g.TeamHomeRowId then 1 end) OVER (PARTITION BY gamerowid ORDER BY goalminute),0),
GoalsGuest = COALESCE(SUM(case when TeamRowId = g.TeamGuestRowId then 1 end) OVER (PARTITION BY gamerowid ORDER BY goalminute),0)
from tblGoals t
join tblPlayers p on t.PlayerRowId = p.RowId
join tblGames g on t.GameRowId = g.RowId
order by t.GameRowId, t.GoalMinute
Another approach (that also works in older versions) is to use a self-join and sum up the rows with lower goalminutes. For ease of reading I've used a common table expression to split the goals into two columns for home and guest team:
;with t as (
select
g.GoalMinute, g.PlayerRowId, g.GameRowId,
case when TeamRowId = ga.TeamHomeRowId then 1 end HomeGoals,
case when TeamRowId = ga.TeamGuestRowId then 1 end GuestGoals
from tblGoals g
join tblGames ga on g.GameRowId = ga.RowId
)
select
g.RowId, g.GameDate, t.GoalMinute, p.PlayerName,
GoalsHome = (select sum(coalesce(HomeGoals,0)) from t t2 where t2.GoalMinute <= t.GoalMinute and t2.GameRowId = t.GameRowId),
GoalsGuest = (select sum(coalesce(GuestGoals,0)) from t t2 where t2.GoalMinute <= t.GoalMinute and t2.GameRowId = t.GameRowId)
from t
join tblPlayers p on t.PlayerRowId = p.RowId
join tblGames g on t.GameRowId = g.RowId
order by t.GameRowId, t.GoalMinute
The CTE isn't necessary though, you could just as well use a derived table
Sample SQL Fiddle
I think the easiest way is with subqueries..
SELECT
tgs.GoalMinute,
tpl.PlayerName,
( SELECT
COUNT(t.RowId)
FROM
tblgoals AS t
WHERE t.GoalMinute <= tgs.GoalMinute
AND t.GameRowId = tgm.RowId
AND t.TeamRowId = tgm.TeamHomeRowId
) AS HomeGoals,
( SELECT
COUNT(t.RowId)
FROM
tblgoals AS t
WHERE t.GoalMinute <= tgs.GoalMinute
AND t.GameRowId = tgm.RowId
AND t.TeamRowId = tgm.TeamGuestRowId
) AS GuestGoals
FROM
tblgoals AS tgs
JOIN tblplayers AS tpl ON tgs.RowId = tpl.RowId
JOIN tblGames AS tgm ON tgm.RowId = tgs.GameRowId
ORDER BY tgs.GoalMinute

Select distinct where date is max

This feels really stupid to ask, but i can't do this selection in SQL Server Compact (CE)
If i have two tables like this:
Statuses Users
id | status | thedate id | name
------------------------- -----------------------
0 | Single | 2014-01-01 0 | Lisa
0 | Engaged | 2014-01-02 1 | John
1 | Single | 2014-01-03
0 | Divorced | 2014-01-04
How can i now select the latest status for each person in statuses?
the result should be:
Id | Name | Date | Status
--------------------------------
0 | Lisa | 2014-01-04 | Divorced
1 | John | 2014-01-03 | Single
that is, select distinct id:s where the date is the highest, and join the name. As bonus, sort the list so the latest record is on top.
In SQL Server CE, you can do this using a join:
select u.id, u.name, s.thedate, s.status
from users u join
statuses s
on u.id = s.id join
(select id, max(thedate) as mtd
from statuses
group by id
) as maxs
on s.id = maxs.id and s.thedate = maxs.mtd;
The subquery calculates the maximum date and uses that as a filter for the statuses table.
Use the following query:
SELECT U.Id AS Id, U.Name AS Name, S.thedate AS Date, S.status AS Status
FROM Statuses S
INNER JOIN Users U on S.id = U.id
WHERE S.thedate IN (
SELECT MAX(thedate)
FROM statuses
GROUP BY id);

How to increment value of a Column based on previous Row's value in SQL

I'm using SQL Server 2008.
I have two Tables: User_master and Item_master.
There is a User with user_id = 10.
|---------|
| user_id |
|---------|
| 10 |
|---------|
There are 5 Items with item_id = 20 to 24.
|---------|---------|------------|
| item_id | user_id | item_order |
|---------|---------|------------|
| 20 | 10 | 0 |
|---------|---------|------------|
| 21 | 10 | 0 |
|---------|---------|------------|
| 22 | 10 | 0 |
|---------|---------|------------|
| 23 | 10 | 0 |
|---------|---------|------------|
| 24 | 10 | 0 |
|---------|---------|------------|
There is one more column in Item_master that is item_order(int).
I want to place item_order = 0 to 4 in all these rows with only single query.
Is it possible?
EDIT :
item_id is not supposed to be in order.
For example, instead of 20,21,22,23,24; it could be 20,25,31,47,58.
You can use the row_number() window function to assign an increasing number to each row with the same user_id. A subquery is required because you cannot use window functions directly in the set clause.
update im
set im.item_order = im.rn
from (
select row_number() over (partition by user_id
order by item_id) - 1 as rn
, *
from item_master
) as im;
Live example at SQL Fiddle.
Extrapolating a little bit and since {item_id, user_id} is unique in the table, here is a generic solution:
UPDATE m
SET item_order = x.new_item_order
FROM item_master m
INNER JOIN (
SELECT [item_id], [user_id],
(ROW_NUMBER() OVER (PARTITION BY [user_id]
ORDER BY [item_id]))-1 AS [new_item_order]
FROM item_master
) x ON m.item_id = x.item_id AND m.user_id = x.user_id
SQL Fiddle example
This will set the item_order column in order of item_id for each user, starting at 0.
I've assumed you'd want to group by user to generate the number and that item_order column is already there, just needs updating?
update IM
set item_order = t.RowNumber
FROM Item_master IM
INNER JOIN
(select item_id , user_id , ROW_NUMBER() over(PARTITION BY user_id order by item_id ) -1 as 'RowNumber' from Item_master) T
ON T.item_id = IM.item_id