What's a clean way to join tables with multiple repetitive joins? - sql-server

I am wondering if I'm doing this in the most efficient way possible.
I have a database table that houses custom data elements. To see what records use the data in the custom object table I have to join on 3 ID numbers, as well as specify the description. It's not that bad, but sometimes I can have up to 30 of these joins in my SQL script and that can be long. I was wondering if there is a cleaner way to code this? Maybe with a temp stored procedure or temp function?
SELECT
r.ID,
apples.status,
oranges.status,
bananas.status
FROM
record r
LEFT JOIN
custom_object apples ON apples.T_ID1 = r.T_ID1
AND apples.T_ID2 = r.T_ID2
AND apples.T_ID3 = r.T_ID3
AND apples.DESC = 'Apples'
LEFT JOIN
custom_object oranges ON oranges .ID1= r.T_ID1
AND oranges.T_ID2 = r.T_ID2
AND oranges.T_ID3 = r.T_ID3
AND oranges.DESC = 'Oranges'
LEFT JOIN
custom_object bananas ON bananas.T_ID1= r.T_ID1
AND bananas.T_ID2 = r.T_ID2
AND bananas.T_ID3 = r.T_ID3
AND bananas.DESC = 'Bananas'

Using a single join with a case expression seems like a much simpler approach here.
SELECT
r.ID,
AppleStatus = case when co.[DESC] = 'Apples' then co.status end,
OrangeStatus = case when co.[DESC]= 'Oranges' then co.status end,
BananaStatus = case when co.[DESC] = 'Bananas' then co.status end
FROM record r
LEFT JOIN custom_object co ON co.T_ID1 = r.T_ID1
AND co.T_ID2 = r.T_ID2
AND co.T_ID3 = r.T_ID3
AND co.[DESC] in ('Apples', 'Oanges', 'Bananas')

Conditional aggregation is a possibility:
SELECT
r.ID,
MAX(CASE WHEN co.[DESC] = 'Apples' THEN co.status END) AS applesStatus,
MAX(CASE WHEN co.[DESC] = 'Oranges' THEN co.status END) AS orangesStatus,
MAX(CASE WHEN co.[DESC] = 'Bananas' THEN co.status END) AS bananasStatus
FROM
record r
LEFT JOIN
custom_object co ON co.T_ID1 = r.T_ID1
AND co.T_ID2 = r.T_ID2
AND co.T_ID3 = r.T_ID3
AND co.[DESC] IN ('Apples', 'Oranges', 'Bananas') -- optional
GROUP BY r.ID
The same inside an OUTER APPLY will avoid the outer GROUP BY and allow more flexibility as you build out the rest of your query.
SELECT
r.ID,
statuses.*
FROM
record r
OUTER APPLY (
SELECT
MAX(CASE WHEN co.[DESC] = 'Apples' THEN co.status END) AS applesStatus,
MAX(CASE WHEN co.[DESC] = 'Oranges' THEN co.status END) AS orangesStatus,
MAX(CASE WHEN co.[DESC] = 'Bananas' THEN co.status END) AS bananasStatus
FROM custom_object co
WHERE co.T_ID1 = r.T_ID1
AND co.T_ID2 = r.T_ID2
AND co.T_ID3 = r.T_ID3
AND co.[DESC] IN ('Apples', 'Oranges', 'Bananas') -- optional
) statuses
A Common Table Expression (CTE) can also be used:
; WITH statuses AS (
SELECT
co.T_ID1, co.T_ID2, co.T_ID3,
MAX(CASE WHEN co.[DESC] = 'Apples' THEN co.status END) AS applesStatus,
MAX(CASE WHEN co.[DESC] = 'Oranges' THEN co.status END) AS orangesStatus,
MAX(CASE WHEN co.[DESC] = 'Bananas' THEN co.status END) AS bananasStatus
FROM custom_object co
WHERE co.[DESC] IN ('Apples', 'Oranges', 'Bananas') -- optional
GROUP BY co.T_ID1, co.T_ID2, co.T_ID3
)
SELECT
r.ID,
s.applesStatus,
s.orangesStatus,
s.bananasStatus
FROM
record r
LEFT JOIN
statuses s ON s.T_ID1 = r.T_ID1
AND s.T_ID2 = r.T_ID2
AND s.T_ID3 = r.T_ID3
The above all assume no duplicate properties per ID combination.
For best performance, you should define an index on custom_object(T_ID1, T_ID2, T_ID3, [DESC]).
Sample results:
ID
applesStatus
orangesStatus
bananasStatus
1
Apples1
Oranges1
Bananas1
2
Apples2
null
null
3
null
Oranges3
null
4
null
null
Bananas4
5
null
null
null
6
Apples6
null
Bananas6
See this db<>fiddle.

