Optimized Query that selects 3 salaries from the past 2 years - sql

I have a query that will show the past 3 salaries within a 2 year period (if there are three) I have the query up and running the problem is, is its extremely slow... I'm wondering if there was a better way to write this query. I'm some-what new to oracle.
Here are my Tables
TABLE 1: Salary
ASSIGN_ID | start_date | end_date | salary |
1 | 11/27/2017 | 1/05/2018 | 50000.0 |
2 | 1/06/2018 | 6/08/2018 | 76000.0 |
3 | 6/09/2018 | 12/31/4712 | 80500.0 |
TABLE 2: Assignments
ASSIGN_ID | per_ID | start_date | end_date |
1 | 1 | 11/2/2017 | 1/05/2018 |
2 | 1 | 1/06/2018 | 6/08/2018 |
3 | 1 | 6/09/2018 | 12/31/4712 |
4 | 2 | 5/12/2016 | 7/18/2017 |
5 | 2 | 7/19/2017 | 12/31/4712 |
Table 3: Person
per_id | first_name | last_name |
1 | John | Smith |
2 | Jane | Doe |
Our end dates default to 12/31/4712 if they're are currently active in the assignment
My Query looks like this:
SELECT
per.first_name,
per.last_name,
(CASE WHEN sal1.start_date >= add_months(CURRENT_DATE, -24)
THEN sal1.salary
ELSE NULL END) oldest_salary,
(CASE WHEN sal2.start_date >= add_months(CURRENT_DATE, -24)
THEN sal2.salary
ELSE NULL END) prior_salary,
sal3.salary current_salary,
FROM
person per
INNER JOIN assignments asg1 ON asg1.per_id = per.per_id
INNER JOIN assignments asg2 ON asg2.per_id = asg1.per_id
INNER JOIN assignments asg3 ON asg3.per_id = asg2.per_id
INNER JOIN salary sal1 ON sal1.assign_id = asg1.assign_id
INNER JOIN salary sal2 ON sal2.assign_id = asg2.assign_id
INNER JOIN salary sal3 ON sal3.assign_id = asg3.assign_id
WHERE asg3.start_date =
(SELECT MAX(asg.start_date
FROM assignments asg
WHERE asg.assign_id = asg3.assign_id)
AND (asg3.start_date - 1) BETWEEN asg2.start_date and asg2.end_date
AND (asg2.start_date - 1) BETWEEN asg1.start_date and asg1.end_date
AND sal1.salary != sal2.salary
AND sal2.salary != sal3.salary
ORDER BY 2,1
Is there a simpler way to do this? because when I run my script it processes forever. I think I might need better joins. like I said i'm new and my understanding of joins is weak.

A simpler form:
SELECT
z.first_name,
z.last_name,
--typical cross-db compatible pivot method
MAX(CASE WHEN z.rown = 1 THEN z.salary END) as recentsalary,
MAX(CASE WHEN z.rown = 2 THEN z.salary END) as oldersalary,
MAX(CASE WHEN z.rown = 3 THEN z.salary END) as oldestsalary
FROM
(
SELECT
per.first_name,
per.last_name,
--number assignments from 1=recent to N older
row_number() over(partition by a.per_id order by a.start_date desc) rown
s.salary
FROM --join up all
person p
INNER JOIN assignments a ON a.per_id = p.per_id
INNER JOIN salary s ON s.assign_id = s.assign_id
WHERE a.end_date > ADD_MONTHS(SYSDATE, -36) --only recent 3 years
) z
WHERE z.rown <= 3 --only the most recent 3 assignments
GROUP BY first_name, last_name --achieve pivot
It works by:
Join up all data so people, assignments and salaries are known
Only consider assignments ended since 3 years ago
Number the assignments in youngest to oldest order (1=youngest)
Pivot the top 3 numberings into 3 columns for recent, older and oldest salary, per person

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

T-SQL Select Join 3 Tables

I'm currently working on a select query in T-SQL on SQL Server 2012. It's a complex query, I want to query a list from 3 tables. The result should look something like this:
Desired Output:
ProjectId | Title | Manager | Contact | StatusId
----------+-------------+-----------+-----------+-----------
1 | projectX | 1123 | 4453 | 1
2 | projectY | 2245 | 5567 | 1
3 | projectZ | 3335 | 8899 | 1
My 3 Tables:
1) Project: ProjectId, ProjectDataId, MemberVersionId
2) ProjectData: ProjectDataId, Title, StatusId
3) Members: MemberId, MemberVersionId, MemberTypeId, EmployeeId
The tricky part is, to implement versioning. Thus, over time the project Members can change, and it should always be possible to return to a previous version, that's why I use MemberVersionId as a foreign key inbetween Project and Members. The tables Project and ProjectData a linked with ProjectDataId.
Hence, 1 Project has 1 OfferData and 1 Project has N Members.
Some sample data:
Project
ProjectId | ProjectDataId | MemberVersionId |
----------+---------------+-----------------+
1 | 2 | 1 |
2 | 3 | 1 |
3 | 4 | 1 |
ProjectData
ProjectDataId | Title | StatusId
--------------+-------------+-----------
2 | projectX | 1
3 | projectY | 1
4 | projectZ | 1
Members: MemberTypeId 1 = Manager, MemberTypeId 2 = Contact, 3 = Other
MemberId | MemberVersionId | MemberTypeId | EmployeeId |
---------+-----------------+--------------+------------+
1 | 1 | 1 | 1123 |
2 | 1 | 2 | 4453 |
3 | 1 | 3 | 9999 |
4 | 2 | 1 | 2245 |
5 | 2 | 2 | 5567 |
6 | 2 | 3 | 9999 |
7 | 3 | 1 | 3335 |
8 | 3 | 2 | 8899 |
9 | 3 | 3 | 9999 |
My current query looks like this:
SELECT ProjectId, Title, EmployeeId AS Manager, EmployeeId AS Contact, StatusId
FROM [MySchema].[Project] a,
[MySchema].[ProjectData] b,
[MySchema].[Members] c
WHERE a.ProjectDataId = b.ProjectDataId
AND a.MemberVersionId = c.MemberVersionId
Unfortunately this doesn't work yet. Do you know how to solve this issue?
Thanks
Something like this?
SELECT
p.ProjectId,
pd.Title,
mm.EmployeeId AS Manager,
mc.EmployeeId AS Contact,
pd.StatusId
FROM
[MySchema].[Project] p
INNER JOIN [MySchema].[ProjectData] pd ON pd.ProjectDataId = p.ProjectDataId
INNER JOIN [MySchema].[Members] mm ON mm.MemberVersionId = p.MemberVersionId AND mm.MemberTypeId = 1
INNER JOIN [MySchema].[Members] mc ON mc.MemberVersionId = p.MemberVersionId AND mc.MemberTypeId = 2;
You can try this:
SELECT ProjectId, Title, C.EmployeeId AS Manager, d.EmployeeId AS Contact, StatusId
FROM [MySchema].[Project] a
INNER JOIN [MySchema].[ProjectData] b ON A.ProjectDataId=B.ProjectDataId
LEFT JOIN (SELECT * FROM [MySchema].[Members] WHERE MemberTypeID=1) c ON a.MemberVersionId=c.MemberVersionId
LEFT JOIN (SELECT * FROM [MySchema].[Members] WHERE MemberTypeID=2) d ON a.MemberVersionId=d.MemberVersionId
You must select members two times, one for the manager and another for contact:
SELECT ProjectId, Title, m.EmployeeId AS Manager, c.EmployeeId AS
Contact, StatusId
FROM [MySchema].[Project] a,
[MySchema].[ProjectData] b,
[MySchema].[Members] m
[MySchema].[Members] c
WHERE a.ProjectDataId = b.ProjectDataId
AND a.MemberVersionId = m.MemberVersionId and m.MemberTypeId = 1
AND a.MemberVersionId = c.MemberVersionId and c.MemberTypeId = 2
try this,
SELECT ProjectId, Title, cmanager.EmployeeId AS Manager, ccon.EmployeeId AS
Contact, StatusId
from [MySchema].[ProjectData] b
inner join [MySchema].[Project] a on b.ProjectDataId=a.ProjectDataId
left join [MySchema].[Members] cmanager on cmanager.MemberVersionId =
a.MemberVersionId and cmanager.MemberTypeId=1
left join [MySchema].[Members] ccon on ccon.MemberVersionId =
a.MemberVersionId and ccon.MemberTypeId=2
The simplest solution to your problem would be introducing additional field to Project table. You'd either call it LatestMemberVersion (int, holds the currently highest MemberVersionId), which would by the most up to date version of the relationship, your you can add even simpler IsLatestMemberVersion (bit, holds 1 if the record is the latest/active). You can compute both of them using ROW_NUMBER() OVER statement.
Then, the query would change to:
SELECT ProjectId, Title, EmployeeId AS Manager, EmployeeId AS Contact, StatusId
FROM [MySchema].[Project] a,
[MySchema].[ProjectData] b ON a.ProjectDataId = b.ProjectDataId
[MySchema].[Members] c ON a.MemberVersionId = c.MemberVersionId
WHERE
a.[IsLatestMemberVersion] = 1 -- alternative is a.[LatestMemberVersion] = a.[MemberVersionId]
Additionally, there are two more things you can try:
you might want to borrow ideas from data warehousing, namely you will want to have combination of Slowly Changing Dimension Type 1 and 2
you can try to use SQL Server features, such as Change Data Tracking. But I have no experience with that, so it's possible it'll lead to nowhere.
And one last piece of advice, if you can, never write join conditions into the WHERE clause. It is not readable and can lead to problems when you suddenly change JOIN to LEFT JOIN. Microsoft itself recommends using ON instead of WHERE when applicable.

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

