TSQL, change value on a comma delimited column - sql-server

I have a column called empl_type_multi which is just a comma delimited column, each value is a link to another table called custom captions.
For instance, i might have the following as a value in empl_type_multi:
123, RHN, 458
Then in the custom_captions table these would be individual values:
123 = Dog
RHN = Cat
458 = Rabbit
All of these fields are NTEXT.
What i am trying to do is convert the empl_type_multi column and chance it to the respective names in the custom_captions table, so in the example above:
123, RHN, 458
Would become
Dog, Cat, Rabbit
Any help on this would be much appreciated.
----- EDIT ------------------------------------------------------------------
Ok so ive managed to convert the values to the corresponding caption and put it all into a temporary table, the following is the output from a CTE query on the table:
ID1 ID2 fName lName Caption_name Row_Number
10007 22841 fname1 lname1 DENTAL ASSISTANT 1
10007 22841 fname1 lname1 2
10007 22841 fname1 lname1 3
10008 23079 fname2 lname2 OPS WARD 1
10008 23079 fname2 lname2 DENTAL 2
10008 23079 fname2 lname2 3
How can i update this so that anything under caption name is added to the caption name of Row_Number 1 separated by a comma?
If i can do that all i need to do is delete all records where Row_Number != 1.
------ EDIT --------------------------------------------------
The solution to the first edit was:
WITH CTE AS
(
SELECT
p.ID1
, p.ID2
, p.fname
, p.lname
, p.caption_name--
, ROW_NUMBER() OVER (PARTITION BY p.id1ORDER BY caption_name DESC) AS RN
FROM tmp_cs p
)
UPDATE tblPerson SET empType = empType + ', ' + c.Data
FROM CTE c WHERE [DB1].dbo.tblPerson.personID = c.personID AND RN = 2
And then i just incremented RN = 2 until i got 0 rows affected.
This was after i ran:
DELETE FROM CTE WHERE RN != 1 AND Caption_name = ''

select ID1, ID2, fname, lname, left(captions, len(captions) - 1) as captions
from (
select distinct ID1, ID2, cast(fname as nvarchar) as fname, cast(lname as nvarchar) as lname, (
select cast(t1.caption_name as nvarchar) + ','
from #temp as t1
where t1.ID1 = t2.ID1
and t1.ID2 = t2.ID2
and cast(caption_name as nvarchar) != ''
order by t1.[row_number]
for xml path ('')) captions
from #temp as t2
) yay_concatenated_rows
This will give you what you want. You'll see casting from ntext to varchar. This is necessary for comparison because many logical ops can't be performed on ntext. It can be implicitly cast back the other way so no worries there. Note that when casting I did not specify length; this will default to 30, so adjust as varchar(length) as needed to avoid truncation. I also assumed that both ID1 and ID2 form a composite key (it appears this is so). Adjust the join as you need for the relationship.

you have just shared your part of problem,not exact problem.
try this,
DECLARE #T TABLE(ID1 VARCHAR(50),ID2 VARCHAR(50),fName VARCHAR(50),LName VARCHAR(50),Caption_name VARCHAR(50),Row_Number INT)
INSERT INTO #T VALUES
(10007,22841,'fname1','lname1','DENTAL ASSISTANT', 1)
,(10007,22841,'fname1','lname1', NULL, 2)
,(10007,22841,'fname1','lname1', NULL, 3)
,(10008,23079,'fname2','lname2','OPS WARD', 1)
,(10008,23079,'fname2','lname2','DENTAL', 2)
,(10008,23079,'fname2','lname2', NULL, 3)
SELECT *
,STUFF((SELECT ','+Caption_name
FROM #T T1 WHERE T.ID1=T1.ID1 FOR XML PATH('')
),1,1,'')
FROM #T T