Related

add a column to the select query which could be null

I would like to add another column to my select query called description inside posts table,
the problem is that this value could be null.
to make it clear, I have already linked foreign keys from tables Users, PostType and Votes.
Attaching the query:
SELECT po.id,
po.title,
CONVERT(varchar, po.pDate, 104) AS pDate,
pt.type,
us.userName,
SUM(CASE WHEN vt.isLike = 1 THEN 1 ELSE 0 END) AS upvotes,
SUM(CASE WHEN vt.isLike = 0 THEN 1 ELSE 0 END) AS downvotes
FROM Posts po
INNER JOIN PostType pt ON po.typeId = pt.id
INNER JOIN Users us ON po.userId = us.id
LEFT OUTER JOIN Votes vt ON vt.postId = po.id
GROUP BY po.id,
po.pDate,
po.title,
pt.type,
us.userName;
How to avoid group by null?
You can make that column non-nullable on the fly. So in the SELECT clause just add one more column to display CASE WHEN po.description IS NULL THEN 'present' ELSE 'absent' END AS description and repeat it in the GROUP BY clause CASE WHEN po.description IS NULL THEN 'present' ELSE 'absent' END
SELECT po.id,
po.title,
CASE WHEN po.description IS NULL THEN 'present' ELSE 'absent' END AS description,
CONVERT(varchar, po.pDate, 104) AS pDate,
pt.type,
us.userName,
SUM(CASE WHEN vt.isLike = 1 THEN 1 ELSE 0 END) AS upvotes,
SUM(CASE WHEN vt.isLike = 0 THEN 1 ELSE 0 END) AS downvotes
FROM Posts po
INNER JOIN PostType pt ON po.typeId = pt.id
INNER JOIN Users us ON po.userId = us.id
LEFT OUTER JOIN Votes vt ON vt.postId = po.id
GROUP BY po.id,
po.pDate,
po.title,
CASE WHEN po.description IS NULL THEN 'present' ELSE 'absent' END,
pt.type,
us.userName;

Using ROW_NUMBER instead of MAX()

