Combining multiple row values into a single column - sql

I appreciate that similar questions have been asked before but I'm unsure what to try next, and am under some pressure. I'm trying to combine multiple row values into a single column, and to do this am trying to use XML Path. The following is where I have got to, but I'm now simply displaying multiple instances of multiple subjects in a single column.
Using SQL Server, I'd like to combine all subjects for a given pupil (around 10) into the 'All Subjects' column. can someone point me to where I'm going wrong? Thanks, Gavin
SELECT distinct
P.FORM AS Class,
NAME.NAME AS [Pupil Name],
(SELECT ';' + SS.DESCRIPTION
FROM PUPIL P
INNER JOIN PUPIL_SET PS on PS.PUPIL_ID = P.PUPIL_ID
INNER JOIN SUBJECT_SET SS on SS.SUBJECT_SET_ID = PS.SUBJECT_SET_ID
FOR XML PATH('')) [All Subjects],
NAME_1.TITLE + ' ' + NAME_1.FIRST_NAMES + ' ' + NAME_1.SURNAME AS [Parent or Carer Name],
Replace(isnull(ADDRESS.HOUSE_STREET,'') + ', ' + isnull(ADDRESS.VILLAGE_AREA,'') + ', ' + isnull(ADDRESS.TOWN_CITY,'') + ', ' + isnull(ADDRESS.COUNTY,'') + ', ' + isnull(ADDRESS.COUNTRY,'') + ' ' + isnull(ADDRESS.POST_CODE,''),',,', '') AS Address,
--RELATIONSHIP.RANK,
CASE WHEN NAME.MAIN_ADDRESS_ID = NAME_1.MAIN_ADDRESS_ID THEN 'HOME' ELSE 'OTHER' END AS [Home or Other]
FROM PUPIL P
INNER JOIN NAME ON P.NAME_ID = NAME.NAME_ID
INNER JOIN ADDRESS ON NAME.MAIN_ADDRESS_ID = ADDRESS.ADDRESS_ID
INNER JOIN RELATIONSHIP ON NAME.NAME_ID = RELATIONSHIP.FROM_NAME_ID
INNER JOIN NAME AS NAME_1 ON RELATIONSHIP.TO_NAME_ID = NAME_1.NAME_ID
INNER JOIN PUPIL_SET PS on PS.PUPIL_ID = P.PUPIL_ID
INNER JOIN SUBJECT_SET SS on SS.SUBJECT_SET_ID = PS.SUBJECT_SET_ID
WHERE
(RELATIONSHIP.RANK=1 Or RELATIONSHIP.RANK=2)
AND P.ACADEMIC_YEAR=YEAR(DateAdd(m,-5,getDate()))
AND P.SUB_SCHOOL='030SEN'
AND P.IN_USE='y'
AND P.RECORD_TYPE='1'
AND Len(P.FORM)>0
and p.ACADEMIC_YEAR = 2015;

First, you only need subject_set and pupil_set in the subquery, not the outer query.
Second, you need a correlation clause. So, something like this:
SELECT P.FORM AS Class,
. . .
(SELECT ';' + SS.DESCRIPTION
FROM PUPIL_SET PS INNER JOIN
SUBJECT_SET SS
on SS.SUBJECT_SET_ID = PS.SUBJECT_SET_ID
WHERE PS.PUPIL_ID = P.PUPIL_ID
FOR XML PATH('')
) [All Subjects],
. . .
FROM PUPIL P INNER JOIN
NAME
ON P.NAME_ID = NAME.NAME_ID INNER JOIN
ADDRESS
ON NAME.MAIN_ADDRESS_ID = ADDRESS.ADDRESS_ID INNER JOIN
RELATIONSHIP
ON NAME.NAME_ID = RELATIONSHIP.FROM_NAME_ID INNER JOIN
NAME AS NAME_1
ON RELATIONSHIP.TO_NAME_ID = NAME_1.NAME_ID
WHERE . . .;
You should not need SELECT DISTINCT. You were getting duplicates because of the unnecessary joins.

Related

SQL to combine multiple rows into a single row