SQL Finding multiple values difficulty

I'm trying to generate the correct SQL for a project.
Here is a sample dataset:
DateTime | EmpID | Function | Location
--------------------------------------------------
1/23/2015 2:00PM | 123 | 1 | 1
1/23/2015 2:10PM | 123 | 2 | 1
1/23/2015 2:20PM | 123 | 1 | 2
1/23/2015 2:40PM | 123 | 2 | 2
1/24/2015 2:00PM | 321 | 1 | 2
1/24/2015 2:15PM | 321 | 2 | 2
1/24/2015 2:30PM | 321 | 1 | 3
I need to pull a count of all records where functionid = 1 and location MUST EQUAL both 1 and 2. So the first row and the third row would be returned and considered a count of 1.
Hopefully I'm making sense with this. Basically I need to know how many times an employee was at two locations. Any help would be appreciated.
Group by EmpId and count locations.
SELECT *
FROM MyTable T1
WHERE Function = 1 AND
NOT EXISTS (SELECT 1
FROM MyTable T2
WHERE T1.EmpId = T2.EmpId AND
T1.Function = T2.Function AND
T2.Location NOT IN (1, 2))
GROUP BY EmpId
HAVING Count(DISTINCT Location) > 1
Have not tested it but think this will work
SELECT EmpID, COUNT(EmpID) AS NumOfTimes
From [Table Name]
WHERE FunctionID = 1 AND (Location = 1 OR Location = 2)
GROUP BY EmpID
HAVING NumOfTimes = 2
SELECT EmpID , COUNT(*) total, COUNT (CASE WHEN Location = 1 THEN 1 END) was_in_1,COUNT (CASE WHEN Location = 2 THEN 1 END) was_in_2
FROM table
WHERE Function = 1
GROUP BY EmpID
HAVING MAX(CASE WHEN Location = 1 THEN 1 ELSE 0 END) = MAX(CASE WHEN Location = 2 THEN 1 ELSE 0 END)
but if you would like to know if the employee was in any 2 locations then jarlh gave the right comment
group by EmpID
having count(distinct location) >= 2
SELECT A.EmpID, COUNT(*)
FROM YOURTABLENAME A
INNER JOIN YOURTABLENAME B
ON A.EmpID= B.EmpID
WHERE A.Location = 1 and B.Location = 2
and A.Function = B.Function and A.Function = 1
GROUP BY A.EmpID