My question is related to this one. I have the following query to get the mother tongue and the fluent languages for an employee :
SELECT
lan.AdminFileId
,MAX(lan.MotherTongue) AS MotherTongue
,MAX(lan.Fluent) AS Fluent
FROM (SELECT
al.AdminFileId
,MAX(CASE
WHEN al.LanguageLevelId = 4 THEN l.Label
END) AS MotherTongue
,MAX(CASE
WHEN al.LanguageLevelId = 2 THEN l.Label
END) AS Fluent
FROM AF_Language al
LEFT JOIN AF_AdminFile aaf ON aaf.AdminFileId=al.AdminFileId
INNER JOIN Employee e ON e.AdminFileId=aaf.AdminFileId
LEFT JOIN Language l ON al.LanguageId = l.ID
GROUP BY al.AdminFileId
,l.Label
,al.LanguageLevelId) AS lan
GROUP BY lan.AdminFileId
The output is like below :
AdminFileId MotherTongue Fluent
45 English French
67 Spanish English
88 Arabic English
How can I use ROW_NUMBER to get the same result?
Essentially
I think you don't need to use ROW_NUMBER(), use AdminFileId only in GROUP BY clause will have only single entry :
SELECT al.AdminFileId,
MAX(CASE WHEN al.LanguageLevelId = 4 THEN l.Label END) AS MotherTongue,
MAX(CASE WHEN al.LanguageLevelId = 2 THEN l.Label END) AS Fluent
FROM AF_Language al LEFT JOIN
AF_AdminFile aaf
ON aaf.AdminFileId = al.AdminFileId LEFT JOIN -- Used LEFT JOIN INSTEAD OF INNER
Employee e
ON e.AdminFileId = aaf.AdminFileId LEFT JOIN
Language l
ON al.LanguageId = l.ID
GROUP BY al.AdminFileId;
EDIT : Using row_number :
SELECT al.AdminFileId, l.Label,
ROW_NUMBER() OVER (PARTITION BY al.AdminFileId
ORDER BY (CASE WHEN al.LanguageLevelId = 4
THEN 1 ELSE 2
END)
) AS Seq
FROM AF_Language al LEFT JOIN
AF_AdminFile aaf
ON aaf.AdminFileId = al.AdminFileId LEFT JOIN -- Used LEFT JOIN INSTEAD OF INNER
Employee e
ON e.AdminFileId = aaf.AdminFileId LEFT JOIN
Language l
ON al.LanguageId = l.ID
WHERE al.LanguageLevelId IN (4, 2);
Then you can use sub-query :
SELECT AdminFileId,
MAX(CASE WHEN Seq = 1 THEN Label END) AS MotherTongue,
MAX(CASE WHEN Seq = 2 THEN Label END) AS Fluent
FROM ( <Query>
) t
GROUP BY AdminFileId;

INSERT INTO SELECT with multiple CASE WHEN

I am trying to insert into table using select from another table with multiple case scenarios. Each person has more than one value, which gives multiple rows in the jointable. I want to select col2 based on col1 from the jointable.
The outcome so far, is 3 line of same person, 1 of the 3 values in each row.
See result here
INSERT INTO #temp (Name, WageNo, Tiltraedelses_dato, Jubilaeum ,Sabbatical, Anciennitet)
SELECT
e.FirstName,
e.WageSystemKey,
e.[StartDate],
CASE WHEN v.EmployeeCustomColumnId = 2 THEN v.Value END,
CASE WHEN v.EmployeeCustomColumnId = 3 THEN v.Value END,
CASE WHEN v.EmployeeCustomColumnId = 1 THEN v.Value END
FROM Employees e
LEFT JOIN EmployeeCustomValue v on e.EmployeeId = v.EmployeeId
SELECT * FROM #temp
I think you need a group by with conditional aggregation, something like this
INSERT INTO #temp (Name, WageNo, Tiltraedelses_dato, Jubilaeum ,Sabbatical, Anciennitet)
SELECT
e.FirstName,
e.WageSystemKey,
e.[StartDate],
max(CASE WHEN v.EmployeeCustomColumnId = 2 THEN v.Value END),
max(CASE WHEN v.EmployeeCustomColumnId = 3 THEN v.Value END),
max(CASE WHEN v.EmployeeCustomColumnId = 1 THEN v.Value END)
FROM Employees e
LEFT JOIN EmployeeCustomValue v on e.EmployeeId = v.EmployeeId
group by e.FirstName,
e.WageSystemKey,
e.[StartDate]
Here are a couple of other ways to go about this.
INSERT INTO #temp (Name, WageNo, Tiltraedelses_dato, Jubilaeum ,Sabbatical, Anciennitet)
SELECT
e.FirstName,
e.WageSystemKey,
e.[StartDate],
v2.Value,
v3.Value,
v1.Value
FROM Employees e
LEFT JOIN EmployeeCustomValue v1 on e.EmployeeId = v1.EmployeeId and v1.EmployeeCustomColumnId = 1
LEFT JOIN EmployeeCustomValue v2 on e.EmployeeId = v2.EmployeeId and v2.EmployeeCustomColumnId = 2
LEFT JOIN EmployeeCustomValue v3 on e.EmployeeId = v3.EmployeeId and v3.EmployeeCustomColumnId = 3
Or
INSERT INTO #temp (Name, WageNo, Tiltraedelses_dato, Jubilaeum ,Sabbatical, Anciennitet)
SELECT FirstName, WageSystemKey, [StartDate],
MAX(Jubilaeum) AS Jubilaeum, MAX(Sabbatical) AS Sabbatical,
MAX(Anciennitet) AS Anciennitet
FROM (SELECT
e.FirstName,
e.WageSystemKey,
e.[StartDate],
CASE WHEN v.EmployeeCustomColumnId = 2 THEN v.Value END AS Jubilaeum,
CASE WHEN v.EmployeeCustomColumnId = 3 THEN v.Value END AS Sabbatical,
CASE WHEN v.EmployeeCustomColumnId = 1 THEN v.Value END AS Anciennitet
FROM Employees e
LEFT JOIN EmployeeCustomValue v on e.EmployeeId = v.EmployeeId) AA
GROUP BY FirstName, WageSystemKey, [StartDate]

