SQL convert rows generated by CTE to columns - including fieldnames - sql-server

I have a CTE that generates data into a variable number of rows. These are hourly values that I need to use in a join to another table, but that part is not important here.
The CTE part alone looks like this:
DECLARE #interval INT = 3 -- hours
DECLARE #period INT = 24 -- hours
;WITH cteHour AS (
SELECT CAST('i0' AS VARCHAR(6)) AS fieldname, 0 AS i, 0 AS startpos
UNION ALL
SELECT CONCAT('i', CAST(i + 1 AS VARCHAR(5))) AS fieldname, i + 1 AS i, startpos + #interval AS startpos
FROM cteHour
WHERE (startpos + #interval) < #period
) SELECT fieldname, startpos FROM cteHour
And it generates data like this (depending on the #interval and #period values):
fieldname startpos
--------- -----------
i0 0
i1 3
i2 6
i3 9
i4 12
i5 15
i6 18
i7 21
My question is how can I get the results into columns instead of rows, using the first column as the field names, like this:
i0 i1 i2 i3 i4 i5 i6 i7
-- -- -- -- -- -- -- --
0 3 6 9 12 15 18 21
I'm guessing it will require a PIVOT, but as far as I know pivoting cannot generate field names dynamically. So I'm open to any answers that get the job done.

Since it required a dynamic sql, skip the pivot part and straight away generate the dynamic query from your cte
DECLARE #sql NVARCHAR(MAX)
SELECT #sql = isnull(#sql + ',', 'select ')
+ quotename(fieldname) + '=' + convert(varchar(10), startpos)
FROM cteHour
order by startpos
print #sql
exec sp_executesql #sql

The following query should do what you want, it uses a temp table but you'd get an idea on how the approach is
DECLARE #interval INT = 3 -- hours
DECLARE #period INT = 24 -- hours
;WITH cteHour AS (
SELECT CAST('i0' AS VARCHAR(6)) AS fieldname, 0 AS i, 0 AS startpos
UNION ALL
SELECT CONCAT('i', CAST(i + 1 AS VARCHAR(5))) AS fieldname, i + 1 AS i, startpos + #interval AS startpos
FROM cteHour
WHERE (startpos + #interval) < #period
)
SELECT * INTO #temp FROM cteHour;
DECLARE #pvt NVARCHAR(MAX) = '';
SET #pvt = STUFF(
(SELECT DISTINCT N', ' + QUOTENAME([fieldname]) FROM #temp FOR XML PATH('')),1,2,N'')
EXEC (N'SELECT *
FROM #temp
PIVOT (MAX([startpos]) FOR [fieldname] IN('+#pvt+')) AS PIV');

Here is the simplest answer using a dynamic query
DECLARE #interval INT = 3 -- hours
DECLARE #period INT = 24 -- hours
DECLARE #startpos INT = 0
DECLARE #i INT = 0
DECLARE #sql NVARCHAR(MAX) = 'SELECT '
WHILE #startpos < #period
BEGIN
SET #sql = #sql + (CASE WHEN #i > 0 THEN ',' ELSE '' END) +
CAST(#startpos AS VARCHAR(5)) + ' AS i' + CAST(#i AS VARCHAR(5))
SET #i = #i + 1
SET #startpos = #startpos + #interval
END
print #sql
exec sp_executesql #sql

Related

Using pivot and then counting the values in each pivot column

I have a table that looks like this:
Agent_id
break_id
time
1
1
15
1
2
12
1
2
12
I used pivot to get this structure:
Agent_id
1
2
1
15
24
The problem is that I need to get the count for the pivoted columns, in the example I need to have a structure like this:
Agent_id
1
2
count1
count2
1
15
24
1
2
And I'm not sure on how to do it ... this is the query so far.
DECLARE #COLUMNS VARCHAR(MAX)
DECLARE #QUERY nVARCHAR(MAX)
SELECT #COLUMNS = COALESCE(#COLUMNS + ', ','') + QUOTENAME([break_id])
FROM
(SELECT DISTINCT [break_id] FROM test) AS B
ORDER BY B.[break_id]
SET #QUERY = '
SELECT agent_id,
'+#COLUMNS+'
FROM (
SELECT TOP (1000)
agent_id,break_id,time_inbreak
FROM test
) as pivotData
PIVOT (
SUM(time_inbreak)
FOR break_id IN ('+#COLUMNS+')
) as pivotResult
'
EXEC sp_executesql #QUERY
Any help is greatly appreciated
Unfortunately, PIVOT can only pivot a single column. But we can do multiple columns using conditional aggregation SUM(CASE WHEN... and COUNT(CASE WHEN...:
DECLARE #COLUMNS VARCHAR(MAX)
DECLARE #QUERY nVARCHAR(MAX)
SELECT #COLUMNS = COALESCE(#COLUMNS + ', ','') +
'Sum' + QUOTENAME([break_id]) +
' = SUM(CASE WHEN break_id = ' + break_id + ' THEN time_inbreak END), Count' +
QUOTENAME([break_id]) + ' = COUNT(CASE WHEN break_id = ' + break_id + ' THEN 1 END)'
FROM
(SELECT DISTINCT [break_id] FROM test) AS B
ORDER BY B.[break_id]
OPTION (MAXDOP 1);
SET #QUERY = '
SELECT agent_id,
'+#COLUMNS+'
FROM (
SELECT TOP (1000)
agent_id,break_id,time_inbreak
FROM test
) as pivotData
GROUP BY agent_id;
';
PRINT #QUERY
EXEC sp_executesql #QUERY
I must say, I'm not sure how safe it is to aggregate the columns like that, especially in the face of parallelism. Preferably use STRING_AGG or FOR XML PATH(''), TYPE. At the very least I have added OPTION (MAXDOP 1) to prevent parallelism

Merging more than one table into one existing table

This is the table creation and insertion query
If not exists(select * from sysobjects where name='hrs')
Create table hrs(hr int)
declare #cnt int =1
while #cnt <= 12
begin
insert into hrs values(#cnt)
set #cnt=#cnt+1
end
The above code gives the output like
but I just want that
declare #cnt1 int = 1
while #cnt1<=12
begin
EXEC('select he'+#cnt1+' = case when hr = 1 then '+#cnt1+' end from hrs')
set #cnt1=#cnt1+1
end
The above code returns the 12 different table but i just want the all records in one table (without creating any new table).
So, how can i do this?
Please help me.
Thanks.
Here the all column are created dynamically through loop
Here are the full query
declare #s varchar(MAX)=''
declare #j int = 1
while #j<=12
begin
if #j = 12
Set #s = #s+'he'+convert(varchar,#j)+'=MAX(case when hr='+convert(varchar,#j)+' then '+convert(varchar,#j)+' end)'
else
set #s = #s+'he'+convert(varchar,#j)+'=MAX(case when hr='+convert(varchar,#j)+' then '+convert(varchar,#j)+' end),'
set #j=#j+1
end
set #s = 'select '+#s+' from hrs'
exec(#s)
Your query doesn't make a lot of sense, but you can build a list of columns and then exec that:
declare #columns nvarchar(max)
declare #cnt int = 1
while #cnt <= 12
begin
set #columns = isnull(#columns + ', ', '') + 'He' + cast(#cnt as nvarchar) +
' = sum(case when hr = ' + cast(#cnt as nvarchar) + ' then hr end)'
end
declare #sql nvarchar(max) = 'select ' + #columns ' + from hr'
exec (#sql)

dynamic sql not parsing correctly

I am writing a dynamic sql statement that will check if the index exist and drop it. Getting compile error Could somebody tell what the problem is . I double checked the ticks but cant understand
declare #startyear int = 2000
declare #startQuarter int = 1
declare #sql nvarchar(max)
declare #tableName varchar(50)
if #startYear is null
set #startYear = 2000;
set #startQuarter = 1;
while #startYear <= year(getdate())
begin
set #startQuarter = 1;
while #startQuarter < 5
begin
set #tableName = 'FinData' + cast(#startYear as varchar) + '_' + cast(#startQuarter as varchar);
set #sql = 'IF EXISTS(SELECT * FROM sys.indexes WHERE object_id = object_id(' + #tableName + ') AND NAME = ' + '.idx_' + #tableName
drop index' + #tableName + '.idx_' + #tableName
print #sql
set #startQuarter += 1
end
set #startYear += 1;
end
Two issues: this isn't adding anything before the .idx, it's just concatenating the strings together:
') AND NAME = ' + '.idx_'
And then there's a single-quote missing at the end of that line. It looks like your indexes are named after the table's name: tablename.idx_tablename, so try this:
DECLARE #startyear INT = 2000;
DECLARE #startQuarter INT = 1;
DECLARE #sql NVARCHAR(MAX);
DECLARE #tableName VARCHAR(50);
IF #startyear IS NULL
SET #startyear = 2000;
SET #startQuarter = 1;
WHILE #startyear <= YEAR(GETDATE())
BEGIN
SET #startQuarter = 1;
WHILE #startQuarter < 5
BEGIN
SET #tableName = 'FinData' + CAST(#startyear AS VARCHAR) + '_'
+ CAST(#startQuarter AS VARCHAR);
SET #sql = 'IF EXISTS(SELECT * FROM sys.indexes WHERE object_id = object_id('
+ #tableName + ') AND NAME = ' + #tableName + '.idx_' + #tableName + '
drop index' + #tableName + '.idx_' + #tableName;
PRINT #sql;
SET #startQuarter += 1;
END;
SET #startyear += 1;
END;
This does not answer your question, however, it is worth mentioning that if you want to do this without looping, you could create a virtual numbers table to join with.
;WITH
A AS(SELECT 0 AS Q UNION ALL SELECT 0),
B AS(SELECT 0 AS Q FROM A AS A CROSS JOIN A AS B),
C AS(SELECT 0 AS Q FROM B AS A CROSS JOIN B AS B),
D AS(SELECT 0 AS Q FROM C AS A CROSS JOIN C AS B),
E AS(SELECT 0 AS Q FROM D AS A CROSS JOIN D AS B),
F AS(SELECT 0 AS Q FROM E AS A CROSS JOIN E AS B),
Numbers AS(SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS Number FROM F)
SELECT
Y.Number,Q.Number
FROM
Numbers Y
INNER JOIN Numbers Q ON Q.Number BETWEEN 1 AND 4
WHERE
Y.Number BETWEEN 2000 AND YEAR(GETDATE())
ORDER BY
Y.Number

Convert a SQL function into a stored procedure

I am having trouble converting an UDF into a stored procedure.
Here is what I've got: this is the stored procedure that calls the function (I am using it to search for and remove all UNICODE characters that are not between 32 and 126):
ALTER PROCEDURE [dbo].[spRemoveUNICODE]
#FieldList varchar(250) = '',
#Multiple int = 0,
#TableName varchar(100) = ''
AS
BEGIN
SET NOCOUNT ON;
DECLARE #SQL VARCHAR(MAX), #counter INT = 0
IF #Multiple > 0
BEGIN
DECLARE #Field VARCHAR(100)
SELECT splitdata
INTO #TempValue
FROM dbo.fnSplitString(#FieldList,',')
WHILE (SELECT COUNT(*) FROM #TempValue) >= 1
BEGIN
DECLARE #Column VARCHAR(100) = (SELECT TOP 1 splitdata FROM #TempValue)
SET #SQL = 'UPDATE ' + #TableName + ' SET ' + #Column + ' = dbo.RemoveNonASCII(' + #Column + ')'
EXEC (#SQL)
--print #SQL
SET #counter = #counter + 1
PRINT #column + ' was checked for ' + #counter + ' rows.'
DELETE FROM #TempValue
WHERE splitdata = #Column
END
END
ELSE IF #Multiple = 0
BEGIN
SET #SQL = 'UPDATE ' + #TableName + ' SET ' + #FieldList + ' = dbo.RemoveNonASCII(' + #FieldList + ')'
EXEC (#SQL)
--print #SQL
SET #counter = #counter + 1
PRINT #column + ' was checked for ' + #counter + ' rows.'
END
END
And here is the UDF that I created to help with the update (RemoveNonASCII):
ALTER FUNCTION [dbo].[RemoveNonASCII]
(#nstring nvarchar(max))
RETURNS varchar(max)
AS
BEGIN
-- Variables
DECLARE #Result varchar(max) = '',#nchar nvarchar(1), #position int
-- T-SQL statements to compute the return value
set #position = 1
while #position <= LEN(#nstring)
BEGIN
set #nchar = SUBSTRING(#nstring, #position, 1)
if UNICODE(#nchar) between 32 and 127
set #Result = #Result + #nchar
set #position = #position + 1
set #Result = REPLACE(#Result,'))','')
set #Result = REPLACE(#Result,'?','')
END
if (#Result = '')
set #Result = null
-- Return the result
RETURN #Result
END
I've been trying to convert it into a stored procedure. I want to track how many rows actually get updated when this is run. Right now it just says that all rows, however many I run this on, are updated. I want to know if say only half of them had bad characters. The stored procedure is already set up so that it tells me which column it is looking at, I want to include how many rows were updated. Here is what I've tried so far:
DECLARE #Result varchar(max) = '',#nchar nvarchar(1), #position int, #nstring nvarchar(max), #counter int = 0, #CountRows int = 0, #Length int
--select Notes from #Temp where Notes is not null order by Notes OFFSET #counter ROWS FETCH NEXT 1 ROWS ONLY
set #nstring = (select Notes from #Temp where Notes is not null order by Notes OFFSET #counter ROWS FETCH NEXT 1 ROWS ONLY)
set #Length = LEN(#nstring)
if #Length = 0 set #Length = 1
-- Add the T-SQL statements to compute the return value here
set #position = 1
while #position <= #Length
BEGIN
print #counter
print #CountRows
select #nstring
set #nchar = SUBSTRING(#nstring, #position, 1)
if UNICODE(#nchar) between 32 and 127
begin
print unicode(#nchar)
set #Result = #Result + #nchar
set #counter = #counter + 1
end
if UNICODE(#nchar) not between 32 and 127
begin
set #CountRows = #CountRows + 1
end
set #position = #position + 1
END
print 'Rows found with invalid UNICODE: ' + convert(varchar,#CountRows)
Right now I'm purposely creating a temp table and adding a bunch of notes and then adding in a bunch of invalid characters.
I created a list of 700+ Notes and then updated 2 of them with some invalid characters (outside the 32 - 127). There are a few that are null and a few that are not null, but that doesn't have anything in them. What happens is that I get 0 updates.
Rows found with invalid UNICODE: 0
Though it does see that the UNICODE for the one that it pulls is 32.
Obviously I'm missing something I just don't see what it is.
Here is a set based solution to handle your bulk replacements. Instead of a slow scalar function this is utilizing an inline table valued function. These are far faster than their scalar ancestors. I am using a tally table here. I keep this as a view on my system like this.
create View [dbo].[cteTally] as
WITH
E1(N) AS (select 1 from (values (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))dt(n)),
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS
(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
)
select N from cteTally
If you are interested about tally tables here is an excellent article on the topic. http://www.sqlservercentral.com/articles/T-SQL/62867/
create function RemoveNonASCII
(
#SearchVal nvarchar(max)
) returns table as
RETURN
with MyValues as
(
select substring(#SearchVal, N, 1) as MyChar
, t.N
from cteTally t
where N <= len(#SearchVal)
and UNICODE(substring(#SearchVal, N, 1)) between 32 and 127
)
select distinct MyResult = STUFF((select MyChar + ''
from MyValues mv2
order by mv2.N
--for xml path('')), 1, 0, '')
FOR XML PATH(''),TYPE).value('.','NVARCHAR(MAX)'), 1, 0, '')
from MyValues mv
;
Now instead of being forced to call this every single row you can utilize cross apply. The performance benefit of just this portion of your original question should be pretty huge.
I also eluded to your string splitter also being a potential performance issue. Here is an excellent article with a number of very fast set based string splitters. http://sqlperformance.com/2012/07/t-sql-queries/split-strings
The last step here would be eliminate the first loop in your procedure. This can be done also but I am not entirely certain what your code is doing there. I will look closer and see what I can find out. In the meantime parse through this and feel free to ask questions about any parts you don't understand.
Here is what I've got working based on the great help from Sean Lange:
How I call the Stored Procedure:
exec spRemoveUNICODE #FieldList='Notes,Notes2,Notes3,Notes4,Notes5',#Multiple=1,#TableName='#Temp'
The #Temp table is created:
create table #Temp (ID int,Notes nvarchar(Max),Notes2 nvarchar(max),Notes3 nvarchar(max),Notes4 nvarchar(max),Notes5 nvarchar(max))
Then I fill it with comments from 5 fields from a couple of different tables that range in length from NULL to blank (but not null) to 5000 characters.
I then insert some random characters like this:
update #Temp
set Notes2 = SUBSTRING(Notes2,1,LEN(Notes2)/2) + N'￿㹊潮Ņ᯸ࢹᖈư㹨ƶ槹鎤⻄ƺ綐ڌ⸀ƺ삸)䀤ƍ샄)Ņᛡ鎤ꗘᖃᒨ쬵Ğᘍ鎤ᐜᏰ>֔υ赸Ƹ쳰డ촜)鉀௿촜)쮜)Ἡ屰山舰霡ࣆ 耏Аం畠Ư놐ᓜતᏛ֔Ꮫ֨Ꮫ꯼ᓜƒ 邰఍厰ఆ邰఍드)抉鎤듄)繟Ĺ띨)᯸ࢹ䮸ࣉ᯸ࢹ䮸ࣉ샰)ԌƏ￿

Sql query for month wise

I was trying on a sql query where we have search on month wise
for example my table is like
Username visited Visisted_Dated
A 1 01/11/2010
B 1 10/11/2010
A 1 03/12/2010
B 1 06/12/2010
B 1 06/12/2010
A 1 03/02/2011
B 1 05/02/2011
A 1 11/03/2011
A 1 20/03/2011
B 1 01/03/2011
Now if i want to search the users for no of visited between Feb to April i need to get output as
Users Nov_2010 Dec_2010 Jan_2011 Feb_2011 March_2011
A 1 1 0 1 2
B 1 2 0 1 1
Please let me know the way to proceed.
Thanks
I'm afraid you have to make use of dynamic SQL:
DECLARE #Sql VARCHAR(8000)
DECLARE #ColumnNames VARCHAR(1000)
DECLARE #BeginDate DATETIME
DECLARE #EndDate DATETIME
SET #BeginDate = '2010-11-1'
SET #EndDate = '2011-4-1'
SET #ColumnNames = ''
WHILE #BeginDate <= #EndDate
BEGIN
SET #ColumnNames = #ColumnNames + ',[' + DateName(month,#BeginDate) + '_' + Cast(Year(#BeginDate) AS VARCHAR(4)) + ']'
SET #BeginDate = DateAdd(Month, 1, #BeginDate)
END
IF Len(#ColumnNames) > 0
SET #ColumnNames = Right(#ColumnNames, Len(#ColumnNames) - 1)
PRINT #ColumnNames
SET #Sql = '
WITH U AS
(
SELECT UserName, DateName(month,Visited_Dated) + ''_'' + Cast(Year(Visited_Dated) AS VARCHAR(4)) AS VisitedMonth, Visited
FROM Users
)
SELECT *
FROM U
PIVOT (
SUM(Visited) FOR VisitedMonth IN (' + #ColumnNames + ')
) AS P'
EXEC (#Sql)
There it´s a solution based on PIVOT
SELECT UserName,[201011], [201012], [201101], [201102], [201103]
FROM
(SELECT UserName,Visited,convert(varchar(6),[Visited_Dated],112) Periodo
FROM [Table]) AS st
PIVOT
(
COUNT(Visited)
FOR Periodo IN ([201011], [201012], [201101], [201102], [201103])
) AS pt;

Resources