Count by unique ID, group by another - sql

I've inherited some scripts that count the number of people in a team by department; the current scripts create a table for each individual department and the previous user would copy/paste the data into Excel. I've been tasked to pull this report into SSRS so I need one table for all the departments by team.
Current Table
+-------+-----------+---------+
| Dept | DataMatch | Team |
+-------+-----------+---------+
| 01 | 4687Joe | Dodgers |
| 01 | 3498Cindy | RedSox |
| 01 | 1057Bob | Yankees |
| 01 | 0497Lucy | Dodgers |
| 02 | 7934Jean | Yankees |
| 02 | 4584Tom | Dodgers |
+-------+-----------+---------+
Desired Results
+-------+---------+--------+---------+
| Dept | Dodgers | RedSox | Yankees |
+-------+---------+--------+---------+
| 01 | 2 | 1 | 1 |
| 02 | 1 | 0 | 1 |
+-------+---------+--------+---------+
The DataMatch field is the unique identifier I will be counting. I started by wrapping each department in a CTE however this results in the Dept as the Column which would not work for my report, so I need to transpose my results and I haven't been able to figure that out. There are 60 departments and my query was getting very long.
Current query
SELECT Dept, DataMatch, Team INTO #temp_Team
FROM TeamDatabase
WHERE Status = 14
AND Team <> 'Missing'
;WITH A_cte (Team, Dept01)
AS
(
SELECT Team
, COUNT(DISTINCT datamatch) AS 'Dept01'
FROM #temp_Team
WHERE Dept = '01'
GROUP BY Team
),
B_cte (Team, Dept02) AS
(
SELECT Team
, COUNT(DISTINCT datamatch) AS 'Dept02'
FROM #temp_Team
WHERE Dept = '02'
GROUP BY Team
)
SELECT A_cte.Team
, A_cte.Dept01
, B_cte.Dept02
FROM A_cte
INNER JOIN B_cte
ON A_cte.Team=B_cte.Team
Which results in:
+----------------------------+-------+-------+
| Team | Prg01 | Prg02 |
+----------------------------+-------+-------+
| RedSox | 144 | 141 |
| Yankees | 63 | 236 |
| Dodgers | 298 | 196 |
+----------------------------+-------+-------+
I feel that using a pivot on my already very long query would be excessive and impact performance, 60 departments with over 30,000 rows.
What, mostly likely basic, step am I missing?
TL;DR - How do I count people by team and list by department?