T-SQL Aggregate function subquery

I get the following error:
Cannot perform an aggregate function on an expression containing an aggregate or a subquery.
With this code:
SELECT
loc.Location
,COUNT(CASE
WHEN hr.SAC in (SELECT [SAC] FROM dbo.[Titles] WHERE [title] = 'XYZ')
THEN 1
ELSE NULL
END) AS XYZ_Trainee_Count
,COUNT(CASE
WHEN hr.SAC in (SELECT [SAC] FROM dbo.[Titles] WHERE [title] = 'ABC')
THEN 1
ELSE NULL
END) AS ABC_Trainee_Count
FROM
dbo.n_HRODS hr INNER JOIN dbo.Locations loc
ON loc.LocationID = hr.LocationID
INNER JOIN dbo.EmpData dat
ON dat.EmpID = hr.EmpID
WHERE dat.Trainee = 1
GROUP BY loc.Location
dbo.[Titles] is a view that combines two columns from two other tables. I'm basically doing it this way because the programmer before me did something like this:
,COUNT(CASE
WHEN SAC in ( lists about 30 items)
THEN 1
ELSE NULL
END)
Obviously, I don't want to list 30 items in that case statement... and when those items change for whatever reason in 3 years, then who's going to remember to go back in this code and updated those items? Nobody...
Thanks in advance for your help.
You can do this with a couple of extra LEFT OUTER JOINs to that title table:
SELECT
loc.Location
,COUNT(CASE WHEN titles1.[SAC] IS NOT NULL THEN 1 ELSE NULL END) AS XYZ_Trainee_Count
,COUNT(CASE WHEN titles2.[SAC] IS NOT NULL THEN 1 ELSE NULL END) AS ABC_Trainee_Count
FROM
dbo.n_HRODS hr INNER JOIN dbo.Locations loc
ON loc.LocationID = hr.LocationID
INNER JOIN dbo.EmpData dat
ON dat.EmpID = hr.EmpID
LEFT OUTER JOIN dbo.[Titles] titles1
ON titles1.[title]='XYZ' AND
hr.SAC = titles1.[SAC]
LEFT OUTER JOIN dbo.[Titles] titles2
ON titles2.[title]='ABC' AND
hr.sac = titles2.[SAC]
WHERE dat.Trainee = 1
GROUP BY loc.Location
Alternatively, if you are really married to those subqueries in your SELECT statement because the actual query is a big nightmare and the thought of monkeying with the joins is enough to make you faint, then you can just remove the aggregation from this query and shove it all into a subquery before aggregation:
SELECT location, count(XYZ_Trainee) AS XYZ_Trainee_Count, count(ABC_Trainee) as ABC_Trainee
FROM
(
SELECT
loc.Location
,CASE
WHEN hr.SAC in (SELECT [SAC] FROM dbo.[Titles] WHERE [title] = 'XYZ')
THEN 1
ELSE NULL
END AS XYZ_Trainee
,CASE
WHEN hr.SAC in (SELECT [SAC] FROM dbo.[Titles] WHERE [title] = 'ABC')
THEN 1
ELSE NULL
END AS ABC_Trainee
FROM
dbo.n_HRODS hr INNER JOIN dbo.Locations loc
ON loc.LocationID = hr.LocationID
INNER JOIN dbo.EmpData dat
ON dat.EmpID = hr.EmpID
WHERE dat.Trainee = 1
) sub
GROUP BY location
I would aim for the first solution though since it's going to be easier to maintain and probably get a better execution path from your RDBMS and run quicker as a result. Although... that's just a guess.
Very similar to JNevill first answer. But if you join with title once, you can check and count for any number of title you want.
SELECT
loc.Location
,COUNT(CASE WHEN t.[title] = 'XYZ' THEN 1 END) AS XYZ_Trainee_Count
,COUNT(CASE WHEN t.[title] = 'ABC' THEN 1 END) AS ABC_Trainee_Count
FROM
dbo.[n_HRODS] hr
INNER JOIN dbo.[Locations] loc
ON loc.LocationID = hr.LocationID
INNER JOIN dbo.[EmpData] dat
ON dat.EmpID = hr.EmpID
INNER JOIN dbo.[Titles] t
ON hr.SAC = t.[SAC]
WHERE dat.Trainee = 1
GROUP BY loc.Location

