Summarizing month columns to fiscal year with SQL Server - sql-server

I want to summarize a table that is mostly made up of a sequence of months. Each month's column has a header in the format of MMMYY. I need to dynamically summarize this table by fiscal year. Our fiscal year starts in November, so the input to output would be as follows:
Input:
Output:
Does anyone have any suggestions on how I can go about doing this using SQL Server? I've thought about partitioning a temporary table containing all of the given months and years by the monthly fiscal year break (NovYY), but I haven't figured out how to even partition based on a LIKE keyword search. Bottom line - I'm completely stumped as to how to effectively do this without hard coding the dates.
Let's say the sample input above is dbo.USA_Historic_Txg_Revenue. I have the following so far:
select * into #temp from dbo.USA_Historic_Txg_Revenue
with MMMYY as
(
select Name
from tempdb.sys.columns
where object_id = OBJECT_ID('tempdb..#temp')
and isnumeric(right(name, 2)) = 1
)
select * from MMMYY

Something like this is what I think you're looking for:
--CREATE TABLE #table (Name NVARCHAR(100), Oct12 INT, Nov12 INT, Dec12 INT, Jan13 INT, Feb13 INT, Mar13 INT, Apr13 INT, May13 INT, Jun13 INT, Jul13 INT, Aug13 INT, Sep13 INT, Oct13 INT, Nov13 INT, Dec13 INT, Jan14 INT, Feb14 INT, Mar14 INT)
--INSERT #table VALUES ('Record1',5,3,2,0,1,5,4,3,6,5,4,2,1,0,1,1,1,2)
--, ('Record2',4,1,3,2,1,3,3,1,4,4,3,2,1,0,2,2,2,2);
DECLARE #monthlist NVARCHAR(MAX)
SELECT #monthlist = ISNULL(#monthlist + ',', '') + Name -- select top 100 *
FROM tempdb.sys.columns
WHERE [object_id] = OBJECT_ID('tempdb..#table')
AND Name <> 'Name';
DECLARE #SQL NVARCHAR(MAX) =
'SELECT DISTINCT Name
, SUM(Val) OVER (PARTITION BY Name, CASE WHEN Mth LIKE ''Nov%'' OR Mth LIKE ''Dec%'' THEN RIGHT(Mth, 2) ELSE RIGHT(Mth, 2) - 1 END) InsertColName
, CASE WHEN Mth LIKE ''Nov%'' OR Mth LIKE ''Dec%'' THEN ''Nov'' + RIGHT(Mth, 2) ELSE ''Nov'' + CONVERT(NVARCHAR, RIGHT(Mth, 2) - 1) END FiscalStart
FROM #table
UNPIVOT (Val FOR Mth IN (' + #monthlist + ')) a
ORDER BY Name, CASE WHEN Mth LIKE ''Nov%'' OR Mth LIKE ''Dec%'' THEN ''Nov'' + RIGHT(Mth, 2) ELSE ''Nov'' + CONVERT(NVARCHAR, RIGHT(Mth, 2) - 1) END'
EXEC(#SQL)
For your particular query, you can derive monthlist directly from the table without the need of a temporary table...
e.g.
DECLARE #monthlist NVARCHAR(MAX)
SELECT #monthlist = ISNULL(#monthlist + ',', '') + COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'USA_Historic_Txg_Revenue'
AND COLUMN_NAME <> 'Name';
DECLARE #SQL NVARCHAR(MAX) =
'SELECT DISTINCT Name
, SUM(Val) OVER (PARTITION BY Name, CASE WHEN Mth LIKE ''Nov%'' OR Mth LIKE ''Dec%'' THEN RIGHT(Mth, 2) ELSE RIGHT(Mth, 2) - 1 END) InsertColName
, CASE WHEN Mth LIKE ''Nov%'' OR Mth LIKE ''Dec%'' THEN ''Nov'' + RIGHT(Mth, 2) ELSE ''Nov'' + CONVERT(NVARCHAR, RIGHT(Mth, 2) - 1) END FiscalStart
FROM dbo.USA_Historic_Txg_Revenue
UNPIVOT (Val FOR Mth IN (' + #monthlist + ')) a
ORDER BY Name, CASE WHEN Mth LIKE ''Nov%'' OR Mth LIKE ''Dec%'' THEN ''Nov'' + RIGHT(Mth, 2) ELSE ''Nov'' + CONVERT(NVARCHAR, RIGHT(Mth, 2) - 1) END'
EXEC(#SQL)

[Summarizing months columns using pivot][1]
---creating the tables
CREATE TABLE [dbo].[Table1]([PID] [int] NULL,[ProductDesc] [nvarchar](50) NULL,[ProductCode] [nvarchar](10) NULL) ON [PRIMARY]
CREATE TABLE [dbo].[Table2]([Date] [varchar](50) NULL,[PID] [int] NULL) ON [PRIMARY]
---insert script---
INSERT [dbo].[Table1] ([PID], [ProductDesc], [ProductCode]) VALUES (1, N'Packet-Eye', N'P001')
INSERT [dbo].[Table1] ([PID], [ProductDesc], [ProductCode]) VALUES (2, N'Wiggy', N'W099 ')
INSERT [dbo].[Table1] ([PID], [ProductDesc], [ProductCode]) VALUES (3, N'Wimax-Lite', N'W001')
INSERT [dbo].[Table1] ([PID], [ProductDesc], [ProductCode]) VALUES (4, N'Wimax-Home', N'e W002 ')
INSERT [dbo].[Table2] ([Date], [PID]) VALUES (N'1/14/2009 ', 1)
INSERT [dbo].[Table2] ([Date], [PID]) VALUES (N'1/15/2009 ', 1)
INSERT [dbo].[Table2] ([Date], [PID]) VALUES (N'2/1/2009', 2)
INSERT [dbo].[Table2] ([Date], [PID]) VALUES (N'3/3/2009', 3)
GO
SELECT *
FROM
(
SELECT t1.productdesc as pd,COUNT(month(t2.date))as dates,
DateName( month , DateAdd( month , MONTH(t2.date) , 0 ) - 1 ) as mon
FROM table1 t1 inner join table2 t2 on t1.pid=t2.pid
where year(date) between 2009
and 2010 group by productdesc,month(t2.date),month (t2.date)
) AS D
PIVOT
(
sum(dates)
FOR mon IN( [January],[February],[March],[April],[May],[June],[July],[August],[September],[October],[November],[December])
) AS P
[using pivot in sqlserver please find the result in the below image][1]
[1]: http://i.stack.imgur.com/jio6c.png

Related

Cannot convert Varchar to DateTime - Conversion failed when converting date and/or time from character string

Source Data that I am working with, this is all in one column called body. This data unfortunately cannot be changed. The data is coming in via an email from outside our Network it then goes into Email2DB. Email2DB is an application that puts the email data into a table.
This is not how data is usually stored in our tables, this is a temporary thing in place to log issues from user pc's whilst off the network so that number of issues can be tracked.
Body
21/05/2020| 16:01| Pathe 2.0 Delay| Pathe| LRW10| terence
For reporting purposes I am splitting the data into individual columns.
I need to get the separate date and time to be in one column as DateTime Format.
For example: 2020-05-21 16:01
I am trying to enter the data into the following temp table, but for the life of me I cannot get the date and time to convert to DateTime.
DECLARE #QuickData TABLE (TicketDate datetime, Issue varchar(50), Category varchar(50), ComputerName varchar(30), UserName varchar(50));
If I change the temp table TicketDate to be TicketDate varchar(25) the data goes in fine. However end goal is to be able to pass in a #DateFrom and #DateTo and be able to do a where TicketDate between #DateFrom and #DateTo and I cannot do this unless TicketDate is datetime format.
What I can't understand is even if I insert the data into a the temp table with Ticket as Varchar, the result looks like the below
Result if insert TicketDate as Varchar
One the data is in the temp table I have tried running a select statement using cast as datetime and cast as datetime2 and also have tried convert to datetime, but nothing works I always get the same error
Conversion failed when converting date and/or time from character string.
I will add one of the attempts I have tried below if anyone can work it out I will be so grateful.
Please know I have googled endless ideas and tried multi formats for the date time.
DECLARE #dateFrom DateTime = '2020-05-21 08:00:00'
DECLARE #dateTo DateTime = '2020-05-25 17:30:00'
DECLARE #QuickData TABLE (TicketDate varchar(25), Issue varchar(50), Category varchar(50), ComputerName varchar(30), UserName varchar(50));
INSERT INTO #QuickData
SELECT CONVERT(VARCHAR(25), (TRIM(CONVERT(VARCHAR(10), CONVERT(date, dbo.f_SplitString(body,1,'|'), 105), 23)) + dbo.f_SplitString(body,2,'|') + ':00'),120),
dbo.f_SplitString(body,3,'|') as 'Issue', dbo.f_SplitString(body,4,'|') as 'Category',
dbo.f_SplitString(body,5,'|') as 'PCName', dbo.f_SplitString(body,6,'|') as 'User' FROM t_Quick;
SELECT convert(datetime,TicketDate) as 'TicketDate' , Issue, Category, ComputerName, UserName
FROM #QuickData
WHERE TicketDate between #dateFrom and #dateTo
ORDER BY TicketDate DESC;
The Split String Function used is below.
USE [Central]
GO
/****** Object: UserDefinedFunction [dbo].[f_SplitString] Script Date: 26/06/2020 00:07:31 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author: <Author,,Name>
-- Create date: <Create Date, ,>
-- Description: <Description, ,>
-- =============================================
ALTER FUNCTION [dbo].[f_SplitString]
(
#multiwordstring VARCHAR(255),
#wordnumber integer,
#delimiter char(1)
)
returns VARCHAR(255)
AS
BEGIN
DECLARE #remainingstring VARCHAR(255)
SET #remainingstring=#multiwordstring
DECLARE #numberofwords NUMERIC
SET #numberofwords=(LEN(#remainingstring) - LEN(REPLACE(#remainingstring, #delimiter, '')) + 1)
DECLARE #word VARCHAR(50)
DECLARE #parsedwords TABLE
(
line NUMERIC IDENTITY(1, 1),
word VARCHAR(255)
)
WHILE #numberofwords > 1
BEGIN
SET #word=LEFT(#remainingstring, CHARINDEX(#delimiter, #remainingstring) - 1)
INSERT INTO #parsedwords(word)
SELECT #word
SET #remainingstring= REPLACE(#remainingstring, #word + #delimiter, '')
SET #numberofwords=(LEN(#remainingstring) - LEN(REPLACE(#remainingstring, #delimiter, '')) + 1)
IF #numberofwords = 1
BREAK
ELSE
CONTINUE
END
IF #numberofwords = 1
SELECT #word = #remainingstring
INSERT INTO #parsedwords(word)
SELECT #word
RETURN
(SELECT RTrim(LTrim(word))
FROM #parsedwords
WHERE line = #wordnumber)
END
Another approach is to create a table-valued function like this, which returns a string split in order:
CREATE FUNCTION dbo.fnOrderedStringSplit
(
#List varchar(4000),
#Delimiter char(1)
)
RETURNS TABLE
AS
RETURN
(
SELECT Element = ROW_NUMBER() OVER (ORDER BY [Number]),
[Value] = LTRIM(RTRIM(SUBSTRING(#List, [Number],
CHARINDEX(#Delimiter, #List + #Delimiter, [Number]
) - [Number])))
FROM (SELECT TOP (4000) ROW_NUMBER() OVER (ORDER BY [object_id])
FROM sys.all_columns) AS x ([Number])
WHERE SUBSTRING(#Delimiter + #List, [Number], 1) = #Delimiter
AND Number <= LEN(#List)
);
Then you can pivot that to extract specific elements:
-- INSERT #QuickData(TicketDate, Issue, Category, ComputerName, UserName)
SELECT TicketDate = TRY_CONVERT(datetime, [1] + ' ' + [2], 103),
Issue = [3],
Category = [4],
PCName = [5],
UserName = [6]
FROM dbo.t_QuickData AS t
CROSS APPLY dbo.fnOrderedStringSplit(t.body, '|') AS f
PIVOT (MAX([Value]) FOR Element IN ([1],[2],[3],[4],[5],[6])) AS p;
One option is to parse the string via XML (JSON is another option if 2016)
Example
Declare #YourTable Table ([ID] varchar(50),[body] varchar(150)) Insert Into #YourTable Values
(1,'21/05/2020| 16:01| Pathe 2.0 Delay| Pathe| LRW10| terence')
Select A.ID
,DateCol = try_convert(datetime,Pos1+' '+Pos2,103)
,Issue = Pos3
,Category = Pos4
,PCName = Pos5
,[User] = Pos6
From #YourTable A
Cross Apply (
Select Pos1 = ltrim(rtrim(xDim.value('/x[1]','varchar(max)')))
,Pos2 = ltrim(rtrim(xDim.value('/x[2]','varchar(max)')))
,Pos3 = ltrim(rtrim(xDim.value('/x[3]','varchar(max)')))
,Pos4 = ltrim(rtrim(xDim.value('/x[4]','varchar(max)')))
,Pos5 = ltrim(rtrim(xDim.value('/x[5]','varchar(max)')))
,Pos6 = ltrim(rtrim(xDim.value('/x[6]','varchar(max)')))
From ( values (cast('<x>' + replace((Select replace([body],'|','§§Split§§') as [*] For XML Path('')),'§§Split§§','</x><x>')+'</x>' as xml))) as A(xDim)
) B
Returns
Solution
Your string splitting function trims strings, hence you are loosing the space between date and time components.
Change your code
SELECT CONVERT(VARCHAR(25), (TRIM(CONVERT(VARCHAR(10), CONVERT(date, dbo.f_SplitString(body,1,'|'), 105), 23))
+ dbo.f_SplitString(body,2,'|') + ':00'),120),
to
SELECT CONVERT(VARCHAR(25), (TRIM(CONVERT(VARCHAR(10), CONVERT(date, dbo.f_SplitString(body,1,'|'), 105), 23))
+ ' ' + dbo.f_SplitString(body,2,'|') + ':00'),120),
Note the blank space before dbo.f_SplitString(body,2,'|')
"Give a Man a Fish, and You Feed Him for a Day. Teach a Man To Fish, and You Feed Him for a Lifetime"
You can easily debug this yourself by deconstructing your query as follows:
SELECT CONVERT(date, dbo.f_SplitString(body,1,'|'), 105) FROM t_Quick
SELECT CONVERT(date, dbo.f_SplitString(body,1,'|'), 105), 23)) FROM t_Quick
SELECT CONVERT(date, dbo.f_SplitString(body,1,'|'), 105), 23)) + dbo.f_SplitString(body,2,'|') FROM t_Quick
You can look at the output at each stage to see where the problem lays.
Thank you all for your suggestions. I have finally figured out the problem though. When converting the time there is a 'not ordinary' space the space cannot be removed by the TRIM and so the solution was using substring and it works great.
DECLARE #QuickData TABLE (TicketDate datetime, Issue varchar(50), Category varchar(50), ComputerName varchar(30), UserName varchar(50));
INSERT INTO #QuickData
SELECT convert(datetime, try_CONVERT(VARCHAR(25), (TRIM(CONVERT(VARCHAR(10), CONVERT(date, dbo.f_SplitString(body,1,'|'), 105), 23)) + ' ' + substring(dbo.f_SplitString(body,2,'|'),2,5) + ':00'),120)),
dbo.f_SplitString(body,3,'|') as 'Issue', dbo.f_SplitString(body,4,'|') as 'Category',
dbo.f_SplitString(body,5,'|') as 'PCName', dbo.f_SplitString(body,6,'|') as 'User' FROM t_Quick;
Select * from #QuickData
where TicketDate between #dateFrom and #dateTo
order by 1 desc;

Counting number of Populated Columns by Row

I wanted to know if there is a way of counting the number of populated columns per row of a table.
For example if I have the simple table below Called Customer:
**Name** **Customer** **DOB** **Order number** **Populated Columns**
ABC Ltd Jo Blogg 2/1/78 123 3
Umbrella Co A Sherman 232 2
Nike 14/5/98 1
What I want is a query which will give me an extra column with a number saying how many columns have a value in them.
Any ideas?
Can be done via trivial check on NULL (and empty strings for such columns):
SELECT
[Name]
, [Customer]
, [DOB]
, [Order number]
, CASE WHEN ISNULL([Name], '') != '' THEN 1 ELSE 0 END
+ CASE WHEN ISNULL([Customer], '') != '' THEN 1 ELSE 0 END
+ CASE WHEN [DOB] IS NOT NULL THEN 1 ELSE 0 END
+ CASE WHEN [Order number] IS NOT NULL THEN 1 ELSE 0 END AS [Populated Columns]
This will work nicely for a fixed and known number of columns.
Such an approach can be perhaps more universal if columns list fetched from the metadata. As a downside - this requires a dynamic SQL.
Below is an example for SQL Server 2017 and higher:
DECLARE #_SQL NVARCHAR(max)
DECLARE #_TableName sysname = 'Table1'
SELECT #_SQL =
'SELECT '
+ STRING_AGG(QUOTENAME(COLUMN_NAME), ',
')
+ ', '
+ STRING_AGG('
CASE WHEN ['+COLUMN_NAME+'] IS NOT NULL THEN 1 ELSE 0 END', ' +')
+ ' AS [Populated Columns]
FROM ' + QUOTENAME(MIN(TABLE_SCHEMA)) + '.' + QUOTENAME(MIN(TABLE_NAME))
FROM INFORMATION_SCHEMA.COLUMNs
WHERE TABLE_NAME = #_TableName
EXEC sys.sp_executesql #_SQL
It will generate and execute a code:
SELECT
[Col1],
[Col2],
[Col3],
CASE WHEN [Col1] IS NOT NULL THEN 1 ELSE 0 END +
CASE WHEN [Col2] IS NOT NULL THEN 1 ELSE 0 END +
CASE WHEN [Col3] IS NOT NULL THEN 1 ELSE 0 END AS [Populated Columns]
FROM [dbo].[Table1]
In older versions, such result is achievable but with other string aggregation workarounds, like XML STUFF or SQLCLR functions...
Just thought of sharing another approach using UNPIVOT to calculate the same, assuming that you will have a primary key/identity in your table.
declare #tmp table (id int, [Name] varchar(100), Customer varchar(100), dob datetime, orderno int)
insert into #tmp select 1, 'name1','c1',getdate(),123
insert into #tmp select 2,'name2',null,getdate(),123
insert into #tmp select 3,'name3',null,null,null
SELECT t.*,
t1.notpopulated
FROM #tmp t
INNER JOIN (SELECT 4 - Count(*) AS NotPopulated,
id
FROM
(SELECT id,
u.x,
u.y
FROM (SELECT id,
Cast([name]AS VARCHAR(100)) [name],
Cast(customer AS VARCHAR(100)) AS customer,
Cast(dob AS VARCHAR(100)) AS dob1,
Cast(orderno AS VARCHAR(100)) orderno
FROM #tmp) AS s
UNPIVOT ( [y]
FOR [x] IN ([name],
[Customer],
dob1,
[orderno]) ) u) t
GROUP BY id) t1
ON t1.id = t.id
Online Demo

create view with declare in script. Procedure and Function doesn't work

I cant create view with declare.
i even tried to create procedure but than i found that i cant create view with procedure. and i think same will be for function. only thing i can think of is to avoid declare. How can I find the way around? i need to have view table (in order to be able to see them in power bi in the future, i cant see it from stored procedure in it)
declare #T as table (
Name nvarchar(100)
, OBJECTID varchar(50)
, started_working datetime
,STOPFROM datetime
,STARTDATE datetime
,STOPDATE datetime
,MODIFIEDDATETIME datetime
,START_STOP int
,STARTDESCRIPTION nvarchar(300)
,STOPDESCRIPTION nvarchar(300)
,wattage nvarchar (50)
,purpose nvarchar(300)
,location nvarchar(300)
,finished_working datetime
,oldDiff int
)
insert into #T
select
NAME
,OBJECTID
,STOPTO
,STOPFROM
,STARTDATE
,STOPDATE
,MODIFIEDDATETIME
,START_STOP
,STARTDESCRIPTION
,STOPDESCRIPTION
,wattage
,purpose
,location
,next_stopfrom
,diff
FROM [MicrosoftDynamicsAX].[dbo].[mroobjectengineworkinghours]
;with rcte as (
select
*, started_working2 = started_working
, next_date = cast(dateadd(dd, 1, started_working) as date), 1 step
from
#T
union all
select
Name,OBJECTID, started_working,STOPFROM,STARTDATE,STOPDATE,MODIFIEDDATETIME,START_STOP,STARTDESCRIPTION
,STOPDESCRIPTION,wattage
,purpose
,location, finished_working,oldDiff
, cast(next_date as datetime)
, dateadd(dd, 1, next_date), step + 1
from
rcte
where
next_date < finished_working
)
select
Name,OBJECTID, started_working,STOPFROM,STARTDATE,STOPDATE,MODIFIEDDATETIME,START_STOP,STARTDESCRIPTION
,STOPDESCRIPTION,wattage
,purpose
,location,oldDiff, started_working2, finished_working
, right(replace(str(diff / 60), ' ', 0), 2) + ':' + right(replace(str(diff % 60), ' ', 0), 2) hours_worked
from (
select
Name,OBJECTID, started_working,STOPFROM,STARTDATE,STOPDATE,MODIFIEDDATETIME,START_STOP,STARTDESCRIPTION
,STOPDESCRIPTION,wattage
,purpose
,location,oldDiff
, case
when step = 1 then started_working
else started_working2
end started_working2
, case
when step = max(step) over (partition by Name, started_working)
then finished_working else next_date
end finished_working
from
rcte
) t
cross apply (select datediff(mi, started_working2, finished_working) diff) ca
OPTION (MAXRECURSION 0);
I agree with Sean Lange.
Why not in your cte simply call the table directly?
Such as:
;with rcte as (
select *
, started_working2 = started_working
, next_date = cast(dateadd(dd, 1, started_working) as date)
, 1 step
from [MicrosoftDynamicsAX].[dbo].[mroobjectengineworkinghours] ...

SQL Server group by count eliminate duplicates [duplicate]

How do I get:
id Name Value
1 A 4
1 B 8
2 C 9
to
id Column
1 A:4, B:8
2 C:9
No CURSOR, WHILE loop, or User-Defined Function needed.
Just need to be creative with FOR XML and PATH.
[Note: This solution only works on SQL 2005 and later. Original question didn't specify the version in use.]
CREATE TABLE #YourTable ([ID] INT, [Name] CHAR(1), [Value] INT)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (1,'A',4)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (1,'B',8)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (2,'C',9)
SELECT
[ID],
STUFF((
SELECT ', ' + [Name] + ':' + CAST([Value] AS VARCHAR(MAX))
FROM #YourTable
WHERE (ID = Results.ID)
FOR XML PATH(''),TYPE).value('(./text())[1]','VARCHAR(MAX)')
,1,2,'') AS NameValues
FROM #YourTable Results
GROUP BY ID
DROP TABLE #YourTable
If it is SQL Server 2017 or SQL Server Vnext, SQL Azure you can use STRING_AGG as below:
SELECT id, STRING_AGG(CONCAT(name, ':', [value]), ', ')
FROM #YourTable
GROUP BY id
using XML path will not perfectly concatenate as you might expect... it will replace "&" with "&" and will also mess with <" and ">
...maybe a few other things, not sure...but you can try this
I came across a workaround for this... you need to replace:
FOR XML PATH('')
)
with:
FOR XML PATH(''),TYPE
).value('(./text())[1]','VARCHAR(MAX)')
...or NVARCHAR(MAX) if thats what youre using.
why the hell doesn't SQL have a concatenate aggregate function? this is a PITA.
I ran into a couple of problems when I tried converting Kevin Fairchild's suggestion to work with strings containing spaces and special XML characters (&, <, >) which were encoded.
The final version of my code (which doesn't answer the original question but may be useful to someone) looks like this:
CREATE TABLE #YourTable ([ID] INT, [Name] VARCHAR(MAX), [Value] INT)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (1,'Oranges & Lemons',4)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (1,'1 < 2',8)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (2,'C',9)
SELECT [ID],
STUFF((
SELECT ', ' + CAST([Name] AS VARCHAR(MAX))
FROM #YourTable WHERE (ID = Results.ID)
FOR XML PATH(''),TYPE
/* Use .value to uncomment XML entities e.g. > < etc*/
).value('.','VARCHAR(MAX)')
,1,2,'') as NameValues
FROM #YourTable Results
GROUP BY ID
DROP TABLE #YourTable
Rather than using a space as a delimiter and replacing all the spaces with commas, it just pre-pends a comma and space to each value then uses STUFF to remove the first two characters.
The XML encoding is taken care of automatically by using the TYPE directive.
Another option using Sql Server 2005 and above
---- test data
declare #t table (OUTPUTID int, SCHME varchar(10), DESCR varchar(10))
insert #t select 1125439 ,'CKT','Approved'
insert #t select 1125439 ,'RENO','Approved'
insert #t select 1134691 ,'CKT','Approved'
insert #t select 1134691 ,'RENO','Approved'
insert #t select 1134691 ,'pn','Approved'
---- actual query
;with cte(outputid,combined,rn)
as
(
select outputid, SCHME + ' ('+DESCR+')', rn=ROW_NUMBER() over (PARTITION by outputid order by schme, descr)
from #t
)
,cte2(outputid,finalstatus,rn)
as
(
select OUTPUTID, convert(varchar(max),combined), 1 from cte where rn=1
union all
select cte2.outputid, convert(varchar(max),cte2.finalstatus+', '+cte.combined), cte2.rn+1
from cte2
inner join cte on cte.OUTPUTID = cte2.outputid and cte.rn=cte2.rn+1
)
select outputid, MAX(finalstatus) from cte2 group by outputid
Install the SQLCLR Aggregates from http://groupconcat.codeplex.com
Then you can write code like this to get the result you asked for:
CREATE TABLE foo
(
id INT,
name CHAR(1),
Value CHAR(1)
);
INSERT INTO dbo.foo
(id, name, Value)
VALUES (1, 'A', '4'),
(1, 'B', '8'),
(2, 'C', '9');
SELECT id,
dbo.GROUP_CONCAT(name + ':' + Value) AS [Column]
FROM dbo.foo
GROUP BY id;
Eight years later... Microsoft SQL Server vNext Database Engine has finally enhanced Transact-SQL to directly support grouped string concatenation. The Community Technical Preview version 1.0 added the STRING_AGG function and CTP 1.1 added the WITHIN GROUP clause for the STRING_AGG function.
Reference: https://msdn.microsoft.com/en-us/library/mt775028.aspx
SQL Server 2005 and later allow you to create your own custom aggregate functions, including for things like concatenation- see the sample at the bottom of the linked article.
This is just an addition to Kevin Fairchild's post (very clever by the way). I would have added it as a comment, but I don't have enough points yet :)
I was using this idea for a view I was working on, however the items I was concatinating contained spaces. So I modified the code slightly to not use spaces as delimiters.
Again thanks for the cool workaround Kevin!
CREATE TABLE #YourTable ( [ID] INT, [Name] CHAR(1), [Value] INT )
INSERT INTO #YourTable ([ID], [Name], [Value]) VALUES (1, 'A', 4)
INSERT INTO #YourTable ([ID], [Name], [Value]) VALUES (1, 'B', 8)
INSERT INTO #YourTable ([ID], [Name], [Value]) VALUES (2, 'C', 9)
SELECT [ID],
REPLACE(REPLACE(REPLACE(
(SELECT [Name] + ':' + CAST([Value] AS VARCHAR(MAX)) as A
FROM #YourTable
WHERE ( ID = Results.ID )
FOR XML PATH (''))
, '</A><A>', ', ')
,'<A>','')
,'</A>','') AS NameValues
FROM #YourTable Results
GROUP BY ID
DROP TABLE #YourTable
An example would be
In Oracle you can use LISTAGG aggregate function.
Original records
name type
------------
name1 type1
name2 type2
name2 type3
Sql
SELECT name, LISTAGG(type, '; ') WITHIN GROUP(ORDER BY name)
FROM table
GROUP BY name
Result in
name type
------------
name1 type1
name2 type2; type3
This kind of question is asked here very often, and the solution is going to depend a lot on the underlying requirements:
https://stackoverflow.com/search?q=sql+pivot
and
https://stackoverflow.com/search?q=sql+concatenate
Typically, there is no SQL-only way to do this without either dynamic sql, a user-defined function, or a cursor.
Just to add to what Cade said, this is usually a front-end display thing and should therefore be handled there. I know that sometimes it's easier to write something 100% in SQL for things like file export or other "SQL only" solutions, but most of the times this concatenation should be handled in your display layer.
Don't need a cursor... a while loop is sufficient.
------------------------------
-- Setup
------------------------------
DECLARE #Source TABLE
(
id int,
Name varchar(30),
Value int
)
DECLARE #Target TABLE
(
id int,
Result varchar(max)
)
INSERT INTO #Source(id, Name, Value) SELECT 1, 'A', 4
INSERT INTO #Source(id, Name, Value) SELECT 1, 'B', 8
INSERT INTO #Source(id, Name, Value) SELECT 2, 'C', 9
------------------------------
-- Technique
------------------------------
INSERT INTO #Target (id)
SELECT id
FROM #Source
GROUP BY id
DECLARE #id int, #Result varchar(max)
SET #id = (SELECT MIN(id) FROM #Target)
WHILE #id is not null
BEGIN
SET #Result = null
SELECT #Result =
CASE
WHEN #Result is null
THEN ''
ELSE #Result + ', '
END + s.Name + ':' + convert(varchar(30),s.Value)
FROM #Source s
WHERE id = #id
UPDATE #Target
SET Result = #Result
WHERE id = #id
SET #id = (SELECT MIN(id) FROM #Target WHERE #id < id)
END
SELECT *
FROM #Target
Let's get very simple:
SELECT stuff(
(
select ', ' + x from (SELECT 'xxx' x union select 'yyyy') tb
FOR XML PATH('')
)
, 1, 2, '')
Replace this line:
select ', ' + x from (SELECT 'xxx' x union select 'yyyy') tb
With your query.
You can improve performance significant the following way if group by contains mostly one item:
SELECT
[ID],
CASE WHEN MAX( [Name]) = MIN( [Name]) THEN
MAX( [Name]) NameValues
ELSE
STUFF((
SELECT ', ' + [Name] + ':' + CAST([Value] AS VARCHAR(MAX))
FROM #YourTable
WHERE (ID = Results.ID)
FOR XML PATH(''),TYPE).value('(./text())[1]','VARCHAR(MAX)')
,1,2,'') AS NameValues
END
FROM #YourTable Results
GROUP BY ID
didn't see any cross apply answers, also no need for xml extraction. Here is a slightly different version of what Kevin Fairchild wrote. It's faster and easier to use in more complex queries:
select T.ID
,MAX(X.cl) NameValues
from #YourTable T
CROSS APPLY
(select STUFF((
SELECT ', ' + [Name] + ':' + CAST([Value] AS VARCHAR(MAX))
FROM #YourTable
WHERE (ID = T.ID)
FOR XML PATH(''))
,1,2,'') [cl]) X
GROUP BY T.ID
Using the Stuff and for xml path operator to concatenate rows to string :Group By two columns -->
CREATE TABLE #YourTable ([ID] INT, [Name] CHAR(1), [Value] INT)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (1,'A',4)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (1,'B',8)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (1,'B',5)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (2,'C',9)
-- retrieve each unique id and name columns and concatonate the values into one column
SELECT
[ID],
STUFF((
SELECT ', ' + [Name] + ':' + CAST([Value] AS VARCHAR(MAX)) -- CONCATONATES EACH APPLICATION : VALUE SET
FROM #YourTable
WHERE (ID = Results.ID and Name = results.[name] )
FOR XML PATH(''),TYPE).value('(./text())[1]','VARCHAR(MAX)')
,1,2,'') AS NameValues
FROM #YourTable Results
GROUP BY ID
SELECT
[ID],[Name] , --these are acting as the group by clause
STUFF((
SELECT ', '+ CAST([Value] AS VARCHAR(MAX)) -- CONCATONATES THE VALUES FOR EACH ID NAME COMBINATION
FROM #YourTable
WHERE (ID = Results.ID and Name = results.[name] )
FOR XML PATH(''),TYPE).value('(./text())[1]','VARCHAR(MAX)')
,1,2,'') AS NameValues
FROM #YourTable Results
GROUP BY ID, name
DROP TABLE #YourTable
Using Replace Function and FOR JSON PATH
SELECT T3.DEPT, REPLACE(REPLACE(T3.ENAME,'{"ENAME":"',''),'"}','') AS ENAME_LIST
FROM (
SELECT DEPT, (SELECT ENAME AS [ENAME]
FROM EMPLOYEE T2
WHERE T2.DEPT=T1.DEPT
FOR JSON PATH,WITHOUT_ARRAY_WRAPPER) ENAME
FROM EMPLOYEE T1
GROUP BY DEPT) T3
For sample data and more ways click here
If you have clr enabled you could use the Group_Concat library from GitHub
Another example without the garbage: ",TYPE).value('(./text())[1]','VARCHAR(MAX)')"
WITH t AS (
SELECT 1 n, 1 g, 1 v
UNION ALL
SELECT 2 n, 1 g, 2 v
UNION ALL
SELECT 3 n, 2 g, 3 v
)
SELECT g
, STUFF (
(
SELECT ', ' + CAST(v AS VARCHAR(MAX))
FROM t sub_t
WHERE sub_t.g = main_t.g
FOR XML PATH('')
)
, 1, 2, ''
) cg
FROM t main_t
GROUP BY g
Input-output is
************************* -> *********************
* n * g * v * * g * cg *
* - * - * - * * - * - *
* 1 * 1 * 1 * * 1 * 1, 2 *
* 2 * 1 * 2 * * 2 * 3 *
* 3 * 2 * 3 * *********************
*************************
I used this approach which may be easier to grasp. Get a root element, then concat to choices any item with the same ID but not the 'official' name
Declare #IdxList as Table(id int, choices varchar(max),AisName varchar(255))
Insert into #IdxLIst(id,choices,AisName)
Select IdxId,''''+Max(Title)+'''',Max(Title) From [dbo].[dta_Alias]
where IdxId is not null group by IdxId
Update #IdxLIst
set choices=choices +','''+Title+''''
From #IdxLIst JOIN [dta_Alias] ON id=IdxId And Title <> AisName
where IdxId is not null
Select * from #IdxList where choices like '%,%'
For all my healthcare folks out there:
SELECT
s.NOTE_ID
,STUFF ((
SELECT
[note_text] + ' '
FROM
HNO_NOTE_TEXT s1
WHERE
(s1.NOTE_ID = s.NOTE_ID)
ORDER BY [line] ASC
FOR XML PATH(''),TYPE).value('(./text())[1]','VARCHAR(MAX)')
,
1,
2,
'') AS NOTE_TEXT_CONCATINATED
FROM
HNO_NOTE_TEXT s
GROUP BY NOTE_ID

SQL: iterating through a list that contains some ranges

I am trying to get information for products that have an ID that is contained in a list. The problem is that the list contains some single values and some range values:
PX03 - PX069, PX20, PX202, PX25 - PX270, PX250 - PX2509, PX251, PX2511 -
PX2513
Basically what I am looking for is some way to take a list or string containing both values and ranges and the end output is a table or list that has all of the values within the ranges individually so that I can loop through them.
I have a stored procedure that loops through all the ID's in the main products table that use the 'PX' prefix, but the table has all ids (i.e. PX 1 - 9999, LX 00001 - 99999) and I only want to search through those contained in the above list. I could write it out all the id's individually but some of the ranges contain many values, which would be time consuming to go through.
My idea was to create a separate table containing this list, in which there would be three columns: an identity column, and then one column each for the beginning and end of the range. Any items that do not have a range would just have the same value for beginning and end range, i.e.:
----------------------------------
rownum | range_start | range_end|
----------------------------------
1 PX03 PX069
2 PX20 PX20
3 PX202 PX202
4 PX25 PX25
5 PX250 PX2509
and then populating a table using something like:
SELECT id from product_table
WHERE id BETWEEN listtable.range_start AND listtable.range_end
where product_table is my original table with the product id's and their information and listtable is the new table I just created. This would give me:
id|
---
PX03
PX030
PX031
PX032
PX033
.
.
.
PX067
PX068
PX069
PX20
PX202
PX25
PX250
PX251
etc.
but I am thinking I would need to iterate through the list and I am not sure how to do that. Any ideas, hints or suggestions?
UPDATE
After creating the table using the solution given by #asantaballa, it was as simple as using an inner join:
SELECT d.id
FROM product_table d
INNER JOIN #RangeTable r
ON d.id BETWEEN r.RangeFrom AND r.RangeTo
See if this works for you for the part about converting the string to a table.
Declare #StrList Varchar(1000) = 'PX03 - PX069, PX20, PX202, PX25 - PX270, PX250 - PX2509, PX251, PX2511 - PX2513'
Declare #RangeTable Table (RangeFrom VarChar(32), RangeTo VarChar(32))
Select #StrList = Replace(#StrList,' ', '') + ','
Declare #StrListItem Varchar(32)
While CHARINDEX(',', #StrList) > 0
Begin
Select #StrListItem = SUBSTRING(#StrList,1,CHARINDEX(',', #StrList) - 1)
Declare
#RangeFrom VarChar(32)
, #RangeTo VarChar(32)
If CHARINDEX('-', #StrListItem) = 0
Begin
Select
#RangeFrom = #StrListItem
, #RangeTo = #StrListItem
End
Else
Begin
Select
#RangeFrom = SUBSTRING(#StrListItem, 1, CHARINDEX('-', #StrListItem) - 1)
, #RangeTo = SUBSTRING(#StrListItem, CHARINDEX('-', #StrListItem) + 1, LEN(#StrListItem) - CHARINDEX('-', #StrListItem))
End
Insert Into #RangeTable (RangeFrom, RangeTo) Values (#RangeFrom, #RangeTo)
Select #StrList = SUBSTRING(#StrList, CHARINDEX(',', #StrList) + 1, LEN(#StrList) - CHARINDEX(',', #StrList))
End
Select * From #RangeTable
Here is your string and product_table
DECLARE #STR VARCHAR(100) = 'PX03 - PX069, PX20, PX202, PX25 - PX270, PX250 - PX2509, PX251, PX2511 - PX2513'
SELECT * INTO #product_table
FROM
(
SELECT 'PX4' PRODID
UNION ALL
SELECT 'PX26'
UNION ALL
SELECT 'PX75'
UNION ALL
SELECT 'PX77'
)TAB
Now create a table to hold the value
CREATE TABLE #listtable(ROWNUM int IDENTITY(1,1),range_start VARCHAR(100),range_end VARCHAR(100))
Now insert the splitted value to the table.
INSERT INTO #listtable
SELECT
ISNULL(PARSENAME(REPLACE(Split.a.value('.', 'VARCHAR(100)'),'-','.'),2),Split.a.value('.', 'VARCHAR(100)')) 'range_start' ,
PARSENAME(REPLACE(Split.a.value('.', 'VARCHAR(100)'),'-','.'),1) 'range_end'
FROM
(
SELECT CAST ('<M>' + REPLACE(#STR, ',', '</M><M>') + '</M>' AS XML) AS Data
) AS A
CROSS APPLY Data.nodes ('/M') AS Split(a)
Since Id is string, you need a function to extract numbers from Id(function created by God of SQL Server - Pinal Dave)
CREATE FUNCTION dbo.udf_GetNumeric
(#strAlphaNumeric VARCHAR(256))
RETURNS VARCHAR(256)
AS
BEGIN
DECLARE #intAlpha INT
SET #intAlpha = PATINDEX('%[^0-9]%', #strAlphaNumeric)
BEGIN
WHILE #intAlpha > 0
BEGIN
SET #strAlphaNumeric = STUFF(#strAlphaNumeric, #intAlpha, 1, '' )
SET #intAlpha = PATINDEX('%[^0-9]%', #strAlphaNumeric )
END
END
RETURN ISNULL(#strAlphaNumeric,0)
END
First of all keep in mind that we will not get PX1,PX2,PX3,PX4 if you give id BETWEEN listtable.range_start AND listtable.range_end because those are of varchar type and not numbers. So we need to extract numbers from each PX and get the values between them and append PX.
Here is the query which filters the IDs in product_table which are in the range between listtable
;WITH CTE AS
(
SELECT ROWNUM,CAST(dbo.udf_GetNumeric(range_start)AS INT) NUMBERS,
CAST(dbo.udf_GetNumeric(range_end)AS INT) RTO1
FROM #listtable
UNION ALL
SELECT T.ROWNUM,NUMBERS+1,RTO1
FROM #listtable T
JOIN CTE ON CTE.ROWNUM = T.ROWNUM
WHERE NUMBERS < RTO1
)
SELECT PRODID IDS--,ROWNUM,NUMBERS NUMS,'PX'+CAST(NUMBERS AS VARCHAR(10)) IDS2
FROM CTE
JOIN #product_table ON PRODID='PX'+CAST(NUMBERS AS VARCHAR(10))
ORDER BY NUMBERS
option (MaxRecursion 0)
SQL FIDDLE

Resources