Splitting SQL column into multiple columns based on value - sql-server

I have a table like the below
Opp_ID Role_Name Role_User_Name
---------------------------------------
1 Lead Person_one
1 Developer Person_two
1 Developer Person_three
1 Owner Person_four
1 Developer Person_five
I now need to split the Role_Name column to be 3 different columns based on the values. I need to make sure there are no NULL values so the table should like the below
Opp_ID Lead Developer Owner
--------------------------------------------------
1 Person_one Person_two Person_four
1 Person_one Person_three Person_four
1 Person_one Person_five Person_four
My code is currently:
SELECT
ID,
CASE WHEN Role_Name = 'Lead' THEN Role_User_Name ELSE NULL END AS Lead,
CASE WHEN Role_Name = 'Developer' THEN Role_User_Name ELSE NULL END AS Developer,
CASE WHEN Role_Name = 'Owner' THEN Role_User_Name ELSE NULL END AS Owner
FROM
[table1]
WHERE
Role_Name IN ('Lead','Developer','Owner')
Unfortunately this returns these results:
Opp_ID Lead Developer Owner
-------------------------------------------
1 Person_one NULL NULL
1 NULL Person_two NULL
1 NULL Person_three NULL
1 NULL NULL Person_four
1 NULL Person_five NULL
I assume to get this working you need to join the code back on itself but I can't seem to get it working.

To apply each developer and lead across your owners for an Opp_ID, you'll want something like:
SELECT o.opp_id
, o.Role_User_Name AS Owner
, l.Role_User_Name AS Lead
, d.Role_User_Name AS Developer
FROM t1 AS o
LEFT OUTER JOIN t1 l ON o.opp_id = l.opp_id AND l.Role_Name = 'Lead'
LEFT OUTER JOIN t1 d ON o.opp_id = d.opp_id AND d.Role_Name = 'Developer'
WHERE o.Role_Name = 'Owner'
https://dbfiddle.uk/?rdbms=sqlserver_2017&fiddle=da4daea062534245bed474f93ffafbb7

You can just switch to aggregation:
SELECT ID,
MAX(CASE WHEN Role_Name = 'Lead' THEN Role_User_Name END) AS Lead,
MAX(CASE WHEN Role_Name = 'Developer' THEN Role_User_Name END) AS Developer,
MAX(CASE WHEN Role_Name = 'Owner' THEN Role_User_Name END) AS Owner
FROM [table1]
WHERE Role_Name IN ('Lead', 'Developer', 'Owner')
GROUP BY ID;
If you could have multiple people, you might want to use STRING_AGG().
Note that I removed the ELSE NULL. This is redundant. With no ELSE clause, the CASE expression returns NULL when there is no match.

You can also use first_value function on Pivot results like below
See working demo
select
opp_id,
lead=COALESCE([lead],FIRST_VALUE([Lead]) over( order by opp_id )),
Developer=COALESCE([Developer],FIRST_VALUE([Developer]) over( order by opp_id )),
Owner=COALESCE([Owner],FIRST_VALUE([Owner]) over( order by opp_id ))
from
(select opp_id,Role_Name,
Role_User_Name,
rn=row_number() over( partition by Role_Name order by (select 1))
from
table1)
src
pivot
(max(Role_user_name) for role_name in ([Lead],[Developer],[Owner]))p

I prefer to use cross join
select o.opp_id,
o.role_user_name o,
l.role_user_name l,
d.role_user_name d
from t1 o cross join t1 l cross join t1 d
where o.role_name = 'Owner'
and l.role_name = 'Lead'
and d.role_name = 'Developer'
enter image description here

Related

SQL Server Overall Total in a group by