How can I get all records from one table, even if there are no corresponding records in the JOINed table?

I have a table of [Users], simplified for this example:
uID | uName | uSalesRep
----+-------+----------
1 | John | 1
2 | Bob | 1
3 | Fred | 1
4 | Stu | 1
And a table of sales [Activity]:
aID | aDate | aUserID | aText
----+------------+---------+---------------
1 | 2013-10-09 | 1 | John did stuff
2 | 2013-10-14 | 2 | Bob did stuff
3 | 2013-10-17 | 3 | Fred did stuff
I want to get a list of all sales reps, together with their activity for the week beginning 14th October 2013, and here's how I'm trying to do it:
SELECT uID, Name, aID, aDate, aText
FROM [Users]
LEFT JOIN [Activity] ON uID = UserID
WHERE (aDate >= '2013-10-14' OR aDate = NULL)
AND (aDate <= '2013-10-18' OR aDate = NULL)
AND uSalesRep = 1
I used a LEFT JOIN in the hope of retrieving all reps, but I think this is being overridden by the aDate requirements. Including aDate = NULL includes reps with no activity at all, but reps who have activity outside the specified range are being omitted.
How can I get all the reps at least once, regardless of any activity they have recorded?
Thanks for your time.
When the filter applies to the joined table, you need to put the filter on the join, not in the where clause
SELECT uID, Name, aID, aDate, aText
FROM [Users]
LEFT JOIN [Activity] ON uID = UserID
AND (aDate >= '2013-10-14')
AND (aDate <= '2013-10-18')
WHERE uSalesRep = 1