I am currently writing a SQL script - takes a business term, and all related synonyms. What it does is creates multiple rows (because there are multiple synonyms (can have other columns that could be multiple values as well.
What I am trying to do is to create a single row for every business term, and concatenate values (, delimited) so that I get one line item for each business term only.
Currently my SQL script is:
SELECT dbo.TblBusinessTerm.BusinessTerm, dbo.TblBusinessTerm.BusinessTermLongDesc,
dbo.TblBusinessTerm.DomainCatID, dbo.TblSystem.SystemName,
dbo.TblDomainCat.DataSteward, dbo.TblDomainCat.DomainCatName,
dbo.TblField.GoldenSource, dbo.TblField.GTS_table,
dbo.TblTableOwner.TableOwnerName, dbo.TblBusinessSynonym.Synonym
FROM dbo.TblTableOwner INNER JOIN
dbo.TblBusinessTerm INNER JOIN
dbo.TblBusinessSynonym ON dbo.TblBusinessTerm.BusinessTermID = dbo.TblBusinessSynonym.BusinessTermID INNER JOIN
dbo.TblField ON dbo.TblBusinessTerm.BusinessTermID = dbo.TblField.BusinessTermID INNER JOIN
dbo.TblSystem INNER JOIN
dbo.TblTable ON dbo.TblSystem.SystemID = dbo.TblTable.SystemID ON dbo.TblField.TableID = dbo.TblTable.TableID INNER JOIN
dbo.TblDomainCat ON dbo.TblBusinessTerm.DomainCatID = dbo.TblDomainCat.DomainCatID ON dbo.TblTableOwner.TableOwnerID = dbo.TblDomainCat.DataSteward
Is there an easy way to do this that takes performance into consideration - am new to SQL.
Thank you
I have managed to create a with statement that now concatenates my rows:
With syn as (
select [BusinessTermID],
syns = STUFF((SELECT ', ' + dbo.TblBusinessSynonym.Synonym
FROM dbo.TblBusinessSynonym
WHERE [BusinessTermID] = x.[BusinessTermID]
AND dbo.TblBusinessSynonym.Synonym <> ''
FOR XML PATH ('')),1,2,'')
FROM dbo.TblBusinessSynonym AS x
GROUP BY [BusinessTermID]
)
select * from syn
But now how can I use it in the above query where everything links?
Would want to replace dbo.TblBusinessSynonym.Synonym with the results from syn
Any SQL 2014 developers that can assist?
Write your with statement at the very top, without the select.
Then write your upper query as it is and change
INNER JOIN dbo.TblBusinessSynonym ON dbo.TblBusinessTerm.BusinessTermID = dbo.TblBusinessSynonym.BusinessTermID
to
INNER JOIN syn ON syn.BusinessTermID = dbo.TblBusinessTerm.BusinessTermID
That's it
With syn as (
select [BusinessTermID],
syns = STUFF((SELECT ', ' + dbo.TblBusinessSynonym.Synonym
FROM dbo.TblBusinessSynonym
WHERE [BusinessTermID] = x.[BusinessTermID]
AND dbo.TblBusinessSynonym.Synonym <> ''
FOR XML PATH ('')),1,2,'')
FROM dbo.TblBusinessSynonym AS x
GROUP BY [BusinessTermID]
)
SELECT dbo.TblBusinessTerm.BusinessTerm,
dbo.TblBusinessTerm.BusinessTermLongDesc,
dbo.TblBusinessTerm.DomainCatID, dbo.TblSystem.SystemName,
dbo.TblDomainCat.DataSteward, dbo.TblDomainCat.DomainCatName,
dbo.TblField.GoldenSource, dbo.TblField.GTS_table,
dbo.TblTableOwner.TableOwnerName, syn.syns
FROM dbo.TblTableOwner INNER JOIN
dbo.TblBusinessTerm INNER JOIN
syn ON dbo.TblBusinessTerm.BusinessTermID = syn.BusinessTermID INNER JOIN
dbo.TblField ON dbo.TblBusinessTerm.BusinessTermID = dbo.TblField.BusinessTermID INNER JOIN
dbo.TblSystem INNER JOIN
dbo.TblTable ON dbo.TblSystem.SystemID = dbo.TblTable.SystemID ON dbo.TblField.TableID = dbo.TblTable.TableID INNER JOIN
dbo.TblDomainCat ON dbo.TblBusinessTerm.DomainCatID = dbo.TblDomainCat.DomainCatID ON dbo.TblTableOwner.TableOwnerID = dbo.TblDomainCat.DataSteward
Please use STRING_AGG function. It combines record items in field ans set them in one record separated with specified delimiter.
Details are here:
https://learn.microsoft.com/en-us/sql/t-sql/functions/string-agg-transact-sql?view=sql-server-2017
Your query is complicated, so I will just post here sample data and how to deal with it in a manner you want. The operation is string aggregation with concatenation, in latest version there's string_agg function, that does the work for us. But, as you can't use this function, here's workaround:
select * into #tt
from (values (1, '1'),(1, '2'),(2, '1'),(2, '2')) A(id, someStr)
select id, (select someStr + ',' from #tt where id = [t].id for xml path('')) [grouped]
from #tt [t] group by id
Above query groups by Id and concaenates all corresponding rows in someStr column.

Search table twice in SQL Server

I've come across this issue several times now and I am not sure how to solve it. I have a query that needs to access the same table twice to pick out two different persons: a consultant and the customer contact.
Consultant + Customer query looks like this
select
CRM7.contact.contact_id as CustomerID,
CRM7.contact.name + ' ' + CRM7.person.firstname + ' ' + CRM7.person.LASTNAME as CustomerName,
CRM7.person.firstname + ' ' + CRM7.person.lastname as ConsultantName,
CRM7.udcontactsmall.long08 as WriteLic,
CRM7.udcontactsmall.long17 as ReadLic,
CRM7.udcontactsmall.long09 as HasMaint,
CRM7.udlist.name as BCVer
from
CRM7.contact,
CRM7.udcontactsmall
left join
CRM7.associate on CRM7.udcontactsmall.long11 = CRM7.associate.associate_id
left join
CRM7.person on CRM7.associate.person_id = CRM7.person.person_id
left join
CRM7.udlist on CRM7.udcontactsmall.long07 = CRM7.udlist.UDList_id
where
CRM7.contact.category_idx = '2' and
CRM7.contact.userdef_id = CRM7.udcontactsmall.udcontactsmall_id
order by
CRM7.contact.name
And the one that gets the contact for each customer looks like this
SELECT
C.NAME+' Kontakt '+ P.FIRSTNAME+' '+P.LASTNAME AS CONTACT
FROM
CRM7.PERSON P
LEFT OUTER JOIN
CRM7.CONTACT C ON P.CONTACT_ID = C.CONTACT_ID
WHERE
C.CATEGORY_IDX IN ('2','5')
AND P.RETIRED = 0
ORDER BY
C.NAME, P.LASTNAME, P.FIRSTNAME
Dataset:
http://oi61.tinypic.com/2j66cjq.jpg
How do I get a query which returns both the consultant for the customer and the customer contact?
This should serve your purpose:
select
CRM7.contact.contact_id as CustomerID
,CRM7.contact.name+' '+CRM7.person.firstname+' '+CRM7.person.LASTNAME as CustomerName
,CRM7.person.firstname+' '+CRM7.person.lastname as ConsultantName
,CRM7.udcontactsmall.long08 as WriteLic
,CRM7.udcontactsmall.long17 as ReadLic
,CRM7.udcontactsmall.long09 as HasMaint
,CRM7.udlist.name as BCVer
,C.NAME+' Kontakt '+ CRM7.person.FIRSTNAME+' '+ CRM7.person.LASTNAME AS CONTACT
from
CRM7.contact
,CRM7.udcontactsmall
left join CRM7.associate on CRM7.udcontactsmall.long11=CRM7.associate.associate_id
left join CRM7.person on CRM7.associate.person_id=CRM7.person.person_id
left join CRM7.udlist on CRM7.udcontactsmall.long07=CRM7.udlist.UDList_id
LEFT OUTER JOIN (SELECT CONTACT_ID,NAME FROM CRM7.CONTACT WHERE CATEGORY_IDX in ('2','5') AND P.RETIRED=0) C ON CRM7.person.CONTACT_ID=C.CONTACT_ID
where
CRM7.contact.category_idx='2'
and CRM7.contact.userdef_id=CRM7.udcontactsmall.udcontactsmall_id
order by
CRM7.contact.name
I have used a join on contact table from the second query and added it into the first one to get customer contacts:
LEFT OUTER JOIN
(SELECT CONTACT_ID, NAME
FROM CRM7.CONTACT
WHERE CATEGORY_IDX IN ('2','5') AND P.RETIRED = 0) C ON CRM7.person.CONTACT_ID = C.CONTACT_ID

SQL Cross Tab Query

Need help figuring out how to do a cross-tabulated report within one query. There are 3-4 tables involved but the users table may not need to be included in the query since we just need a count.
I have put together a screenshot of the table schema and data as an example which can be seen below:
What I need it to return is a query result that looks like:
So I can make a report that looks like:
I've tried to do cursor loops as it's the only way I can do it with my basic knowledge, but it's way too slow.
One particular report I'm trying to generate contains 32 rows and 64 columns with about 70,000 answers, so it's all about the performance of getting it down to one query and fast as possible.
I understand this may depend on indexes and so on but if someone could help me figure out how I could get this done in 1 query (with multiple joins?), that would be awesome!
Thanks!
SELECT MIN(ro.OptionText) RowOptionText, MIN(co.OptionText) RowOptionText, COUNT(ca.AnswerID) AnswerCount
FROM tblQuestions rq
CROSS JOIN tblQuestions cq
JOIN tblOptions ro ON rq.QuestionID = ro.QuestionID
JOIN tblOptions co ON cq.QuestionID = co.QuestionID
LEFT JOIN tblAnswers ra ON ra.OptionID = ro.OptionID
LEFT JOIN tblAnswers ca ON ca.OptionID = co.OptionID AND ca.UserID = ra.UserID
WHERE rq.questionText = 'Gender'
AND cq.questionText = 'How happy are you?'
GROUP BY ro.OptionID, co.OptionID
ORDER BY ro.OptionID, co.OptionID
This should be at least close to what you asked for. Turning this into a pivot will require dynamic SQL as SQL Server requires you to specify the actual value that will be pivoted into a column.
We cross join the questions and limit the results from each of those question references to the single question for the row values and column values respectively. Then we join the option values to the respective question reference. We use LEFT JOIN for the answers in case the user didn't respond to all of the questions. And we join the answers by UserID so that we match the row question and column question for each user. The MIN on the option text is because we grouped and ordered by OptionID to match your sequencing shown.
EDIT: Here's a SQLFiddle
For what it's worth, your query is complicated because you are using the Entity-Attribute-Value design pattern. Quite a few SQL Server experts consider that pattern to be problematic and to be avoided if possible. For instance see https://www.simple-talk.com/sql/t-sql-programming/avoiding-the-eav-of-destruction/.
EDIT 2: Since you accepted my answer, here's the dynamic SQL pivot solution :) SQLFiddle
DECLARE #SqlCmd NVARCHAR(MAX)
SELECT #SqlCmd = N'SELECT RowOptionText, ' + STUFF(
(SELECT ', ' + QUOTENAME(o.OptionID) + ' AS ' + QUOTENAME(o.OptionText)
FROM tblOptions o
WHERE o.QuestionID = cq.QuestionID
FOR XML PATH ('')), 1, 2, '') + ', RowTotal AS [Row Total]
FROM (
SELECT ro.OptionID RowOptionID, ro.OptionText RowOptionText, co.OptionID ColOptionID,
ca.UserID, COUNT(ca.UserID) OVER (PARTITION BY ra.OptionID) AS RowTotal
FROM tblOptions ro
JOIN tblOptions co ON ro.QuestionID = ' + CAST(rq.QuestionID AS VARCHAR(10)) +
' AND co.QuestionID = ' + CAST(cq.QuestionID AS VARCHAR(10)) + '
LEFT JOIN tblAnswers ra ON ra.OptionID = ro.OptionID
LEFT JOIN tblAnswers ca ON ca.OptionID = co.OptionID AND ca.UserID = ra.UserID
UNION ALL
SELECT 999999, ''Column Total'' RowOptionText, co.OptionID ColOptionID,
ca.UserID, COUNT(ca.UserID) OVER () AS RowTotal
FROM tblOptions ro
JOIN tblOptions co ON ro.QuestionID = ' + CAST(rq.QuestionID AS VARCHAR(10)) +
' AND co.QuestionID = ' + CAST(cq.QuestionID AS VARCHAR(10)) + '
LEFT JOIN tblAnswers ra ON ra.OptionID = ro.OptionID
LEFT JOIN tblAnswers ca ON ca.OptionID = co.OptionID AND ca.UserID = ra.UserID
) t
PIVOT (COUNT(UserID) FOR ColOptionID IN (' + STUFF(
(SELECT ', ' + QUOTENAME(o.OptionID)
FROM tblOptions o
WHERE o.QuestionID = cq.QuestionID
FOR XML PATH ('')), 1, 2, '') + ')) p
ORDER BY RowOptionID'
FROM tblQuestions rq
CROSS JOIN tblQuestions cq
WHERE rq.questionText = 'Gender'
AND cq.questionText = 'How happy are you?'
EXEC sp_executesql #SqlCmd
I think I see the problem. I know you can't modify the schema, but you need a conceptual table for the crosstab information such as which questionID is the rowHeader and which is the colHeader. You can create it in an external data source and join with the existing source or simply hard-code the table values in your sql.
you need to have 2 instances of the question/option/answer relations, one for each rowHeader and colHeader for each crosstab. Those 2 relations are joined by the userID.
this version has your outer joins:
sqlFiddle
this version doesn't have the crossTab table, just the row and col questionIDs hard-coded:
sqlFiddleNoTbl
The following piece of mess works with no hard-coded values but fails to show the rows where the count is 0.
This might however still work for your report.
;with stepone as(
SELECT
RANK() OVER(PARTITION BY a.UserId ORDER BY o.QuestionID) AS [temprank]
, o.QuestionID AS [QID1]
, o.OptionID AS [OID1]
, same.QuestionID
, same.OptionID
, a.UserId AS [IDUser]
, same.UserId
FROM
tblAnswers a
INNER JOIN
tblOptions o
ON a.OptionID = o.OptionID
INNER JOIN
tblQuestions q
ON o.QuestionID = q.QuestionID
INNER JOIN
(
SELECT
a.AnswerID
, a.OptionID
, a.UserId
, o.QuestionID
FROM
tblAnswers a
INNER JOIN
tblOptions o
ON a.OptionID = o.OptionID
) same
ON a.UserId = same.UserId AND a.AnswerID <> same.AnswerID
)
, stepthree AS(
SELECT
t.QID1, t.OID1, t.QuestionID, t.OptionID
, COUNT(UserId) AS myCount
FROM
stepone t
WHERE t.temprank = 1
GROUP BY
t.QID1, t.OID1, t.QuestionID, t.OptionID
)
SELECT
o1.OptionText AS [RowTest]
, o2.OptionText AS [ColumnText]
, t.myCount AS [Count]
FROM
stepthree t
INNER JOIN tblOptions o1
ON t.OID1 = o1.OptionID
INNER JOIN tblOptions o2
ON t.OptionID = o2.OptionID
ORDER BY t.OID1
Hope it helps, I enjoyed trying to do it.

Need help with a complex SQL query

I have a stored procedure that uses this select statement:
SELECT dbo.VendorProgram.id,
dbo.VendorProgram.CODE,
dbo.Programs.ProgramName
+ '-' + dbo.Divisions.Division
+ '-' + dbo.Vendors.Source
+ '-' + dbo.Mediums.Medium
+ '-' + dbo.VendorProgram.content
AS SourceDetail,
dbo.Vendors.Source,
dbo.Programs.ProgramName,
dbo.Divisions.Division,
dbo.Mediums.Medium,
dbo.VendorProgram.content,
dbo.VendorProgram.url,
dbo.VendorProgram.cost,
dbo.VendorProgram.Notes,
dbo.VendorProgram.StartDate,
dbo.VendorProgram.EndDate
FROM dbo.Programs
RIGHT OUTER JOIN dbo.VendorProgram
ON dbo.Programs.id = dbo.VendorProgram.programID
LEFT OUTER JOIN dbo.Vendors
ON dbo.VendorProgram.vendorID = dbo.Vendors.mappingID
LEFT OUTER JOIN dbo.Divisions
ON dbo.VendorProgram.divisionID = dbo.Divisions.id
LEFT OUTER JOIN dbo.Mediums
ON dbo.VendorProgram.mediumID = dbo.Mediums.id
Basically we have a system that put together a code for a vendor. It is made up of 5 IDs pulled from 5 separate tables. Those tables have actual text, for a vendor's name, a type of medium, etc... that match to each ID. So far nothing truly ground breaking in use here I think.
What I need to do is be able to write a query that uses paramaters, in text, to search those 5 separate tables, and find all the "vendor mappings" that could match. I am still new in the SQL world so I am not quite sure what that query would look like.
As an example of how I want to search. I enter into my search form the text "Face" for the vendor field. I would then expect the query runs a select against the vendor table itself to first find all possible vendor IDs with "face" in the name. Then it would need to select all rows from the combined table that have any of those IDs in them.
Hopefully this makes sense, and is possible. As always thanks for any help.
SELECT vp.id, vp.CODE,
p.ProgramName + '-' + d.Division + '-' + v.Source + '-' + m.Medium + '-' + vp.content AS SourceDetail,
v.Source,
p.ProgramName,
d.Division,
m.Medium,
vp.content,
vp.url,
vp.cost,
vp.Notes,
vp.StartDate,
vp.EndDate
FROM dbo.Vendors v
LEFT OUTER JOIN dbo.VendorProgram vp vp.vendorID = v.mappingID
LEFT OUTER JOIN dbo.Programs p ON p.id = vp.programID
LEFT OUTER JOIN dbo.Divisions d ON vp.divisionID = d.id
LEFT OUTER JOIN dbo.Mediums m ON vp.mediumID = m.id
WHERE v.Name LIKE '%Face%'
add
WHERE dbo.Vendors.ID in
(select ID From dbo.Vendors where dbo.Vendors.Field LIKE '%'+ #QueryText + '%'
If you want only the rows related to your search, switch to inner joins (or include some where not nulls) and in your join to Vendors, include:
AND Vendor.VendorName LIKE #PassedInParam
Where #PassedInParam would be passed into your proc and would be your search string with a '%' on either side, ie '%face%'

Unified records for database query with Sql

Basic need is if a record has an Attribute of "Urgent", then the attributevalue should be displayed in the Urgent column. If the record has an attribute value of "closed", then the attributevalue must be displayed in the "Closed" column.
I have a query below. My problem is that among the results I am getting back, there are two records with the same RequesterID (one with a valid value in "Urgent" column and one with a value in "Closed" colum)
My problem is that I need these two particular records to be displayed as one record.
Any ideas?
SELECT DISTINCT
r.RequesterID,
sr.ModifiedDate,
p.FirstName + ' ' + p.LastName AS RequesterName,
CASE
WHEN sa.Attribute = 'Urgent' THEN sa.AttributeValue
ELSE NULL
END AS Urgent,
CASE
WHEN sa.Attribute = 'Closed' THEN sa.AttributeValue
ELSE NULL
END AS Closed
FROM
Requester AS r
INNER JOIN SubRequester AS sr
ON r.RequesterID = sr.RequesterID
INNER JOIN SubRequesterAttribute AS sa
ON sr.SubRequesterID = sa.SubRequesterID
CROSS JOIN Personnel AS p
WHERE
(r.UserID = p.ContractorID
OR r.UserID = p.EmployeeID)
AND
(sa.Attribute IN ('Urgent', 'Closed'))
GROUP BY r.RequesterID, sr.ModifiedDate, p.FirstName, p.LastName,
sa.Attribute, sa.AttributeValue
You will need to join to your sub requester attribute table to the query twice. One with the attribute of Urgent and one with the attribute of Close.
You will need to LEFT join to these for the instances where they may be null and then reference each of the tables in your SELECT to show the relevent attribute.
I also wouldn't reccomend the cross join. You should perform your "OR" join on the personnel table in the FROM clause rather than doing a cross join and filtering in the WHERE clause.
EDIT: Sorry, my first response was a bit rushed. Have now had a chance to look further. Due to the sub requester and the sub requester attribute both being duplicates you need to split them both up into a subquery. Also, your modified date could be different for both values. So i've doubled that up. This is completely untested, and by no means the "optimum" solution. It's quite tricky to write the query without the actual database to check against. Hopefully it will explain what I meant though.
SELECT
r.RequesterID,
p.FirstName + ' ' + p.LastName AS RequesterName,
sra1.ModifiedDate as UrgentModifiedDate,
sra1.AttributeValue as Urgent,
sra2.ModifiedDate as ClosedModifiedDate,
sra2.AttributeValue as Closed
FROM
Personnel AS p
INNER JOIN
Requester AS r
ON
(
r.UserID = p.ContractorID
OR
r.UserID = p.EmployeeID
)
LEFT OUTER JOIN
(
SELECT
sr1.RequesterID,
sr1.ModifiedDate,
sa1.Attribute,
sa1.AttributeValue
FROM
SubRequester AS sr1
INNER JOIN
SubRequesterAttribute AS sa1
ON
sr1.SubRequesterID = sa1.SubRequesterID
AND
sa1.Attribute = 'Urgent'
) sra1
ON
sra1.RequesterID = r.RequesterID
LEFT OUTER JOIN
(
SELECT
sr2.RequesterID,
sr2.ModifiedDate,
sa2.Attribute,
sa2.AttributeValue
FROM
SubRequester AS sr2
INNER JOIN
SubRequesterAttribute AS sa2
ON
sr2.SubRequesterID = sa2.SubRequesterID
AND
sa2.Attribute = 'Closed'
) sra1
ON
sra2.RequesterID = r.RequesterID
SECOND EDIT: My last edit was that there were multiple SubRequesters as well as multiple Attribute, from your last comment you want to show all SubRequesters and the two relevent attributes? You can achieve this as follows.
SELECT
r.RequesterID,
p.FirstName + ' ' + p.LastName AS RequesterName,
sr.ModifiedDate,
sa1.AttributeValue as Urgent,
sa2.AttributeValue as Closed
FROM
Personnel AS p
INNER JOIN
Requester AS r
ON
(
r.UserID = p.ContractorID
OR
r.UserID = p.EmployeeID
)
INNER JOI N
SubRequester as sr
ON
sr.RequesterID = r.RequesterID
LEFT OUTER JOIN
SubRequesterAttribute AS sa1
ON
sa1.SubRequesterID = sr.SubRequesterID
AND
sa1.Attribute = 'Urgent'
LEFT OUTER JOIN
SubRequesterAttribute AS sa2
ON
sa2.SubRequesterID = sr.SubRequesterID
AND
sa2.Attribute = 'Closed'
Generally, if you have multiple rows and want to collapse them into one, GROUP BY is the basic tool to achieve that. It looks like you tried to go in that direction but didn't quite get there. What you want to do is group by the expressions that are duplicated between the rows, and apply group functions to the other expressions that will eliminate the NULL values. I used MIN in the example below but you could just as easily use MAX; the point is that since at most one of the rows will have a value for that expression, that value is both the minimum and the maximum.
SELECT
r.RequesterID,
sr.ModifiedDate,
p.FirstName + ' ' + p.LastName AS RequesterName,
MIN(
CASE
WHEN sa.Attribute = 'Urgent' THEN sa.AttributeValue
ELSE NULL
END
) AS Urgent,
MIN(
CASE
WHEN sa.Attribute = 'Closed' THEN sa.AttributeValue
ELSE NULL
END
) AS Closed
FROM
Requester AS r
INNER JOIN SubRequester AS sr
ON r.RequesterID = sr.RequesterID
INNER JOIN SubRequesterAttribute AS sa
ON sr.SubRequesterID = sa.SubRequesterID
CROSS JOIN Personnel AS p
WHERE
(r.UserID = p.ContractorID
OR r.UserID = p.EmployeeID)
AND
(sa.Attribute IN ('Urgent', 'Closed'))
GROUP BY r.RequesterID, sr.ModifiedDate, p.FirstName + ' ' + p.LastName