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.
Related
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.
I'm trying to help a coworker with a large complex query. One of the joins returns the data correctly but performs poorly. If we run the join by itself, it returns in seconds. As part of the larger query, it takes forever if it ever works at all.
Can someone provide some pointers on how to optimize this? Would sub selects perform better than joins?
LEFT JOIN (SELECT cip.CaseID, dispo_sub2.CaseTitle, DATEPART(YEAR,MAX(cip.DispoDt)) DispoYr, cip.DispoDt, cip.DispoCode, cip.DispoDesc, LTRIM(rea.Reasons) Reasons, cip.CountInvPersAddDt
FROM
(SELECT dispo_sub.CaseID, dispo_sub.CaseTitle, dispo_sub.DispoDt, MAX(cip_sub2.CountInvPersAddDt) LastAddDt
FROM
(SELECT cip_sub.CaseID, c.casetitle, MAX(cip_sub.DispoDt) DispoDt
FROM jw50_CountInvPers cip_sub
INNER JOIN jw50_Case c on c.CaseID = cip_sub.CaseID
WHERE c.CaseTypeCode = 'TY706'
AND cip_sub.DispoDt IS NOT NULL
AND cip_sub.DispoCode IS NOT NULL
GROUP BY cip_sub.CaseID, c.CaseTitle) dispo_sub
INNER JOIN jw50_CountInvPers cip_sub2 on cip_sub2.CaseID = dispo_sub.CaseID and cip_sub2.DispoDt = dispo_sub.DispoDt
GROUP BY dispo_sub.CaseID, dispo_sub.CaseTitle, dispo_sub.DispoDt) dispo_sub2
INNER JOIN jw50_CountInvPers cip on cip.CaseID = dispo_sub2.CaseID AND cip.DispoDt = dispo_sub2.DispoDt AND cip.CountInvPersAddDt = dispo_sub2.LastAddDt
LEFT JOIN (SELECT
c_sub.CaseID,
STUFF((SELECT '; ' + ConditionTypeDesc
FROM jw50_Condition con
WHERE con.CaseID = c_sub.CaseId
ORDER BY ConditionTypeDesc
FOR XML PATH('')), 1, 1, '') [Reasons]
FROM jw50_Case c_sub
GROUP BY c_sub.CaseID) rea on rea.CaseID = dispo_sub2.CaseID
GROUP BY cip.CaseID, dispo_sub2.CaseTitle, cip.DispoDt, cip.DispoCode, cip.DispoDesc, rea.Reasons, cip.CountInvPersAddDt) dispo on dispo.CaseID = c.CaseID
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.
Im having performance issues with this query. If I remove the status column it runs very fast but adding the subquery in the column section delays way too much the query 1.02 min. How can I modify this query so it runs fast getting the desired data.
The reason I put that subquery there its because I needed the status for the latest activity, some activities have null status so I have to ignore them.
Establishments: 6.5k rows -
EstablishmentActivities: 70k rows -
Status: 2 (Active, Inactive)
SELECT DISTINCT
est.id,
est.trackingNumber,
est.NAME AS 'establishment',
actTypes.NAME AS 'activity',
(
SELECT stat3.NAME
FROM SACPAN_EstablishmentActivities eact3
INNER JOIN SACPAN_ActivityTypes at3
ON eact3.activityType_FK = at3.code
INNER JOIN SACPAN_Status stat3
ON stat3.id = at3.status_FK
WHERE eact3.establishment_FK = est.id
AND eact3.rowCreatedDT = (
SELECT MAX(est4.rowCreatedDT)
FROM SACPAN_EstablishmentActivities est4
INNER JOIN SACPAN_ActivityTypes at4
ON est4.establishment_fk = est.id
AND est4.activityType_FK = at4.code
WHERE est4.establishment_fk = est.id
AND at4.status_FK IS NOT NULL
)
AND at3.status_FK IS NOT NULL
) AS 'status',
est.authorizationNumber,
reg.NAME AS 'region',
mun.NAME AS 'municipality',
ISNULL(usr.NAME, '') + ISNULL(+ ' ' + usr.lastName, '')
AS 'created',
ISNULL(usr2.NAME, '') + ISNULL(+ ' ' + usr2.lastName, '')
AS 'updated',
est.rowCreatedDT,
est.rowUpdatedDT,
CASE WHEN est.rowUpdatedDT >= est.rowCreatedDT
THEN est.rowUpdatedDT
ELSE est.rowCreatedDT
END AS 'LatestCreatedOrModified'
FROM SACPAN_Establishments est
INNER JOIN SACPAN_EstablishmentActivities eact
ON est.id = eact.establishment_FK
INNER JOIN SACPAN_ActivityTypes actTypes
ON eact.activityType_FK = actTypes.code
INNER JOIN SACPAN_Regions reg
ON est.region_FK = reg.code --
INNER JOIN SACPAN_Municipalities mun
ON est.municipality_FK = mun.code
INNER JOIN SACPAN_ContactEstablishments ce
ON ce.establishment_FK = est.id
INNER JOIN SACPAN_Contacts con
ON ce.contact_FK = con.id
--JOIN SACPAN_Status stat ON stat.id = actTypes.status_FK
INNER JOIN SACPAN_Users usr
ON usr.id = est.rowCreatedBy_FK
LEFT JOIN SACPAN_Users usr2
ON usr2.id = est.rowUpdatedBy_FK
WHERE (con.ssn = #ssn OR #ssn = '*')
AND eact.rowCreatedDT = (
SELECT MAX(eact2.rowCreatedDT)
FROM SACPAN_EstablishmentActivities eact2
WHERE eact2.establishment_FK = est.id
)
--AND est.id = 6266
ORDER BY 'LatestCreatedOrModified' DESC
Try moving that 'activiy' query to a Left Join and see if it speeds it up.
I solved the problem by creating a temporary table and creating an index to it, this removed the need of the slow subquery in the select statement. Then I join the temp table as I do with normal tables.
Thanks to all.
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