SQL Pivot - Dynamic Columns, No Aggregation - sql

I'm trying to do a Pivot, but I'm not very experienced with pivots and I'm stuck - I can't figure out how to structure the query.
What I have:
Data Types (types of measurements that are recorded)
Locations
Data Sources (things at each location that will be measured)
Data Readings (measurements of the sources)
Additional information:
The number of Sources at any one Location can change
There will never be more than 5 sources at a single Location
Only 1 Reading is saved per Source/Type/date
In the returned table:
Table shows Data_Type info and Readings for a single Location and date
Columns: Data_Name, Units, Is_Required (from Data_Type table), plus one column for each Source
Rows: one row for each Data_Type
Rows should be ordered by Type_Display_Order
Sources (extra columns) should be ordered by Source_Display_Order
Some Readings are optional, and some Sources aren't measured daily - these still need to be included in the table
Example:
Table: Data_Type
Data_Type_ID Data_Name Units Is_Required (BIT) Type_Display_Order
-----------------------------------------------------------------------
1 Height In. 1 2
2 Length In. 0 3
3 Weight Lbs. 1 1
Table: Location
Location_ID Location
-----------------------
1 West
2 East
Table: Data_Source
Data_Source_ID Location_ID Source_Name Source_Display_Order
----------------------------------------------------------------
1 1 WCS 2
2 2 ECS 1
3 1 WBN 1
Table: Data_Reading
Data_Reading_ID Data_Type_ID Data_Source_ID Reading Reading_Date
----------------------------------------------------------------------
1 1 1 5 6/3/2016
2 3 2 3 5/1/2016
3 1 1 7 5/1/2016
4 2 3 2 6/3/2016
5 3 1 4 6/3/2016
Desired results from query for Location = "West", Date = 6/3/2016:
Data_Type_ID Data_Name Units Is_Required WBN WCS
---------------------------------------------------------
3 Weight Lbs. 1 NULL 4
1 Height In. 1 NULL 5
2 Length In. 0 NULL NULL
This solution seems to be similar: Pivot Dynamic Columns, no Aggregation but I'm still having some problems.
This is what I have so far:
DECLARE #date DATE, #locationID INT
SET #date = CAST('6/3/2016' AS DATE)
SET #locationID = 1
DECLARE #cols AS NVARCHAR(MAX), #query AS NVARCHAR(MAX)
SET #cols = STUFF((SELECT distinct ',' + QUOTENAME(s.Source_Name)
FROM Data_Source s
WHERE s.Location_ID = #locationID
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
SET #query = 'SELECT Data_Type_ID, Data_Name, Units, Is_Required, ' + #cols +
' FROM
(
SELECT
t.Data_Type_ID
, t.Data_Name
, t.Units
, t.Is_Required
, r.Reading
, s.Source_Name
FROM
Data_Type t
LEFT JOIN
Data_Reading r ON t.Data_Type_ID = r.Data_Type_ID
LEFT JOIN
Data_Source s ON r.Data_Source_ID = s.Data_Source_ID
WHERE
r.Reading_Date = CAST(CAST(' + #date + ' AS NVARCHAR(10)) AS DATE)
AND s.Location_ID = CAST(' + #locationID + ' AS INT)
) x
PIVOT
(
MIN(Reading)
for Source_Name in (' + #cols + ')
) p '
I have the query working properly now, but I still have a few problems:
#cols is not sorted by Source_Display_Order
rows are not sorted by Type_Display_Order (I did have ORDER BY in the inner SELECT statement for part X, but I was getting errors saying I can't have an ORDER BY clause there)
Date comparison in WHERE statement doesn't work - for some reason, it always computes as False, even when the dates are the same

Solved!
DECLARE #date DATE, #locationID INT
SET #date = CAST('6/3/2016' AS DATE)
SET #locationID = 1
DECLARE #cols AS NVARCHAR(MAX), #query AS NVARCHAR(MAX)
SET #cols = STUFF((SELECT ',' + QUOTENAME(s.Source_Name)
FROM Data_Source s
WHERE s.Location_ID = #locationID
ORDER BY s.Source_Display_Order
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
SET #query =
'SELECT
Data_Type_ID
, Data_Name
, Units
, Is_Required
, ' + #cols + '
FROM
(
SELECT
t.Data_Type_ID
, t.Data_Name
, t.Units
, t.Is_Required
, r.Reading
, s.Source_Name
, t.Type_Display_Order
FROM
Data_Type t
LEFT JOIN
Data_Reading r ON t.Data_Type_ID = r.Data_Type_ID
LEFT JOIN
Data_Source s ON r.Data_Source_ID = s.Data_Source_ID
WHERE
r.Reading_Date = ''' + CAST(#date AS NVARCHAR(10)) + '''
AND s.Location_ID = ' + CAST(#locationID AS NVARCHAR(20)) + '
) x
PIVOT
(
MIN(Reading)
for Source_Name in (' + #cols + ')
) p
ORDER BY
Type_Display_Order'
EXECUTE(#query)
To fix my problems:
Convert #date to NVARCHAR before adding to #query string and include extra quotes to surround the new NVARCHAR in quotes within #query
Remove DISTINCT clause from #cols and add ORDER BY (all of the names in my table are unique, so the DISTINCT is unnecessary)
Add Type_Display_Order to the inner SELECT statement, and add ORDER BY after the PIVOT statement

Related

Get dates for last 30 days dynamically in SQL

I have below SQL which gives me count of files received in particular country according to date. But here dates are hard coded. I want them dynamically. I want it in such a way that whenever I run this query, I get result for last 30 days. Below is the SQL:
with t (Country ,Date,total)
as
(
select b.country as Market, CAST(a.ProcessDate AS Date) AS DATE, count(a.ProcessDate) AS total from Log a LEFT JOIN File b ON a.FileID = b.FileID where a.ProcessDate BETWEEN '2022-11-01' AND '2022-11-07' GROUP BY b.country, CAST(a.ProcessDate AS DATE)
)
Select
*
from
(
Select
Date,
Total,
Country
from t
) x
Pivot(
sum(total)
for Date in (
[2022-11-01],
[2022-11-02],
[2022-11-03],
[2022-11-04]
)
) as pivottable
Below is Result of the query with dummy data:
Country
2022-11-01
2022-11-02
2022-11-03
2022-11-04
Brazil
2
1
Chile
1
1
Switzerland
1
Below is the structure of MasterFile and FileProcessLog with dummy data:
MasterFile:
FileID
Country
1
Brazil
2
Brazil
3
Chile
4
Chile
5
Switzerland
FileProcessLog:
FileID
ProcessDate
1
2022-11-01T15:31:53.0000000
2
2022-11-01T15:32:28.0000000
3
2022-11-02T15:33:34.0000000
4
2022-11-03T15:33:34.0000000
5
2022-11-04T15:37:10.0000000
Create function as below to return last 30 day dates:
CREATE FUNCTION [dbo].[RETURNDATE]()
RETURNS
#ParsedList table
(
DATEINFO DATE
)
AS
BEGIN
DECLARE #Counter Int
SET #Counter=1
WHILE ( #Counter <= 30)
BEGIN
--PRINT 'The counter value is = ' + CONVERT(VARCHAR,Convert(Date,DATEADD(DAY, -(#Counter), getdate())))
INSERT INTO #ParsedList (DATEINFO)
VALUES (CONVERT(VARCHAR,Convert(Date,DATEADD(DAY, -(#Counter), getdate()))))
SET #Counter = #Counter + 1
END
RETURN
END
now use inside your code as below:
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT distinct ',' + QUOTENAME([DateInfo])
from [DBO].RETURNDATE()
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'WITH t (Country ,Date,total) AS (
SELECT b.country as Market,
CAST(a.ProcessDate AS Date) AS DATE,
COUNT(a.ProcessDate) AS total
FROM [dbo].[FileProcessLog] a
LEFT JOIN [dbo].[MasterFile] b ON a.FileID = b.FileID where a.ProcessDate BETWEEN ''2022-11-01'' AND ''2022-11-07''
GROUP BY b.country, CAST(a.ProcessDate AS DATE)
)
SELECT * FROM (SELECT Date,Total,Country from t) x
PIVOT(SUM(total)
FOR Date IN ('
+ #cols +
')
) as PIVOTTABLE
'
execute(#query)
I think your full answer is ready now. Happy Coding.

Converting column values (rows) to columns and aggregate count on two rows of two different tables

I have two tables
Program
Student
Student:
Name Status1 Syear SCode
--------------------
kk A 2000 1
ra A 2001 2
Paras L 2000 2
Prit L 2001 2
Poot A 2002 4
Program:
PName PCode
--------------------
Msc 1
DC 2
PO 4
Join on ID
Required output :
SELECT *
FROM
(SELECT
Program.PName AS v, Status1
FROM
Student, Program
WHERE
Student.PCode = SCode
GROUP BY
Program.PName, Student.Syear, Status1) AS src
pivot
(
count(v)
FOR Status1 IN ([A],)
) as piv
It does not display PNAME in output
A L
-----------
1 0
1 2
1 0
Desired output
PNAME A L
-----------
Msc 1 0
DC 1 2
PO 1 0
1. STATIC PIVOT
You can do this if the column names is known in advance
SELECT PName,ISNULL([A],0) [A],ISNULL([L],0)[L] FROM
(
-- Source data for pivoting
SELECT P.PName,Status1,
COUNT(Status1)OVER(PARTITION BY PNAME,Status1)CNT
FROM #PROGRAM P
JOIN #Student S ON P.PCODE=S.SCODE
) x
PIVOT
(
--Defines the values in each dynamic columns
MIN(CNT)
-- Get the names of columns to pivot
FOR Status1 IN ([A],[L])
) p
ORDER BY PName
Click here to view result
2. DYNAMIC PIVOT
Dynamic pivoting can be done if the number of columns is not known in advance.
First of all get columns dynamically to pivot
DECLARE #cols NVARCHAR (MAX)
SELECT #cols = COALESCE (#cols + ',[' + Status1 + ']', '[' + Status1 + ']')
FROM (SELECT DISTINCT Status1 FROM #Student) PV
ORDER BY Status1
Now the below variable is used to replace NULL with zero.
DECLARE #NulltoZeroCols NVARCHAR (MAX)
SELECT #NullToZeroCols = SUBSTRING((SELECT ',ISNULL(['+Status1+'],0) AS ['+Status1+']'
FROM (SELECT DISTINCT Status1 FROM #Student)TAB
ORDER BY Status1 FOR XML PATH('')),2,8000)
Now pivot it. I have written the logic inside
DECLARE #query NVARCHAR(MAX)
SET #query = '-- This outer query forms your pivoted result
SELECT PName,'+#NullToZeroCols+' FROM
(
-- Source data for pivoting
SELECT P.PName,Status1,
COUNT(Status1)OVER(PARTITION BY PNAME,Status1)CNT
FROM #PROGRAM P
JOIN #Student S ON P.PCODE=S.SCODE
) x
PIVOT
(
--Defines the values in each dynamic columns
MIN(CNT)
-- Get the names from the #cols variable to show as column
FOR Status1 IN (' + #cols + ')
) p
ORDER BY PName;'
EXEC SP_EXECUTESQL #query
Click here to view result

Not able to combine multiple rows into single row based on certain conditions

In the image above, i have shown table structure i use to store result of student. However I need to select data in such a manner such that depending on particular FEID(examination ID),
I get marks obtained and subID of single student in single row. Something like below:
FEID SubID1 MarksObtained SubID2 MarksObtained SubID3 MarksObtained StdID
2 1 0 2 0 3 0 50
2 1 45 2 45 3 45 51
Result Column wont affect outcome as for a particular stdID and FEID it remains same for no matter how many SubID are there.
Basically I am storing each subject marks in single row and subjects are can be any number( more than 3 as in this case) , which is not known before hand. But for each I create one row to enter its marks
I tried sytax below .
DECLARE #cols nvarchar(MAX);
--get the list of subids from the table
SELECT #cols = SubjectName from tbSubjects where SubID IN(select distinct SubID from tbFinalMarks);
Declare #sql nvarchar(MAX) = 'SELECT StdId, FEID, ' + #cols + 'FROM
(
SELECT * FROM tbFinalMarks
)t
PIVOT
(
MAX(MarksObtained) FOR SubId IN (' + #cols + ')
)p';
Something like this will do it. It will also dynamically add new columns for new sub ids without you needing to worry about it.
DECLARE #cols nvarchar(MAX);
--get the list of subids from the table
SELECT #cols = COALESCE(#cols + ',', '') + '[' + CAST(SubId AS nvarchar) + ']' FROM (SELECT DISTINCT SubId FROM table);
Declare #sql nvarchar(MAX) = 'SELECT StdId, FEID, ' + #cols + 'FROM
(
SELECT * FROM table
)t
PIVOT
(
MAX(MarksObtained) FOR SubId IN (' + #cols + ')
)p';
EXECUTE sp_executesql #sql;
Although you can use pivot, I think the explicit aggregation approach is easier to construct:
select feid,
1 as SubId_1,
max(case when SubId = 1 then MarksObtained end) as MarksObtained_1,
2 as SubId_2,
max(case when SubId = 2 then MarksObtained end) as MarksObtained_2,
3 as SubId_3,
max(case when SubId = 3 then MarksObtained end) as MarksObtained_3,
stdid
from table t
group by feid, stdid;

change the column name while selecting from dyanamic table

Hi I have attendence query which will generate the attendence report with using PIVOT function
Here's the procedure :
declare #in_date DATETIME
/*Select all the stagign entries related to promotion id and investment type id */
/* also only those staging daat related interface status tracking*/
-- Getting all distinct dates into a temporary table #Dates
SELECT a.date as full_date_of_attendence INTO #Dates
FROM dbo.getFullmonth(#in_date) a
ORDER BY a.date
-- The number of days will be dynamic. So building
-- a comma seperated value string from the dates in #Dates
SELECT #cols = COALESCE(#cols + ',[' + CONVERT(varchar, full_date_of_attendence, 106)
+ ']','[' + CONVERT(varchar, full_date_of_attendence, 106) + ']')
FROM #Dates
ORDER BY full_date_of_attendence
--select #cols
---- Building the query with dynamic dates
SET #qry =
'SELECT * FROM
(SELECT admission_id, attendence_status , date_of_attendence
FROM dbo.tblattendence)emp
PIVOT (MAX(attendence_status) FOR date_of_attendence IN (' + #cols + ')) AS stat'
-- Executing the query
EXEC(#qry)
-- Dropping temporary tables
DROP TABLE #Dates
here is the output of the above query::
admission_id 01 May 2013 02 May 2013 03 May 2013
2 NULL 1 0
3 NULL 1 1
4 NULL 0 0
5 NULL 0 1
Here I want to change the names of the columns as 01,02,03......
and I want the values 1 as 'P' and 0 as 'A'
can anyone would help me to achieve this ??
I would suggest the following changes to your code. If you want a list of the days (1, 2, 3, etc), then you can use the DAY function.
Typically when I get a list of columns dynamically, my preference is using STUFF and FOR XML PATH, I would alter that code to the following:
select #colsPiv = STUFF((SELECT ',' + QUOTENAME(cast(day(full_date_of_attendence) as varchar(2)))
from #Dates
GROUP BY full_date_of_attendence
ORDER BY full_date_of_attendence
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
Then if you want to replace the 0 with an A and a 1 with a P, you will want to create a query to get a list of columns to replace the values:
select #colsSel = STUFF((SELECT ', case when ' + QUOTENAME(cast(day(full_date_of_attendence) as varchar(2)))+'= 1 then ''P'' else ''A'' end as '+QUOTENAME(cast(day(full_date_of_attendence) as varchar(2)))
from #Dates
GROUP BY full_date_of_attendence
ORDER BY full_date_of_attendence
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
Basically, this is creating a select list similar to this:
select case when [1] = 1 then 'P' else 'A' end as [1], ...
Then your final query will be:
SET #qry =
'SELECT admission_id, '+#colsSel +'
FROM
(
SELECT admission_id,
attendence_status ,
day(date_of_attendence) date_of_attendence
FROM dbo.tblattendence
)emp
PIVOT
(
MAX(attendence_status)
FOR date_of_attendence IN (' + #colsPiv + ')
) AS stat'
See SQL Fiddle with Demo
Let's change just the two things you wanted to, i.e.
CONVERT(CHAR(2), full_date_of_attendence, 106) -- use CHAR(2) instead of varchar
CASE attendence_status when 1 then 'P' else 'A' END in the SELECT...
The code with minimal changes. Hope this helps you see how you can make similar changes in future to other code.
declare #in_date DATETIME
/*Select all the stagign entries related to promotion id and investment type id */
/* also only those staging daat related interface status tracking*/
-- Getting all distinct dates into a temporary table #Dates
SELECT a.date as full_date_of_attendence INTO #Dates
FROM dbo.getFullmonth(#in_date) a
ORDER BY a.date
-- The number of days will be dynamic. So building
-- a comma seperated value string from the dates in #Dates
SELECT #cols = COALESCE(#cols + ',', '') + [' +
CONVERT(CHAR(2), full_date_of_attendence, 106) + ']'
FROM #Dates
ORDER BY full_date_of_attendence
--select #cols
---- Building the query with dynamic dates
SET #qry =
'SELECT * FROM
(SELECT admission_id, CASE attendence_status when 1 then 'P' else 'A' END, date_of_attendence
FROM dbo.tblattendence)emp
PIVOT (MAX(attendence_status) FOR date_of_attendence IN (' + #cols + ')) AS stat'
-- Executing the query
EXEC(#qry)
-- Dropping temporary tables
DROP TABLE #Dates

How can I create a month name as a column name for a given date range in sql?

I have a data as below:
Table
country date value
------------------------------------------------------
test1 5/1/2008 500
test1 5/7/2008 200
test1 5/8/2008 300
test1 7/1/2008 100
test1 7/2/2008 100
test2 6/1/2008 100
And I want a result as below:
Result
-----------
countryName May-08 Jun-08 July-08
test1 1000 - 200
test2 - 100
This is adapted from T-SQL Pivot? Possibility of creating table columns from row values
You can see it working here: http://sqlfiddle.com/#!3/7b8c0/28
I think you might need to fiddle around with the column ordering
-- Static PIVOT
SELECT *
FROM (SELECT country,
CONVERT(char(3), date, 0) + '-' +
RIGHT(CONVERT(varchar, YEAR(date)), 2) AS date,
value
FROM country) AS D
PIVOT(SUM(value) FOR date IN([May-08],[Jun-08],[Jul-08])) AS P;
GO
-- Dynamic PIVOT
DECLARE #T AS TABLE(y INT NOT NULL PRIMARY KEY);
DECLARE
#cols AS NVARCHAR(MAX),
#y AS INT,
#sql AS NVARCHAR(MAX)
SELECT #cols = STUFF(
(SELECT N',' + QUOTENAME(y) AS [text()]
FROM (SELECT DISTINCT CONVERT(char(3), date, 0) + '-' +
RIGHT(CONVERT(varchar, YEAR(date)), 2) AS y
FROM Country
) AS Y
ORDER BY y desc
FOR XML PATH('')),
1, 1, N'')
-- Construct the full T-SQL statement
-- and execute dynamically
SET #sql = N'SELECT *
FROM (SELECT country, CONVERT(char(3), date, 0) + ''-'' +
RIGHT(CONVERT(varchar, YEAR(date)), 2) AS date, value
FROM Country) AS D
PIVOT(SUM(value) FOR date IN(' + #cols + N')) AS P;'
EXEC sp_executesql #sql
You have to use a rather complex query for that, using a LOOP it think.
For creating dynamic column names look at this post: https://stackoverflow.com/a/10926106/1321564
With sql server you have some advantages: https://stackoverflow.com/a/5638042/1321564