Add a WHERE clause in a complex SQL query

I want to pass a ShowRoomId value to the query below. The Employees table has a ShowRoomId column.
How can I do it?
My SQL query is as following:
SELECT *
FROM Employees A
OUTER APPLY (SELECT TOP 1 *
FROM EmployeeBasics B
WHERE (A.EmployeeID = B.EmployeeID)
ORDER BY B.BasicUpdateDate DESC) AS B
OUTER APPLY (
SELECT C.EmployeeId , count(*) AS TotalAbsent
FROM EmployeeAbsents C
WHERE C.AbsentDate BETWEEN '2016-05-01' AND '2016-05-30' AND A.EmployeeID = C.EmployeeID
GROUP BY C.EmployeeId
) AS C
OUTER APPLY (
SELECT EmployeeId,
SUM(CASE WHEN TransctionTypeId = 1 THEN Amount ELSE 0 END) AS Payment,
SUM(CASE WHEN TransctionTypeId = 2 THEN Amount ELSE 0 END) AS RecoverSalary,
SUM(CASE WHEN TransctionTypeId = 3 THEN Amount ELSE 0 END) AS RecoverCash
FROM dbo.EmployeeAdvances D
WHERE A.EmployeeID = D.EmployeeID
GROUP BY EmployeeId
) AS D
Simply use a WHERE clause at the end as following:
... YOUR SELECT ...
WHERE Col = ...YourCondition...
OR
Use WITH keyword to keep your current SELECT-statement in a cte. Then do your query on it.
WITH cte AS
(
... YOUR SELECT ...
)
SELECT *
FROM cte
WHERE Col = ...YourCondition...
OR
You can add your SELECT-statement in to parentheses and name it with an allias name. So you can do query on it too.
SELECT *
FROM
(
... YOUR SELECT ...
) t
WHERE t.Col = ...YourCondition...
As per Giorgi Nakeuri's advice, I added the WHERE clause at the end of the statement.
And it works for me. Revised code is here:
SELECT *
FROM Employees A
OUTER APPLY (SELECT TOP 1 *
FROM EmployeeBasics B
WHERE (A.EmployeeID = B.EmployeeID)
ORDER BY B.BasicUpdateDate DESC) AS B
OUTER APPLY (
SELECT C.EmployeeId , count(*) AS TotalAbsent
FROM EmployeeAbsents C
WHERE C.AbsentDate BETWEEN '2016-05-01' AND '2016-05-30' AND A.EmployeeID = C.EmployeeID
GROUP BY C.EmployeeId
) AS C
OUTER APPLY (
SELECT EmployeeId,
SUM(CASE WHEN TransctionTypeId = 1 THEN Amount ELSE 0 END) AS Payment,
SUM(CASE WHEN TransctionTypeId = 2 THEN Amount ELSE 0 END) AS RecoverSalary,
SUM(CASE WHEN TransctionTypeId = 3 THEN Amount ELSE 0 END) AS RecoverCash
FROM dbo.EmployeeAdvances D
WHERE A.EmployeeID = D.EmployeeID
GROUP BY EmployeeId
) AS D
WHERE A.ShowRoomId = 2

Resources