You can construct the caption_name string easily by looping through while loop
declare #i int = 2,#Caption_name varchar(100)= (select series from
#temp where Row_Number= 1)
while #i <= (select count(*) from #temp)
begin
select #Caption_name = #Caption_name + Caption_name from #temp where Row_Number = #i)
set #i = #i+1
end
update #temp set Caption_name = #Caption_name where Row_Number = 1
and use case statement to remove null values
(select case when isnull(Caption_name ,'') = '' then
'' else ',' + Caption_name end

Related

Split a string in SQL by hyphen in 2012 version

I have multiple string in a column where I have get last string after column
Below are three example like same I have different number hyphen that can occur in a string but desired result is I have string before last hyphen
1. abc-def-Opto
2. abc-def-ijk-5C-hello-Opto
3. abc-def-ijk-4C-hi-Build
4. abc-def-ijk-4C-123-suppymanagement
Desired result set is
def
hello
hi
123
How to do this in SQL query to get this result set. I have MSSQL 2012 version
Require a generic sql which can get the result set
There are many ways to split/parse a string. ParseName() would fail because you may have more than 4 positions.
One option (just for fun), is to use a little XML.
We reverse the string
Convert into XML
Grab the second node
Reverse the desired value for the final presentation
Example
Declare #YourTable Table ([SomeCol] varchar(50))
Insert Into #YourTable Values
('abc-def-Opto')
,('abc-def-ijk-5C-hello-Opto')
,('abc-def-ijk-4C-hi-Build')
,('abc-def-ijk-4C-123-suppymanagement')
Select *
,Value = reverse(convert(xml,'<x>'+replace(reverse(SomeCol),'-','</x><x>')+'</x>').value('x[2]','varchar(150)'))
from #YourTable
Returns
SomeCol Value
abc-def-Opto def
abc-def-ijk-5C-hello-Opto hello
abc-def-ijk-4C-hi-Build hi
abc-def-ijk-4C-123-suppymanagement 123
Without getting into XML stuff, simply using string functions of sql server.
Declare #YourTable Table ([SomeCol] varchar(50))
Insert Into #YourTable Values
('abc-def-Opto')
,('abc-def-ijk-5C-hello-Opto')
,('abc-def-ijk-4C-hi-Build')
,('abc-def-ijk-4C-123-suppymanagement');
SELECT *
,RTRIM(LTRIM(REVERSE(
SUBSTRING(
SUBSTRING(REVERSE([SomeCol]) , CHARINDEX('-', REVERSE([SomeCol])) +1 , LEN([SomeCol]) )
, 1 , CHARINDEX('-', SUBSTRING(REVERSE([SomeCol]) , CHARINDEX('-', REVERSE([SomeCol])) +1 , LEN([SomeCol]) ) ) -1
)
)))
FROM #YourTable
i am not sure this script will exactly useful to your requirement but i am just trying to give an idea how to split the data
IF OBJECT_ID('tempdb..#Temp')IS NOT NULL
DROP TABLE #Temp
;WITH CTE(Id,data)
AS
(
SELECT 1,'abc-def-Opto' UNION ALL
SELECT 2,'abc-def-ijk-5C-hello-Opto' UNION ALL
SELECT 3,'abc-def-ijk-4C-hi-Build' UNION ALL
SELECT 4,'abc-def-ijk-4C-123-suppymanagement'
)
,Cte2
AS
(
SELECT Id, CASE WHEN Id=1 AND Setdata=1 THEN data
WHEN Id=2 AND Setdata=2 THEN data
WHEN Id=3 AND Setdata=3 THEN data
WHEN Id=4 AND Setdata=4 THEN data
ELSE NULL
END AS Data
FROM
(
SELECT Id,
Split.a.value('.','nvarchar(1000)') AS Data,
ROW_NUMBER()OVER(PARTITION BY id ORDER BY id) AS Setdata
FROM(
SELECT Id,
CAST('<S>'+REPLACE(data ,'-','</S><S>')+'</S>' AS XML) AS data
FROM CTE
) AS A
CROSS APPLY data.nodes('S') AS Split(a)
)dt
)
SELECT * INTO #Temp FROM Cte2
SELECT STUFF((SELECT DISTINCT ', '+ 'Set_'+CAST(Id AS VARCHAR(10))+':'+Data
FROM #Temp WHERE ISNULL(Data,'')<>'' FOR XML PATH ('')),1,1,'')
Result
Set_1:abc, Set_2:def, Set_3:ijk, Set_4:4C
You can do like
WITH CTE AS
(
SELECT 1 ID,'abc-def-Opto' Str
UNION
SELECT 2, 'abc-def-ijk-5C-hello-Opto'
UNION
SELECT 3, 'abc-def-ijk-4C-hi-Build'
UNION
SELECT 4, 'abc-def-ijk-4C-123-suppymanagement'
)
SELECT ID,
REVERSE(LEFT(REPLACE(P2, P1, ''), CHARINDEX('-', REPLACE(P2, P1, ''))-1)) Result
FROM (
SELECT LEFT(REVERSE(Str), CHARINDEX('-', REVERSE(Str))) P1,
REVERSE(Str) P2,
ID
FROM CTE
) T;
Returns:
+----+--------+
| ID | Result |
+----+--------+
| 1 | def |
| 2 | hello |
| 3 | hi |
| 4 | 123 |
+----+--------+
Demo

Compare the two tables and update the value in a Flag column

I have two tables and the values like this
`create table InputLocationTable(SKUID int,InputLocations varchar(100),Flag varchar(100))
create table Location(SKUID int,Locations varchar(100))
insert into InputLocationTable(SKUID,InputLocations) values(11,'Loc1, Loc2, Loc3, Loc4, Loc5, Loc6')
insert into InputLocationTable(SKUID,InputLocations) values(12,'Loc1, Loc2')
insert into InputLocationTable(SKUID,InputLocations) values(13,'Loc4,Loc5')
insert into Location(SKUID,Locations) values(11,'Loc3')
insert into Location(SKUID,Locations) values(11,'Loc4')
insert into Location(SKUID,Locations) values(11,'Loc5')
insert into Location(SKUID,Locations) values(11,'Loc7')
insert into Location(SKUID,Locations) values(12,'Loc10')
insert into Location(SKUID,Locations) values(12,'Loc1')
insert into Location(SKUID,Locations) values(12,'Loc5')
insert into Location(SKUID,Locations) values(13,'Loc4')
insert into Location(SKUID,Locations) values(13,'Loc2')
insert into Location(SKUID,Locations) values(13,'Loc2')`
I need to get the output by matching SKUID's from Each tables and Update the value in Flag column as shown in the screenshot, I have tried something like this code
`SELECT STUFF((select ','+ Data.C1
FROM
(select
n.r.value('.', 'varchar(50)') AS C1
from InputLocation as T
cross apply (select cast('<r>'+replace(replace(Location,'&','&'), ',', '</r><r>')+'</r>' as xml)) as S(XMLCol)
cross apply S.XMLCol.nodes('r') as n(r)) DATA
WHERE data.C1 NOT IN (SELECT Location
FROM Location) for xml path('')),1,1,'') As Output`
But not convinced with output and also i am trying to avoid xml path code, because performance is not first place for this code, I need the output like the below screenshot. Any help would be greatly appreciated.
I think you need to first look at why you think the XML approach is not performing well enough for your needs, as it has actually been shown to perform very well for larger input strings.
If you only need to handle input strings of up to either 4000 or 8000 characters (non max nvarchar and varchar types respectively), you can utilise a tally table contained within an inline table valued function which will also perform very well. The version I use can be found at the end of this post.
Utilising this function we can split out the values in your InputLocations column, though we still need to use for xml to concatenate them back together for your desired format:
-- Define data
declare #InputLocationTable table (SKUID int,InputLocations varchar(100),Flag varchar(100));
declare #Location table (SKUID int,Locations varchar(100));
insert into #InputLocationTable(SKUID,InputLocations) values (11,'Loc1, Loc2, Loc3, Loc4, Loc5, Loc6'),(12,'Loc1, Loc2'),(13,'Loc4,Loc5'),(14,'Loc1');
insert into #Location(SKUID,Locations) values (11,'Loc3'),(11,'Loc4'),(11,'Loc5'),(11,'Loc7'),(12,'Loc10'),(12,'Loc1'),(12,'Loc5'),(13,'Loc4'),(13,'Loc2'),(13,'Loc2'),(14,'Loc1');
--Query
-- Derived table splits out the values held within the InputLocations column
with i as
(
select i.SKUID
,i.InputLocations
,s.item as Loc
from #InputLocationTable as i
cross apply dbo.fn_StringSplit4k(replace(i.InputLocations,' ',''),',',null) as s
)
select il.SKUID
,il.InputLocations
,isnull('Add ' -- The split Locations are then matched to those already in #Location and those not present are concatenated together.
+ stuff((select ', ' + i.Loc
from i
left join #Location as l
on i.SKUID = l.SKUID
and i.Loc = l.Locations
where il.SKUID = i.SKUID
and l.SKUID is null
for xml path('')
)
,1,2,''
)
,'No Flag') as Flag
from #InputLocationTable as il
order by il.SKUID;
Output:
+-------+------------------------------------+----------------------+
| SKUID | InputLocations | Flag |
+-------+------------------------------------+----------------------+
| 11 | Loc1, Loc2, Loc3, Loc4, Loc5, Loc6 | Add Loc1, Loc2, Loc6 |
| 12 | Loc1, Loc2 | Add Loc2 |
| 13 | Loc4,Loc5 | Add Loc5 |
| 14 | Loc1 | No Flag |
+-------+------------------------------------+----------------------+
For nvarchar input (I have different functions for varchar and max type input) this is my version of the string splitting function linked above:
create function [dbo].[fn_StringSplit4k]
(
#str nvarchar(4000) = ' ' -- String to split.
,#delimiter as nvarchar(1) = ',' -- Delimiting value to split on.
,#num as int = null -- Which value in the list to return. NULL returns all.
)
returns table
as
return
-- Start tally table with 10 rows.
with n(n) as (select 1 union all select 1 union all select 1 union all select 1 union all select 1 union all select 1 union all select 1 union all select 1 union all select 1 union all select 1)
-- Select the same number of rows as characters in #str as incremental row numbers.
-- Cross joins increase exponentially to a max possible 10,000 rows to cover largest #str length.
,t(t) as (select top (select len(isnull(#str,'')) a) row_number() over (order by (select null)) from n n1,n n2,n n3,n n4)
-- Return the position of every value that follows the specified delimiter.
,s(s) as (select 1 union all select t+1 from t where substring(isnull(#str,''),t,1) = #delimiter)
-- Return the start and length of every value, to use in the SUBSTRING function.
-- ISNULL/NULLIF combo handles the last value where there is no delimiter at the end of the string.
,l(s,l) as (select s,isnull(nullif(charindex(#delimiter,isnull(#str,''),s),0)-s,4000) from s)
select rn
,item
from(select row_number() over(order by s) as rn
,substring(#str,s,l) as item
from l
) a
where rn = #num
or #num is null;
go

Comma separated string total count by variable by row

This is the schema:
User_ID Page_ID Timestamp
1 48,51,94 7/26/2017 8:30
2 42,11,84 7/26/2017 9:40
3 4,16,24 7/26/2017 16:20
4 7,2,94 7/27/2017 8:00
1 48,22,94 7/27/2017 13:50
2 42,11 7/27/2017 14:00
3 4,24 7/27/2017 18:15
The code below gives aggregate count of page ids ran per user (non-unique on purpose):
SELECT User_ID, sum(len(Page_ID) - len(replace(Page_ID, ',', '')) +1) as TotalPageCount
FROM DBTABLE
group by User_ID
Output:
User_ID TotalPageCount
1 6
2 5
3 5
4 3
However, I am looking to add a (comma separated) column with page count per page id per user id. ie. a column as newsletter id 1: count, newsletter id 2: count, etc. (essentially a dictionary). Can be a different format, but needs to be descriptive at the page id level, with its respective count.
Something like this:
User_ID PageIDCount TotalPageCount
1 48:2, 51:1, 94:2, 22:1, 6
2 42:2, 11:2, 84:1, 5
3 4:2, 16:1, 24:2, 5
4 7:1, 2:1, 94:1, 3
Your help is greatly appreciated!
Edit:
As per SeanLange's amazing solution, you can change the definition to MyCTE to the below, in order to avoid using any functions:
select user_id, page_id, page_count = count(*)
FROM (
SELECT user_id, Split.a.value('.', 'NVARCHAR(max)') AS page_id FROM
( SELECT user_id, CAST ('<M>' + REPLACE(page_id, ',', '</M><M>') + '</M>' AS XML) page_id
FROM #temp
) AS A
CROSS APPLY page_id.nodes ('/M') AS Split(a)
) x
group by user_id, page_id
Wow this is a nightmare. You are going to need a string splitter to start with. My personal favorite is this one. http://www.sqlservercentral.com/articles/Tally+Table/72993/ There are a number of other excellent choices here. https://sqlperformance.com/2012/07/t-sql-queries/split-strings
Starting with your data you will need to do something like this.
declare #Something table
(
User_ID int
, Page_ID varchar(100)
, MyDate datetime
)
insert #Something
select 1, '48,51,94', '7/26/2017 8:30' union all
select 2, '42,11,84', '7/26/2017 9:40' union all
select 3, '4,16,24', '7/26/2017 16:20' union all
select 4, '7,2,94', '7/27/2017 8:00' union all
select 1, '48,22,94', '7/27/2017 13:50' union all
select 2, '42,11', '7/27/2017 14:00' union all
select 3, '4,24', '7/27/2017 18:15'
select User_ID
, Page_ID = x.Item
, count(*)
from #Something s
cross apply dbo.DelimitedSplit8K(s.Page_ID, ',') x
group by User_ID
, x.Item
order by User_ID
, x.Item
This gets the data with the counts you want. From there you are going to have to shove this back into the denormalized structure that you want. You can do this with FOR XML. Here is an article that explains how to do that part of this. Simulating group_concat MySQL function in Microsoft SQL Server 2005?
-----EDIT-----
OK here is the complete working solution. You have obviously been working hard at trying to get this sorted out. I am using the DelimitedSplit8K function here so I didn't have to inline XML like your solution was doing.
with MyCTE as
(
select User_ID
, Page_ID = x.Item
, PageCount = count(*)
from #Something s
cross apply dbo.DelimitedSplit8K(s.Page_ID, ',') x
group by User_ID
, x.Item
)
, GroupedPageViews as
(
select c.User_ID
, sum(c.PageCount) as TotalPageCount
, PageViews = STUFF((select ', ' + convert(varchar(4), c2.Page_ID) + ':' + convert(varchar(4), c2.PageCount)
from MyCTE c2
where c.User_ID = c2.User_ID
order by c2.Page_ID
for xml path('')), 1, 1, '')
from MyCTE c
group by c.User_ID
)
select gpv.User_ID
, gpv.PageViews
, gpv.TotalPageCount
from GroupedPageViews gpv
join MyCTE c on c.User_ID = gpv.User_ID
group by gpv.PageViews
, gpv.User_ID
, gpv.TotalPageCount
order by gpv.User_ID
This will return your data like this.
User_ID PageViews TotalPageCount
1 22:1, 48:2, 51:1, 94:2 6
2 11:2, 42:2, 84:1 5
3 16:1, 24:2, 4:2 5
4 2:1, 7:1, 94:1 3
Here you go
SELECT DISTINCT User_Id
, (
SELECT CAST(t.Value AS VARCHAR) + ':' + CAST(COUNT(t.value) AS VARCHAR) + ', '
FROM TBL_46160346_DBTABLE ii
CROSS APPLY (
SELECT *
FROM fn_ParseText2Table(Page_ID, ',')
) t
WHERE pp.User_Id = ii.User_Id
GROUP BY User_Id
, VALUE
ORDER BY User_Id
FOR XML PATH('')
) PageIDCount
, (
SELECT COUNT(*)
FROM TBL_46160346_DBTABLE ii
CROSS APPLY (
SELECT *
FROM fn_ParseText2Table(Page_ID, ',')
) t
WHERE pp.User_Id = ii.User_Id
GROUP BY User_Id
) TotalPageCount
FROM TBL_46160346_DBTABLE pp
fn_ParseText2Table function
ALTER FUNCTION [dbo].[fn_ParseText2Table] (
#p_SourceText VARCHAR(8000), #p_Delimeter VARCHAR(10) = ',' --default comma
)
RETURNS #retTable TABLE (Value BIGINT)
AS
BEGIN
DECLARE #w_Continue INT, #w_StartPos INT, #w_Length INT, #w_Delimeter_pos INT, #w_tmp_txt VARCHAR(48), #w_Delimeter_Len TINYINT
IF LEN(#p_SourceText) = 0
BEGIN
SET #w_Continue = 0 -- force early exit
END
ELSE
BEGIN
-- parse the original #p_SourceText array into a temp table
SET #w_Continue = 1
SET #w_StartPos = 1
SET #p_SourceText = RTRIM(LTRIM(#p_SourceText))
SET #w_Length = DATALENGTH(RTRIM(LTRIM(#p_SourceText)))
SET #w_Delimeter_Len = LEN(#p_Delimeter)
END
WHILE #w_Continue = 1
BEGIN
SET #w_Delimeter_pos = CHARINDEX(#p_Delimeter, SUBSTRING(#p_SourceText, #w_StartPos, #w_Length - #w_StartPos + #w_Delimeter_Len))
IF #w_Delimeter_pos > 0 -- delimeter(s) found, get the value
BEGIN
SET #w_tmp_txt = LTRIM(RTRIM(SUBSTRING(#p_SourceText, #w_StartPos, #w_Delimeter_pos - 1)))
SET #w_StartPos = #w_Delimeter_pos + #w_StartPos + #w_Delimeter_Len - 1
END
ELSE -- No more delimeters, get last value
BEGIN
SET #w_tmp_txt = LTRIM(RTRIM(SUBSTRING(#p_SourceText, #w_StartPos, #w_Length - #w_StartPos + #w_Delimeter_Len)))
SELECT #w_Continue = 0
END
INSERT INTO #retTable
VALUES (#w_tmp_txt)
END
RETURN
END

How to check for a specific condition by looping through every record in SQL Server?

I do have following table
ID Name
1 Jagan Mohan Reddy868
2 Jagan Mohan Reddy869
3 Jagan Mohan Reddy
Name column size is VARCHAR(55).
Now for some other task we need to take only 10 varchar length i.e. VARCHAR(10).
My requirement is to check that after taking the only 10 bits length of Name column value for eg if i take Name value of ID 1 i.e. Jagan Mohan Reddy868 by SUBSTRING(Name, 0,11) if it equals with another row value. here in this case the final value of SUBSTRING(Jagan Mohan Reddy868, 0,11) is equal to Name value of ID 3 row whose Name is 'Jagan Mohan Reddy'. I need to make a list of those kind rows. Can somebody help me out on how can i achieve in SQL Server.
My main check is that the truncated values of my Name column should not match with any non truncated values of Name column. If so i need to get those records.
Assuming I understand the question, I think you are looking for something like this:
Create and populate sample data (Please save us this step in your future questions)
DECLARE #T as TABLE
(
Id int identity(1,1),
Name varchar(15)
)
INSERT INTO #T VALUES
('Hi, I am Zohar.'),
('Hi, I am Peled.'),
('Hi, I am Z'),
('I''m Zohar peled')
Use a cte with a self inner join to get the list of ids that match the first 10 chars:
;WITH cte as
(
SELECT T2.Id As Id1, T1.Id As Id2
FROM #T T1
INNER JOIN #T T2 ON LEFT(T1.Name, 10) = t2.Name AND T1.Id <> T2.Id
)
Select the records from the original table, inner joined with a union of the Id1 and Id2 from the cte:
SELECT T.Id, Name
FROM #T T
INNER JOIN
(
SELECT Id1 As Id
FROM CTE
UNION
SELECT Id2
FROM CTE
) U ON T.Id = U.Id
Results:
Id Name
----------- ---------------
1 Hi, I am Zohar.
3 Hi, I am Z
Try this
SELECT Id,Name
FROM(
SELECT *,ROW_NUMBER() OVER(PARTITION BY Name, LEFT(Name,11) ORDER BY ID) RN
FROM Tbale1 T
) Tmp
WHERE Tmp.RN = 1
loop over your column for all the values and put your substring() function inside this loop and I think in Sql index of string starts from 1 instead of 0. If you pass your string to charindex() like this
CHARINDEX('Y', 'Your String')
thus you will come to know whether it is starting from 0 or 1
and you can save your substring value as value of other column with length 10
I hope it will help you..
I think this should cover all the cases you are looking for.
-- Create Table
DECLARE #T as TABLE
(
Id int identity(1,1),
Name varchar(55)
)
-- Create Data
INSERT INTO #T VALUES
('Jagan Mohan Reddy868'),
('Jagan Mohan Reddy869'),
('Jagan Mohan Reddy'),
('Mohan Reddy'),
('Mohan Reddy123551'),
('Mohan R')
-- Get Matching Items
select *, SUBSTRING(name, 0, 11) as ShorterName
from #T
where SUBSTRING(name, 0, 11) in
(
-- get all shortnames with a count > 1
select SUBSTRING(name, 0, 11) as ShortName
from #T
group by SUBSTRING(name, 0, 11)
having COUNT(*) > 1
)
order by Name, LEN(Name)

TSQL While Loop over months in year

I have an output that I need to achieve and I am not too certain how to go about it.
I first need to start by looping over each month in the year and using that month in a select statement to check for data.
For example:
Select * from table where MONTH(A.[submissionDate]) = 1
Select * from table where MONTH(A.[submissionDate]) = 2
Select * from table where MONTH(A.[submissionDate]) = 3
My end result is to create this XML output to use with a chart plugin. It needs to include the months even if there is no data which is why I wanted to loop through each month to check for it.
<root>
<dataSet>
<areaDesc>Area 1</areaDesc>
<data>
<month>January</month>
<monthValue>1</monthValue>
<submissions>0</submissions>
</data>
<data>
<month>February</month>
<monthValue>2</monthValue>
<submissions>7</submissions>
</data>
<data>
<month>March</month>
<monthValue>3</monthValue>
<submissions>5</submissions>
</data>
</dataSet>
<dataSet>
<areaDesc>Area 2</areaDesc>
<data>
<month>January</month>
<monthValue>1</monthValue>
<submissions>0</submissions>
</data>
<data>
<month>February</month>
<monthValue>2</monthValue>
<submissions>7</submissions>
</data>
<data>
<month>March</month>
<monthValue>3</monthValue>
<submissions>5</submissions>
</data>
</dataSet>
</root>
I may be way over thinking this but I'm hoping I talking it through may help me out a little.
Here is my current set up of how I get some other stats:
--Temp table
DECLARE #areas TABLE (
area VARCHAR (100));
IF #dept = 'global'
OR #dept = ''
BEGIN
INSERT INTO #areas (area)
SELECT DISTINCT(AreaDesc)
FROM dbo.EmpTable;
END
ELSE
BEGIN
INSERT INTO #areas
SELECT #dept;
END
IF (#action = 'compare')
BEGIN
SELECT DATENAME(month, A.[submissionDate]) AS [month],
MONTH(A.[submissionDate]) AS [monthValue],
count(A.[submissionID]) AS submissions,
B.[AreaDesc]
FROM empowermentSubmissions AS A
INNER JOIN empTable AS B
ON A.[nomineeQID] = B.[QID]
WHERE YEAR(A.[submissionDate]) = #year
AND A.[statusID] = 3
AND A.[locationID] IN (SELECT location
FROM #table)
GROUP BY DATENAME(month, A.[submissionDate]), MONTH(A.[submissionDate]), B.[AreaDesc]
ORDER BY [monthValue] ASC
FOR XML PATH ('dataSet'), TYPE, ELEMENTS, ROOT ('root');
END
ELSE
This is a great application for a "Dates" table or view. Create a new table in your database with schema like:
CREATE TABLE dbo.Dates (
Month INT,
MonthName VARCHAR(20)
)
Populate this table with the years and months you may want to aggregate over. Then, you can make your query like:
SELECT
Area
Dates.MonthName,
COUNT(*) AS Count
FROM
dbo.Dates
LEFT OUTER JOIN
dbo.Submissions
AND Dates.Month = MONTH(Submissions.SubmissionDate)
GROUP BY
Dates.MonthName,
Area
The LEFT OUTER JOIN will give you one row for every Year and Month in the dates table, and a count of any submissions on that month. You end up with output like:
Area | MonthName | Count
Area 1 | Jan | 0
Area 2 | Feb | 2
&c.
You'll want to do a FOR XML structure to get the exact result set you're looking for in one go, I think. I put this together with what I could glean about your XML. Just change the name of the table variable here to your real table name and this should work.
EDIT: changed up the query to match the definition from the posted query. Updated the data element where clause to maintain month instantiation when zero counts were found in a month.
EDIT: Added Status requirement.
EDIT: Moved areaDesc criteria for constant month output.
declare #empowermentSubmissions table (submissionID int primary key identity(1,1), submissionDate datetime, nomineeQID INT, statusID INT)
declare #empTable table (QID int primary key identity(1,1), AreaDesc varchar(10))
declare #n int = 1
while #n < 50
begin
insert into #empTable (AreaDesc) values ('Area ' + cast((#n % 2)+1 as varchar(1)))
set #n = #n + 1
end
set #n = 1
while #n < 500
begin
insert into #empowermentSubmissions (submissionDate, nomineeQID, StatusID) values (dateadd(dd,-(cast(rand()*600 as int)),getdate()), (select top 1 QID from #empTable order by newid()), 3 + (#n % 2) - (#n % 3) )
set #n = #n + 1
end
declare #year int = 2014
select (
select (
select (
select e1.areaDesc
from #empTable e1
where e1.areaDesc = e2.areaDesc
group by e1.areaDesc
for xml path(''),type
)
, (
select [month], [monthValue], count(s1.submissionID) as submissions
from (
select #year [Year]
, datename(month,dateadd(mm,RowID-1,#year-1900)) [Month]
, month(dateadd(mm,RowID-1,#year-1900)) [MonthValue]
from (
select *, row_number()over(order by name) as RowID
from master..spt_values
) d
where d.RowID <= 12
) t
left join (
select s3.submissionID, s3.submissionDate, e3.AreaDesc
from #empowermentSubmissions s3
inner join #empTable e3 on s3.nomineeQID = e3.QID
where s3.statusID = 3
and e3.areaDesc = e2.areaDesc
) s1 on year(s1.submissionDate) = t.[Year]
and month(s1.submissionDate) = t.[MonthValue]
group by [Month], [MonthValue]
order by [MonthValue]
for xml path('data'),type
)
for xml path(''),type
) dataset
from #empowermentSubmissions s2
inner join #empTable e2 on s2.nomineeQID = e2.QID
group by e2.areaDesc
for xml path(''), type
) root
for xml path (''), type
You should be able to use a tally table to get the months:
SELECT TOP 12 IDENTITY(INT,1,1) AS N
INTO #tally
FROM master.dbo.syscolumns sc1
SELECT DATENAME(MONTH,DATEADD(MONTH,t.N-1,'2014-01-01')) AS namemonth, t.N AS monthvalue, COUNT(tbl.submissionDate) AS submissions, tbl.Area
FROM #tally t
LEFT OUTER JOIN tbl ON MONTH(tbl.submissionDate) = t.N
GROUP BY t.n, tbl.Area
DROP TABLE #tally

Resources