In my SQL Server Query, I am trying to count the number of employees per site. This works, but when I try to add in a percentage of total, it still groups by Site so it is inaccurate.
Is there an easier way to achieve this?
I am using this Query to create a view.
select Site.SiteName,
sum(case when Employee.ActiveStatus = 'Yes' then 1 else 0 end) as
"NumberOfEmployees",
CONVERT(decimal(6,2),(sum(case when Employee.ActiveStatus = 'Yes' then 1
else 0 end))/(convert(decimal(6,2),COUNT(EmployeeID)))) as PercentageOfEmps
from Employee
left join Site
on(Employee.SiteID=Site.SiteID)
GROUP BY Site.SiteName;
GO
You could use subquery:
select
Site.SiteName,
NumberOfEmployees = sum(case when Employee.ActiveStatus = 'Yes' then 1 else 0 end),
PercentageOfEmps = CONVERT(decimal(6,2),(sum(case when Employee.ActiveStatus = 'Yes' then 1
else 0 end))/(SELECT COUNT(EmployeeID) FROM Employee)
from Employee
left join Site
on Employee.SiteID=Site.SiteID
GROUP BY Site.SiteName;
I can't provide an answer for your scenario, as I don't have any sample data to use, therefore I've provided a small dataset.
One method is to use a CTE/Subquery to get a total number and then include the total in the GROUP BY. This method avoids 2 scans of the table:
WITH VTE AS(
SELECT *
FROM (VALUES(1,'Steve',1),
(2,'Jayne',1),
(3,'Greg',2),
(4,'Sarah',3)) V(EmpID, EmpName, SiteID)),
CTE AS(
SELECT V.EmpID,
V.EmpName,
V.SiteID,
COUNT(V.EmpID) OVER () AS TotalCount
FROM VTE V)
SELECT C.SiteID,
COUNT(C.EmpID) AS Employees,
COUNT(C.EmpID) / (C.TotalCount *1.0) AS Perc
FROM CTE C
GROUP BY C.SiteID,
C.TotalCount;
This script should help-
SELECT
Site.SiteName,
COUNT(EmployeeID) AS [NumberOfEmployees],
((COUNT(EmployeeID)*1.0)/(SELECT COUNT(*) FROM Employee WHERE ActiveStatus = 'Yes'))*100.00 as PercentageOfEmps
FROM Employee
INNER JOIN Site
ON Employee.SiteID = Site.SiteID
WHERE Employee.ActiveStatus = 'Yes'
GROUP BY Site.SiteName;
Data creation script
declare #Employee Table(EmployeeID int ,ActiveStatus nvarchar(20) ,SiteID int)
declare #Site Table(SiteName nvarchar(20) ,SiteID int)
insert into #Employee values(1,'Yes',101),(2,'Yes',101),(3,'Yes',102),(4,'Yes',102),
(5,'Yes',101)
insert into #Site values('Site1',101)
insert into #Site values('Site2',102)
//real script to get the %percentage
;with cte as
(
select s.SiteName,sum(case when e.ActiveStatus = 'Yes' then 1 else 0 end) as "NumberOfEmployees"
from #Employee e
left join #Site s
on(e.SiteID=s.SiteID)
GROUP BY s.SiteName
),
cte_sum as
(select sum(NumberOfEmployees) as total from cte )
select c.*, convert (decimal(6,2),c.NumberOfEmployees)/convert (decimal(6,2),cs.total)*100 from cte_sum cs, cte c;

SQL - conditional ISNULL statement in a query

I am using the following query to gather some information about each ProductId - note that a ProductId can contain several records in the dbo.Sales table:
SELECT
c.ProductId,
COUNT(*) as NumberOfRecords,
(SELECT
(ISNULL(NULLIF(c.Text, ''), 'FALSE'))) as TextFieldHasData
FROM dbo.Sales c
JOIN dbo.Sources s
ON c.ProductId = s.ProductId
AND s.SourceStatusId in (1,2)
GROUP BY c.ProductId, c.Status, s.SourceStatusId, c.Text
ORDER BY c.ProductId
I need to tweak the ISNULL part of the query, and I'm having trouble with the syntax; what I actually need to do is first check the NumberofRecordscount - if the the NumberofRecords count for a given result record is greater than 1, then the TextFieldHadData field for that record should just say 'N/A'. But, if the NumberofRecordscount for a given result record = 1, then it should check whether the c.Text field is NULL or blank. If it is NULL or Blank, the TextFieldHasData field would say 'FALSE.' If it is not NULL or blank, the TextFieldHasData field would say 'TRUE.'
Looking at your code, perhaps you are looking for something like the following (where you would be grouping up to ProductId level):
SELECT
c.ProductId
, COUNT(*) as NumberOfRecords
,
CASE
WHEN COUNT(*) > 1
THEN 'N/A'
ELSE
CASE
WHEN SUM(CASE WHEN ISNULL(c.Text, '') = '' THEN 0 ELSE 1 END) > 0
THEN 'TRUE'
ELSE 'FALSE'
END
END TextFieldHasData
FROM
dbo.Sales c
JOIN dbo.Sources s ON
c.ProductId = s.ProductId
AND s.SourceStatusId in (1, 2)
GROUP BY c.ProductId
ORDER BY c.ProductId
You can use the query:
I can not validate it as I don't have these tables, but it should work, unless you find a minor syntax error.
The idea is to use "case when ..." sql function
select v.productid,v.NumberOfRecords,
case
when v.NumberOfRecords>1 then 'N/A'
when v.NumberOfRecords=1 and isnull(v.TextFieldHasData,'') ='' then 'FALSE'
else 'TRUE' end [textfieldhasdata]
from(
SELECT
c.ProductId,
COUNT(*) as NumberOfRecords,
(SELECT
(ISNULL(NULLIF(c.Text, ''), 'FALSE'))) as TextFieldHasData
FROM dbo.Sales c
JOIN dbo.Sources s
ON c.ProductId = s.ProductId
AND s.SourceStatusId in (1,2)
GROUP BY c.ProductId, c.Status, s.SourceStatusId, c.Text) v
ORDER BY ProductId

How to use CTE to get a query repeated for multiple inputs?

I have the following query:
SELECT **top 1** account, date, result
FROM table_1 as t1
JOIN table_2 at t2 ON t1.accountId = t2.frn_accountId
WHERE accountID = 1
ORDER BY date
This query returns the result that I want however I want that result for multiple accountID. They query should return the top 1 value for each accountID.
The query that produce the list of the accountID-s is:
SELECT accountID from lskin WHERE refname LIKE '%BHA%' and isactive = 1
How can I write this query so it can produce the desired result? I have been playing around with CTE but haven't been able to make it correct. It doesn't have to be with CTE, I just thought it can be easier using CTE...
Here is CTE solution.
SELECT *
FROM (SELECT account
, date
, result
, ROW_NUMBER() OVER (PARTITION BY t1.accountId ORDER BY date DESC) AS Rownum
FROM table_1 AS t1
INNER JOIN table_2 AS t2
ON t1.accountId = t2.frn_accountId
INNER JOIN lskin AS l
ON l.accountID = t1.accountID
WHERE l.refname LIKE '%BHA%'
) a
WHERE a.Rownum = 1;
Use max on your date and group by the account, or what ever columns are appropriate.
SELECT
account,
DT = max(date),
result
FROM table_1 as t1
JOIN table_2 as t2 ON t1.accountId = t2.frn_accountId
JOIN lskin as l on l.accountID = t1.accountID
WHERE l.refname like '%BHA%'
GROUP BY
account
,result
If the grouping isn't correct, just join to a sub-query to limit it with max date. Just change the table names as necessary.
SELECT
account,
date,
result
FROM table_1 as t1
JOIN table_2 as t2 ON t1.accountId = t2.frn_accountId
JOIN lskin as l on l.accountID = t1.accountID
INNER JOIN (select max(date) dt, accountID from table_1 group by accountID) tt on tt.dt = t1.accountId and tt.accountId = t1.accountId
WHERE l.refname like '%BHA%'
Ignore the CTE at the top. That's just test data.
/* CTE Test Data */
; WITH table_1 AS (
SELECT 1 AS accountID, 'acc1' AS account UNION ALL
SELECT 2 AS accountID, 'acc2' AS account UNION ALL
SELECT 3 AS accountID, 'acc3' AS account
)
, table_2 AS (
SELECT 1 AS frn_accountID, 'new1' AS result, GETDATE() AS [date] UNION ALL
SELECT 1 AS frn_accountID, 'mid1' AS result, GETDATE()-1 AS [date] UNION ALL
SELECT 1 AS frn_accountID, 'old1' AS result, GETDATE()-2 AS [date] UNION ALL
SELECT 2 AS frn_accountID, 'new2' AS result, GETDATE() AS [date] UNION ALL
SELECT 2 AS frn_accountID, 'mid2' AS result, GETDATE()-1 AS [date] UNION ALL
SELECT 2 AS frn_accountID, 'old2' AS result, GETDATE()-2 AS [date] UNION ALL
SELECT 3 AS frn_accountID, 'new3' AS result, GETDATE() AS [date] UNION ALL
SELECT 3 AS frn_accountID, 'mid3' AS result, GETDATE()-1 AS [date] UNION ALL
SELECT 3 AS frn_accountID, 'old3' AS result, GETDATE()-2 AS [date]
)
, lskin AS (
SELECT 1 AS accountID, 'purple' AS refName, 1 AS isActive UNION ALL
SELECT 2 AS accountID, 'blue' AS refName, 1 AS isActive UNION ALL
SELECT 3 AS accountID, 'orange' AS refName, 0 AS isActive UNION ALL
SELECT 4 AS accountID, 'blue' AS refName, 1 AS isActive
)
,
/* Just use the below and remove comment markers around WITH to build Orders CTE. */
/* ; WITH */
theCTE AS (
SELECT s1.accountID, s1.account, s1.result, s1.[date]
FROM (
SELECT t1.accountid, t1.account, t2.result, t2.[date], ROW_NUMBER() OVER (PARTITION BY t1.account ORDER BY t2.[date]) AS rn
FROM table_1 t1
INNER JOIN table_2 t2 ON t1.accountID = t2.frn_accountID
) s1
WHERE s1.rn = 1
)
SELECT lskin.accountID
FROM lskin
INNER JOIN theCTE ON theCTE.accountid = lskin.accountID
WHERE lskin.refName LIKE '%blue%'
AND lskin.isActive = 1
;
EDITED:
I'm still making a lot of assumptions about your data structure. And again, make sure you're querying what you need. CTEs are awesome, but you don't want to accidentally filter out expected results.

Turn many to many relationship into single row

I have the following tables
Users
Id
FirstName
LastName
Sample Data
1,'Peter','Smith'
2,'John','Como'
Phones
Id
UserId
PhoneTypeId
Phone
ContactName
Sample data
1,1,4,'555-555-5551','Peter'
2,1,4,'555-555-5552','Paul'
3,1,4,'555-555-5553','Nancy'
4,1,4,'555-555-5554','Hellen'
PhoneTypes
Id
Type
with sample data
1 Home
2 Work
3 Cell
4 Emergency
I have to create following result
UserId, UserFirstName, UserLastName, FirstEmergencyContactName, FirstEmergencyContactPhone, SecondEmergencyContactName, SecondEmergencyContactPhone, ThirdEmergencyContactName, ThirdEmergencyContactPhone, FourthEmergencyContactName, FourthEmergencyContactPhone, FifthEmergencyContactName, FifthEmergencyContactPhone
How can I create a single row for every user with emergency contacts? Some of the users might have one emergency contact and others might have many, but I need only five of them.
This is called table pivoting. Since you want no more than 5 results, you can use conditional aggregation with row_number:
select id, firstname, lastname,
max(case when rn = 1 then contactname end) emergency_contact1,
max(case when rn = 1 then phone end) emergency_phone1,
max(case when rn = 2 then contactname end) emergency_contact2,
max(case when rn = 2 then phone end) emergency_phone2,
...
from (
select u.id, u.firstname, u.lastname, p.phone, p.contactname,
row_number() over (partition by u.id order by p.phonetypeid) rn
from users u
join phones p on u.id = p.userid
) t
group by id, firstname, lastname
Also you can use pivoting, without dynamic SQL and hard-coding, because you need only 5 contacts/phones. Example below:
;WITH cte AS (
SELECT p.UserId,
FirstName,
LastName,
CAST(ContactName as nvarchar(100)) as ContactName,
CAST(Phone as nvarchar(100)) as ContactPhone,
CAST(ROW_NUMBER() OVER (PARTITION BY p.UserId ORDER BY pt.Id) as nvarchar(100)) as RN
FROM Users u
INNER JOIN Phones p
ON p.UserId = u.Id
INNER JOIN PhoneTypes pt
ON pt.Id = p.PhoneTypeId
WHERE pt.Id = 4
)
SELECT *
FROM (
SELECT UserId,
FirstName,
LastName,
[Columns]+RN as [Columns],
[Values]
FROM cte
UNPIVOT (
[Values] FOR [Columns] IN (ContactName, ContactPhone)
) as unp
) as t
PIVOT (
MAX([Values]) FOR [Columns] IN (ContactName1,ContactPhone1,ContactName2,ContactPhone2,ContactName3,ContactPhone3,
ContactName4,ContactPhone4,ContactName5,ContactPhone5)
) as pvt
Output:
UserId FirstName LastName ContactName1 ContactPhone1 ContactName2 ContactPhone2 ContactName3 ContactPhone3 ContactName4 ContactPhone4 ContactName5 ContactPhone5
1 Peter Smith Peter 555-555-5551 Paul 555-555-5552 Nancy 555-555-5553 Hellen 555-555-5554 NULL NULL
2 John Cono Harry 555-555-5555 William 555-555-5556 John 555-555-5557 NULL NULL NULL NULL
I add some more contacts.

SQL Server - Select most recent records with condition

I have a table like this.
Table :
ID EnrollDate ExitDate
1 4/1/16 8/30/16
2 1/1/16 null
2 1/1/16 7/3/16
3 2/1/16 8/1/16
3 2/1/16 9/1/16
4 1/1/16 12/12/16
4 1/1/16 12/12/16
4 1/1/16 12/12/16
4 1/1/16 null
5 5/1/16 11/12/16
5 5/1/16 11/12/16
5 5/1/16 11/12/16
Need to select the most recent records with these conditions.
One and only one record has the most recent enroll date - select that
Two or more share same most recent enroll date and one and only one record has either a NULL Exit Date or the most recent Exit Date - Select the record with null. If no null record pick the record with recent exit date
Two or more with same enroll and Exit Date - If this case exists, don't select those record
So the expected result for the above table should be :
ID EnrollDate ExitDate
1 4/1/16 8/30/16
2 1/1/16 null
3 2/1/16 9/1/16
4 1/1/16 null
I wrote the query with group by. I am not sure how to select with the conditions 2 and 3.
select t1.* from table t1
INNER JOIN(SELECT Id,MAX(EnrollDate) maxentrydate
FROM table
GROUP BY Id)t2 ON EnrollDate = t2.maxentrydate and t1.Id=t2.Id
Please let me know what is the best way to do this.
Using the rank() window function, I think it's possible.
This is untested, but it should work:
select t.ID, t.EnrollDate, t.ExitDate
from (select t.*,
rank() over(
partition by ID
order by EnrollDate desc,
case when ExitDate is null then 1 else 2 end,
ExitDate desc) as rnk
from tbl t) t
where t.rnk = 1
group by t.ID, t.EnrollDate, t.ExitDate
having count(*) = 1
The basic idea is that the rank() window function will rank the most "recent" rows with a value of 1, which we filter on in the outer query's where clause.
If more than one row have the same "most recent" data, they will all share the same rank of 1, but will get filtered out by the having count(*) = 1 clause.
Use ROW_NUMBER coupled with CASE expression to achieve the desired result:
WITH Cte AS(
SELECT t.*,
ROW_NUMBER() OVER(
PARTITION BY t.ID
ORDER BY
t.EnrollDate DESC,
CASE WHEN t.ExitDate IS NULL THEN 0 ELSE 1 END,
t.ExitDate DESC
) AS rn
FROM Tbl t
INNER JOIN (
SELECT
ID,
COUNT(DISTINCT CHECKSUM(EnrollDate, ExitDate)) AS DistinctCnt, -- Count distinct combination of EnrollDate and ExitDate per ID
COUNT(*) AS RowCnt -- Count number of rows per ID
FROM Tbl
GROUP BY ID
) a
ON t.ID = a.ID
WHERE
(a.DistinctCnt = 1 AND a.RowCnt = 1)
OR a.DistinctCnt > 1
)
SELECT
ID, EnrollDate, ExitDate
FROM Cte c
WHERE Rn = 1
The ORDER BY clause in the ROW_NUMBER takes care of conditions 2 and 3.
The INNER JOIN and the WHERE clause take care of 1 and 4.
ONLINE DEMO
with B as (
select id, enrolldate ,
exitdate,
row_number() over (partition by id order by enrolldate desc, case when exitdate is null then 0 else 1 end, exitdate desc) rn
from ab )
select b1.id, b1.enrolldate, b1.exitdate from b b1
left join b b2
on b1.rn = b2.rn -1 and
b1.id = b2.id and
b1.exitdate = b2.exitdate and
b1.enrolldate = b2.enrolldate
where b1.rn = 1 and
b2.id is nULL
The left join is used to fullfill the 3) requirement. When record is returned then we don't want it.

Resources