I would replace the whole query with a dynamic pivot instead of adding a pivot to your CTEs.
You can add your Status/Team conditions to the SELECT inside the dynamic query at the bottom. They would be WHERE STATUS=14 AND TEAM !=''MISSING'' - note that is two single quotes to nest it within the string.
IF OBJECT_ID('tempdb..#data') IS NOT NULL DROP TABLE #data
CREATE TABLE #data (Dept VARCHAR(50), DataMatch NVARCHAR(50), Team VARCHAR(50))
INSERT INTO #data (Dept, DataMatch, Team)
VALUES ('01', '4687Joe','Dodgers'),
('01', '3498Cindy','RedSox'),
('01', '1057Bob','Yankees'),
('01', '0497Lucy','Dodgers'),
('02', '7934Jean','Yankees'),
('02', '4584Tom','Dodgers')
DECLARE #cols AS NVARCHAR(MAX),
#sql AS NVARCHAR(MAX)
SET #cols = STUFF(
(SELECT N',' + QUOTENAME(y) AS [text()]
FROM (SELECT DISTINCT Team AS y FROM #data) AS Y
ORDER BY y
FOR XML PATH('')),
1, 1, N'');
SET #sql = 'SELECT Dept, '+#cols+'
FROM (SELECT Dept, DataMatch, Team
FROM #data D) SUB
PIVOT (COUNT([DataMatch]) FOR Team IN ('+#cols+')) AS P'
PRINT #SQL
EXEC (#SQL)
In case you don't want to use a dynamic pivot, here is just a stand-alone query... again, add your conditions as you need.
SELECT Dept, Dodgers, RedSox, Yankees
FROM (SELECT Dept, DataMatch, Team
FROM #data D) SUB
PIVOT (COUNT([DataMatch]) FOR Team IN ([Dodgers], [RedSox], [Yankees])) AS P

I'm not sure I follow what relevance your existing query has, but to get from your current table to your desired results is a pretty straightforward usage of PIVOT:
SELECT *
FROM Table1
PIVOT(COUNT(DataMatch) FOR Team IN (Dodgers,RedSox,Yankees))pvt
Demo: SQL Fiddle
And this of course could be done dynamically if the teams list isn't static.

Related

SQL Server select query dynamic column output

I want to accomplish the following in SQL Server 2008
I have an article table like follows
| ArticleId | Description |
|-----------+-------------|
| 1 | Test |
|-----------+-------------|
And a order forecast table like this.
| ArticleId | Week | Order | Amount |
|-----------+--------------+--------+
| 1 | 51 | 1 | 0 |
| 1 | 52 | 2 | 150 |
| 1 | 1 | 3 | 0 |
| 1 | 2 | 4 | 200 |
| 1 | 3 | 5 | 0 |
|-----------+------+-------+--------+
Is there a way to create a query the produces a column for each record in the forecast table in the order of the order column. If it's possible how could I do that?
| ArticleId | Description | Week51 | Week52 | Week1 | Week2 | Week3 |
|-----------+-------------+-----------------+-------+-------+-------+
| 1 | Test | 0 | 150 | 0 | 200 | 0 |
|-----------+-------------+--------+--------+-------+-------+-------+
Provided the WEEK numbers and Order numbers are consistent, it is a small matter to maintain the column sequence.
You may notice I used #forecast and #article because I did not know your actual table names.
Example
Declare #SQL varchar(max) = '
Select *
From (
Select A.ArticleID
,D.Description
,B.*
From #forecast A
Join #article D on A.ArticleID=D.ArticleID
Cross Apply (values (''Week''+left(Week,4),Amount) ) B(Item,Value)
) A
Pivot (max([Value])
For [Item] in (' + Stuff((Select ','+QuoteName('Week'+left(Week,4))
From (Select Distinct top 100 [Order],Week From #forecast Order by [Order] ) A
For XML Path('')),1,1,'') + ') ) p'
Exec(#SQL);
--Print #SQL
Returns
ArticleID Description Week51 Week52 Week1 Week2 Week3
1 Test 0 150 0 200 0
In the general case, the SQL language strictly requires the number and data types of result columns be known at query compile time. What you're asking for here can't be known until after the execution plan is prepared and you start looking in the data.
Therefore, the best you'll be able to do generally is run this as three separate steps:
Run a query to tell you about the columns you'll want to use.
Use the results from #1 to compose a new query on the fly.
Execute the new query and return the results
Even then, this kind of pivot is typically better handled in your client code or report tool. The only good news is it's still possible to accomplish all this from most platforms with a single long SQL string.
For this specific situation, where you're clearly looking at week numbers, you can work around the issue by assuming all 53 possible weeks up front (not 52, because of partial weeks at the end of the year!), and writing a large SQL statement which manually accounts for all 55 columns (53 weeks + Article and Description).
You can try the following query using Pivot for your desired result.
Create Table Article (ArticleId Int, [Description] Varchar(10))
Insert Into Article Values (1, 'Test')
Create Table OrderForecast(ArticleId Int, [Week] Int, [Order] Int, Amount Int)
Insert Into OrderForecast Values (1, 51, 1, 0),(1, 52, 2, 150), (1, 1, 3, 0),(1, 2, 4, 200), (1, 3, 5,0)
Select ArticleId, [Description], Week51, Week52, Week1, Week2, Week3
from
(
select ArticleId, [Description], Amount, [Week]
from
(
SELECT OrderForecast.ArticleId, 'Week' + Convert(Varchar(10), OrderForecast.[Week]) as [Week], [Order], Amount,
Article.[Description] as [Description] FROM OrderForecast
Inner Join Article On OrderForecast.ArticleId = Article.ArticleId
)a
) d
pivot
(
max(Amount)
for [Week] in (Week51, Week52, Week1, Week2, Week3)
) piv;
The result will be as shown below
ArticleId Description Week51 Week52 Week1 Week2 Week3
-------------------------------------------------------------
1 Test 0 150 0 200 0
Here I have used query as table because week was in numbers like 1, 2 but you want the result in the Week1, Week2, etc. So I have concatenated word Week in the number and used it in the Pivot query.
You can find the live demo Live Demo Here

Crosstab query in SQL that compares and adds columns

I have a table in sql server that contains three columns: "date", "noon", and "3pm." The first column is self-explanatory, but the latter two contain the names of guest speakers at a venue according to the time they arrived. I want to write a cross-tab query that writes speaker names into the column header and counts the number of times that speaker spoke on that date.
Example
Date | Noon | 3pm
092916 | Tom | <null>
092816 | Dick | Tom
092716 | <null> | Suzy
Desired Output
Date | Dick | Tom | Suzy
092916 | <null> | 1 | <null>
092816 | 1 | 1 | <null>
092716 | <null> | <null> | 1
I can do this pretty easily with a crosstab query if I only select one time and put a count into the value category, but I'm having trouble with merging multiple times so that I can get an accurate count of who spoke on what day.
you can build your query dynamically.
this will create a count(case) statement for each name found in either the noon or 3pm column.. similar to COUNT(CASE WHEN 'Dick' IN ([Noon],[3pm]) THEN 1 END) as [Dick]
DECLARE #speakers NVARCHAR(MAX),
#sql NVARCHAR(MAX)
SET #speakers = STUFF((
SELECT ',COUNT(CASE WHEN ''' + [Name] + ''' IN ([Noon],[3pm]) THEN 1 END) as ' + QUOTENAME([Name])
FROM (SELECT [Noon] AS [Name] FROM Table1
UNION ALL SELECT [3pm] FROM Table1) t
GROUP BY t.Name
FOR XML PATH('')
), 1, 1, '')
SET #sql = N'SELECT Date, ' + #speakers + ' FROM Table1 GROUP BY Date'
--Print #sql to see what's going on
EXEC(#sql)
You could use this query:
select *
from (
select date, noon as speaker, count(*) as times
from events
group by date, noon
union all
select date, [3pm], count(*)
from events
group by date, [3pm]
) as u
pivot (
sum(times)
for speaker in ([Dick], [Tom], [Suzy])
) as piv
order by date desc;
... which gives you a count per cell (null, 1 or 2):
Date | Dick | Tom | Suzy
092916 | <null> | 1 | <null>
092816 | 1 | 1 | <null>
092716 | <null> | <null> | 1

Using XML PATH to manipulate row data when grouping

In a sql query I'm trying to do two things.
I have a table Attendance like this
TABLE
EID | PID | In_Time | Out_Time | Shift
__________________________________________________________
100 | S001 | 2014-05-01 07:10 | 2014-05-01 19:20 | D
100 | S001 | 2014-05-04 07:00 | 2014-05-04 19:00 | D
100 | S001 | 2014-05-04 19:00 | 2014-05-05 07:00 | N
EID -EmployeeID
PID -PointID (Location)
D - Day Shift
N - Night Shift
When I group by all fields except Shift, (When grouping only the DatePart of In_Time fields will be taken) I want to get this
INTERMEDIATE STEP
EID | DAY | Shift |
___________________
100 | 01 | D |
100 | 04 | D/N |
Finally I want to PIVOT this to get following result
EXPECTED FINAL RESULT
EmployeeID | 01 | 02 | 03 | 04 |
__________________________________
100 | D | _ | _ | D/N |
I Use following query in this purpose but I'm getting slightly different result.
SELECT EID AS EmployeeID, [1],[2],[3],[4]
FROM (
SELECT
EID, datepart(dd,in_time) as [DAY],
STUFF((
SELECT '/ ' + Shift
FROM Attendance
WHERE ([in_time] = Results.[in_time] )
FOR XML PATH(''),TYPE).value('(./text())[1]','VARCHAR(MAX)')
,1,2,'') AS Shifts
FROM Attendance Results
WHERE EID = '100' AND PID ='C002'
GROUP BY EID , in_time
) AS SourceTable
PIVOT
(
MAX (Shifts )
FOR [DAY] IN ( [1],[2],[3],[4])
) AS PivotTable
This is the result of the query
EmployeeID | 01 | 02 | 03 | 04 |
________________________________
100 | D | _ | _ | N/N |
So something wrong in my query and could you pleas help me to sort this out? What's I'm missing in this query? Do you know any better way to do this?
EDIT: I just realized that the above code works well as long as the Attendance table contains records relevant to a single employee and a single Point (PID).
If the above table has details of several employees (EIDs) who works at different locations (PIDs) then the out put is wrong. So I see the code is not consistent but my knowledge in sql seems not enough to sort this out with out a help :(
the below answer may help you. The SQL looks lengthy, but the logic is simple, just grouping D and N data and finally joining. may be you can work from this.
DECLARE #Lu_Dt table
(id int identity(1,1),Dt Date)
INSERT INTO #Lu_Dt VALUES('2014-05-01'),
('2014-05-02'),('2014-05-03'),
('2014-05-04'),('2014-05-05')
DECLARE #tab table
(EID int,PID varchar(15),In_Time datetime,Out_Time datetime,[Shift] Char(1))
INSERT INTO #tab Values
(100,'S001','2014-05-01 07:10','2014-05-01 19:20','D'),
(100,'S001','2014-05-04 07:00','2014-05-04 19:00','D'),
(100,'S001','2014-05-04 19:00','2014-05-05 07:00','N')
SELECT * FROM #tab
DECLARE #refTab table
(id int identity(1,1),EID int, Dt Date,[shift] varchar(3))
INSERT INTO #refTab
SELECT Lu.EID,Lu.Dt,coalesce(LU1.[Flag],LU2.[Flag]) [shift]
FROM (SELECT *,100 EID FROM #Lu_Dt) Lu
LEFT JOIN (SELECT D.EID,D.In_Time,'D/N' [Flag]
FROM (SELECT EID,CAST(In_Time AS DATE) In_Time FROM #tab Where Shift = 'D') D
JOIN (SELECT EID,CAST(In_Time AS DATE) In_Time FROM #tab Where Shift = 'N') N
ON D.In_Time = N.In_Time AND D.EID = N.EID) LU1 ON Lu.Dt = LU1.In_Time
LEFT JOIN (SELECT CAST(In_Time AS DATE) In_Time,'D' [Flag] FROM #tab Where Shift = 'D') LU2 ON Lu.Dt = LU2.In_Time
LEFT JOIN (SELECT CAST(In_Time AS DATE) In_Time,'N' [Flag] FROM #tab Where Shift = 'N') LU3 ON Lu.Dt = LU3.In_Time
SELECT * FROM #refTab
SELECT *
FROM (SELECT EID,Dt,[shift] from #refTab) As src
PIVOT
(MAX([shift]) for Dt in
( [2014-05-01],
[2014-05-02],
[2014-05-03],
[2014-05-04],
[2014-05-05])) AS PivotTable;
Result
As there is a lot of hard coding, it cant be used as such.Hope this helped.

Formatting SQL Output (Pivot)

This is running on SQL Server 2008.
Anyway, I have sales data, and I can write a query to get the output to look like this:
id | Name | Period | Sales
1 | Customer X | 2013/01 | 50
1 | Customer X | 2013/02 | 45
etc. Currently, after running this data, I am rearranging the data in the code behind so that the final output looks like this:
id | Name | 2013/01 | 2013/02
1 | Customer X | 50 | 40
The issues are:
The date (YYYY/MM) range is an input from the user.
If the user selects more outputs (like, say, address, and a ton of other possible fields relating to that customer), that information is duplicated in every line. When you're doing 10-15 items per line, over a period of 5+ years, for 50000+ users, this causes problems with running out of memory, and is also inefficient.
I've considered pulling only the necessary data (the customer id -- how they're joined together, the period, and the sales figure), and then after the fact running a separate query to get the additional data. This doesn't seem like it would be efficient though, but it's a possibility.
The other, which is what I'm thinking should be the best option, would be to rewrite my query to go ahead and do what my current code behind is doing, and pivot the data together, that way the customer data is never duplicated and I'm not moving a lot of unnecessary data around.
To give a better example of what I'm working with, let's assume these tables:
Address
id | HouseNum | Street | Unit | City | State
Customer
id | Name |
Sales
id | Period | Sales
So I would like to join these tables on the customer id, display all of the address data, assume the user inputs "2012/01 -- 2012/12", I can translate that into 2012/01, 2012/02 ... 2012/12 in my code behind to input into the query before it executes, so I have that available.
What I want it to look like would be:
id | Name | HouseNum | Street | City | State | 2012/01 | 2012/02 | ... | 2012/12
1 | X | 100 | Main St. | ABC | DEF | 30 | | ... | 20
(no sales data for that customer on 2012/02 -- if any of the data is blank I want it to be a blank string "", not a NULL)
I realize I may not be explaining this the best way possible, so just let me know and I'll add more information. Thank you!
edit: oh, one last thing. Would it be possible to add a Min, Max, Avg, & Total columns to the end, which sum up all of the pivoted data? It wouldn't be a big deal to do it on the code behind, but the more sql server can do for me the better, imo!
edit: One more, the period is in the tables as "2013/01" etc, but I'd like to rename them to "Jan 2013" etc, if it's not too complicated?
You can implement the PIVOT function to transform the data from rows into columns. You can use the following to get the result:
select id,
name,
HouseNum,
Street,
City,
State,
isnull([2013/01], 0) [2013/01],
isnull([2013/02], 0) [2013/02],
isnull([2012/02], 0) [2012/02],
isnull([2012/12], 0) [2012/12],
MinSales,
MaxSales,
AvgSales,
TotalSales
from
(
select c.id,
c.name,
a.HouseNum,
a.Street,
a.city,
a.state,
s.period,
s.sales,
min(s.sales) over(partition by c.id) MinSales,
max(s.sales) over(partition by c.id) MaxSales,
avg(s.sales) over(partition by c.id) AvgSales,
sum(s.sales) over(partition by c.id) TotalSales
from customer c
inner join address a
on c.id = a.id
inner join sales s
on c.id = s.id
) src
pivot
(
sum(sales)
for period in ([2013/01], [2013/02], [2012/02], [2012/12])
) piv;
See SQL Fiddle with Demo.
If you have a unknown number of period values that you want to transform into column, then you will have to use dynamic SQL to get the result:
DECLARE #cols AS NVARCHAR(MAX),
#colsNull AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT distinct ',' + QUOTENAME(period)
from Sales
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
select #colsNull = STUFF((SELECT distinct ', IsNull(' + QUOTENAME(period) + ', 0) as '+ QUOTENAME(period)
from Sales
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT id,
name,
HouseNum,
Street,
City,
State,' + #colsNull + ' ,
MinSales,
MaxSales,
AvgSales,
TotalSales
from
(
select c.id,
c.name,
a.HouseNum,
a.Street,
a.city,
a.state,
s.period,
s.sales,
min(s.sales) over(partition by c.id) MinSales,
max(s.sales) over(partition by c.id) MaxSales,
avg(s.sales) over(partition by c.id) AvgSales,
sum(s.sales) over(partition by c.id) TotalSales
from customer c
inner join address a
on c.id = a.id
inner join sales s
on c.id = s.id
) x
pivot
(
sum(sales)
for period in (' + #cols + ')
) p '
execute(#query)
See SQL Fiddle with Demo. These give the result:
| ID | NAME | HOUSENUM | STREET | CITY | STATE | 2012/02 | 2012/12 | 2013/01 | 2013/02 | MINSALES | MAXSALES | AVGSALES | TOTALSALES |
---------------------------------------------------------------------------------------------------------------------------------------------------
| 1 | Customer X | 100 | Maint St. | ABC | DEF | 0 | 20 | 50 | 45 | 20 | 50 | 38 | 115 |
| 2 | Customer Y | 108 | Lost Rd | Unknown | Island | 10 | 0 | 0 | 0 | 10 | 10 | 10 | 10 |

SQL: Putting an individuals distinct diagnosis into one horizontal row

I'm using Microsoft SQL Server 2008 for a mental health organization.
I have a table that lists all of out clients and their diagnoses, but each diagnoses that a client has is in a new row. I want them all to be in a single row listed out horizontally with the date for each diagnosis. Some people have just one diagnosis, some have 20, some have none.
Here's an example of how my data sort of looks now (only with a lot few clients, we have thousands):
And Here's the format I'd like it to end up:
Any solutions you could offer or hints in the right direction would be great, thanks!
In order to get the result, I would first unpivot and then pivot your data. The unpivot will take your date and diagnosis columns and convert them into rows. Once the data is in rows, then you can apply the pivot.
If you have a known number of values, then you can hard-code your query similar to this:
select *
from
(
select person, [case#], age,
col+'_'+cast(rn as varchar(10)) col,
value
from
(
select person,
[case#],
age,
diagnosis,
convert(varchar(10), diagnosisdate, 101) diagnosisDate,
row_number() over(partition by person, [case#]
order by DiagnosisDate) rn
from yourtable
) d
cross apply
(
values ('diagnosis', diagnosis), ('diagnosisDate', diagnosisDate)
) c (col, value)
) t
pivot
(
max(value)
for col in (diagnosis_1, diagnosisDate_1,
diagnosis_2, diagnosisDate_2,
diagnosis_3, diagnosisDate_3,
diagnosis_4, diagnosisDate_4)
) piv;
See SQL Fiddle with Demo.
I am going to assume that you will have an unknown number of diagnosis values for each case. If that is the case, then you will need to use dynamic sql to generate the result:
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT ',' + QUOTENAME(col+'_'+cast(rn as varchar(10)))
from
(
select row_number() over(partition by person, [case#]
order by DiagnosisDate) rn
from yourtable
) t
cross join
(
select 'Diagnosis' col union all
select 'DiagnosisDate'
) c
group by col, rn
order by rn, col
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT person,
[case#],
age,' + #cols + '
from
(
select person, [case#], age,
col+''_''+cast(rn as varchar(10)) col,
value
from
(
select person,
[case#],
age,
diagnosis,
convert(varchar(10), diagnosisdate, 101) diagnosisDate,
row_number() over(partition by person, [case#]
order by DiagnosisDate) rn
from yourtable
) d
cross apply
(
values (''diagnosis'', diagnosis), (''diagnosisDate'', diagnosisDate)
) c (col, value)
) t
pivot
(
max(value)
for col in (' + #cols + ')
) p '
execute(#query);
See SQL Fiddle with Demo. Both queries give the result:
| PERSON | CASE# | AGE | DIAGNOSIS_1 | DIAGNOSISDATE_1 | DIAGNOSIS_2 | DIAGNOSISDATE_2 | DIAGNOSIS_3 | DIAGNOSISDATE_3 | DIAGNOSIS_4 | DIAGNOSISDATE_4 |
------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| John | 13784 | 56 | Depression | 03/13/2012 | Brain Injury | 03/14/2012 | Spinal Cord Injury | 03/15/2012 | Hypertension | 03/16/2012 |
| Kate | 2643 | 37 | Bipolar | 03/11/2012 | Hypertension | 03/12/2012 | (null) | (null) | (null) | (null) |
| Kevin | 500934 | 25 | Down Syndrome | 03/18/2012 | Clinical Obesity | 03/19/2012 | (null) | (null) | (null) | (null) |
| Pete | 803342 | 34 | Schizophenia | 03/17/2012 | (null) | (null) | (null) | (null) | (null) | (null) |
For this type of pivoting, I think the aggregate/group method is feasible:
select d.case, d.person,
max(case when seqnum = 1 then diagnosis end) as d1,
max(case when seqnum = 1 then diagnosisdate end) as d1date,
max(case when seqnum = 2 then diagnosis end) as d2,
max(case when seqnum = 2 then diagnosisdate end) as d2date,
. . . -- and so on, for as many groups that you want
from (select d.*, row_number() over (partition by case order by diagnosisdate) as seqnum
from diagnoses d
) d
group by d.case, d.person
Since you are dealing with sensitive medical information, identifyiable information (name age etc) shouldn't be stored in the same table as the medical information. Also, if you extract out the person info into its own table and a Diagnosis table that has the personID foreign key you can establish the 1 to many relationship you want.
Unless you use Dynamic SQL, the PIVOT operator will not work here. I assume that patients can come in on any date. The PIVOT operator works with a finite and predefined number of columns. Your options are to use Dynamic SQL to create the PIVOT table, or to use Excel or a reporting tool like SSRS to do a Pivot report.
I think the Dynamic SQL option would not be practical here, since, you could end up having hundreds of columns for each of the patient visit dates.
If you want to explore the Dynamic SQL option anyway, have a look here:
https://www.simple-talk.com/blogs/2007/09/14/pivots-with-dynamic-columns-in-sql-server-2005/