Convert columns to xml collection with attributes - sql-server

I would like to have a row for each row in the table, but transform the columns to an xml collection as efficiently as possible. In the example below it is a flattened table - but in the real world the columns would require many joins to get - resulting in many reads.
For example:
declare #tbl table (
Id int identity (1, 1) primary key
,PolicyNumber varchar(100) not null
,InsuredName varchar(100) not null
,EffectiveDate datetime2 not null
,Premium numeric(22, 7)
)
insert into #tbl (PolicyNumber, InsuredName, EffectiveDate, Premium)
values ('2017A-ALKJ02', 'Insured Number 1', '2017-01-01', 1000)
,('2017A-BSDSDFWEF2', 'Insured Number 2', '2017-06-01', 2000)
select Id
,(select [#name] = 'PolicyNumber', [#type] = 'string', [text()] = PolicyNumber from #tbl [inner] where [inner].Id = [outer].Id for xml path ('dt'))
,(select [#name] = 'InsuredName', [#type] = 'string', [text()] = [inner].InsuredName from #tbl [inner] where [inner].Id = [outer].Id for xml path ('dt'))
,(select [#name] = 'EffectiveDate', [#type] = 'datetime', [text()] = [inner].EffectiveDate from #tbl [inner] where [inner].Id = [outer].Id for xml path ('dt'))
,(select [#name] = 'Premium', [#type] = 'numeric', [text()] = [inner].Premium from #tbl [inner] where [inner].Id = [outer].Id for xml path ('dt'))
from #tbl [outer]
Yields the individual columns in their own xml element, but I am after each row to have it's primary key and the structure:
<dts>
<dt name="PolicyNumber" type="string">2017A-ALKJ02</dt>
<dt name="InsuredName" type="string">Insured Number 1</dt>
<dt name="EffectiveDate" type="datetime">2017-01-01T00:00:00</dt>
<dt name="Premium" type="numeric">1000.0000000</dt>
</dts>
I understand this can be achieved with many sub-queries, but does anyone know of an easy way to have a single query that is smart enough to have the PK and all the individual columns transformed into an element in the dts collection?

If you know all metadata (column name and type) in advance this can be done very simply like here:
declare #tbl table (
Id int identity (1, 1) primary key
,PolicyNumber varchar(100) not null
,InsuredName varchar(100) not null
,EffectiveDate datetime2 not null
,Premium numeric(22, 7)
);
insert into #tbl (PolicyNumber, InsuredName, EffectiveDate, Premium)
values ('2017A-ALKJ02', 'Insured Number 1', '2017-01-01', 1000)
,('2017A-BSDSDFWEF2', 'Insured Number 2', '2017-06-01', 2000);
SELECT 'PolicyNumber' AS [dt/#name]
,'string' AS [dt/#type]
,PolicyNumber AS [dt]
,''
,'InsuredName' AS [dt/#name]
,'string' AS [dt/#type]
,InsuredName AS [dt]
,''
,'EffectiveDate' AS [dt/#name]
,'datetime' AS [dt/#type]
,EffectiveDate AS [dt]
,''
,'Premium' AS [dt/#name]
,'numeric' AS [dt/#type]
,Premium AS [dt]
FROM #tbl
FOR XML PATH('dts'),ROOT('root')
The result
<root>
<dts>
<dt name="PolicyNumber" type="string">2017A-ALKJ02</dt>
<dt name="InsuredName" type="string">Insured Number 1</dt>
<dt name="EffectiveDate" type="datetime">2017-01-01T00:00:00</dt>
<dt name="Premium" type="numeric">1000.0000000</dt>
</dts>
<dts>
<dt name="PolicyNumber" type="string">2017A-BSDSDFWEF2</dt>
<dt name="InsuredName" type="string">Insured Number 2</dt>
<dt name="EffectiveDate" type="datetime">2017-06-01T00:00:00</dt>
<dt name="Premium" type="numeric">2000.0000000</dt>
</dts>
</root>
The trick is the nameless empty "column" between the <dt> elements. The engine is told: Look, there's a new element, close the one before and start a new one!
Otherwise you'd get an error...
UPDATE: generic approach
This will extract all meta data and construct the same statement as above, which is executed with EXEC:
CREATE TABLE tmpTbl (
Id int identity (1, 1) primary key
,PolicyNumber varchar(100) not null
,InsuredName varchar(100) not null
,EffectiveDate datetime2 not null
,Premium numeric(22, 7)
);
insert into tmpTbl (PolicyNumber, InsuredName, EffectiveDate, Premium)
values ('2017A-ALKJ02', 'Insured Number 1', '2017-01-01', 1000)
,('2017A-BSDSDFWEF2', 'Insured Number 2', '2017-06-01', 2000);
DECLARE #cmd NVARCHAR(MAX)='SELECT ' +
STUFF(
(
SELECT ',''' + c.COLUMN_NAME + ''' AS [dt/#name]' +
',''' + c.DATA_TYPE + ''' AS [dt/#type]' +
',' + QUOTENAME(c.COLUMN_NAME) + ' AS [dt]' +
','''''
FROM INFORMATION_SCHEMA.COLUMNS AS c WHERE TABLE_NAME='tmpTbl'
FOR XML PATH('')
),1,1,'') +
'FROM tmpTbl FOR XML PATH(''dts''),ROOT(''root'')';
EXEC( #cmd);
GO
--cleanup (careful with real data)
--DROP TABLE tmpTbl;
If you need for example "string" instead of "varchar" you'd need a mapping table or a CASE expression.

This can be solve using different techniques. Here is one of them using UNPIVOT to generate the type column:
WITH DataSource AS
(
SELECT [id]
,[column]
,[value]
,CASE [column]
WHEN 'PolicyNumber' THEN 'string'
WHEN 'InsuredName' THEN 'string'
WHEN 'EffectiveDate' THEN 'datetime'
WHEN 'Premium' THEN 'numeric'
END AS [type]
FROM
(
SELECT Id
,PolicyNumber
,InsuredName
,CAST(EffectiveDate AS VARCHAR(100)) AS EffectiveDate
,CAST(Premium AS VARCHAR(100)) AS Premium
FROM #tbl
) DS
UNPIVOT
(
[value] FOR [column] IN ([PolicyNumber], [InsuredName], [EffectiveDate], [Premium])
) UNPVT
)
SELECT DISTINCT [id]
,[Info]
FROM #tbl DS
CROSS APPLY
(
SELECT [column] "#name"
,[type] "#type"
,CASE WHEN [column] = 'EffectiveDate' THEN CONVERT(VARCHAR(32), CAST([value] AS DATETIME2), 126) ELSE [value] END "text()"
FROM DataSource Info
WHERE DS.[Id] = Info.[Id]
FOR XML PATH('dt'), ROOT('dts')
) DSInfo (Info);
It will give you XML like this for each row:
<dts>
<dt name="PolicyNumber" type="string">2017A-ALKJ02</dt>
<dt name="InsuredName" type="string">Insured Number 1</dt>
<dt name="EffectiveDate" type="datetime">2017-01-01T00:00:00</dt>
<dt name="Premium" type="numeric">1000.0000000</dt>
</dts>

There is no convenient way to produce this kind of output in SQL Server. One possible solution might be a FLWOR transformation, but I suspect it will be quite convoluted, indeed.
The other is by using UNPIVOT, as in the example below, though it is far from being easily expandable:
select (
select upt.ColumnName as [#name],
isnull(dt.ColumnType, 'string') as [#type],
upt.ColumnValue as [text()]
from (
select t.Id, t.PolicyNumber, t.InsuredName,
convert(varchar(100), t.EffectiveDate, 126) as [EffectiveDate],
cast(t.Premium as varchar(100)) as [Premium]
from #tbl t
) sq
unpivot (
ColumnValue for ColumnName in (
sq.PolicyNumber, sq.InsuredName, sq.EffectiveDate, sq.Premium
)
) upt
left join (values
('EffectiveDate', 'datetime'),
('Premium', 'numeric')
) dt (ColumnName, ColumnType) on upt.ColumnName = dt.ColumnName
where upt.Id = t.Id
for xml path('dt'), type
)
from #tbl t
for xml path('dts'), type;
First, you need to transpose column values into rows, so that your relational output will start resembling your required XML. In order to fit all your columns into the same ColumnValue, you have to cast them to the same data type.
Second, you have to provide the data for your type attribute. In the example above, I used an inline table constructor, because there is no way you can get data types from TV columns on the fly. If your actual data resides in a static table, you can try to join it with system metadata objects, such as INFORMATION_SCHEMA.COLUMNS. Although for your required values you will probably need an additional mapping table, as well (in order to substitute varchar with string, for example).
Last, in order to get a single /dts element for every original table row, I join the unpivotted data with the table again. This allows to generate the required nesting of XML elements, because root() clause is unsuitable for this.

Thanks for the suggestions! I decided flattening the query into a temp table is the best approach, then using the generic approach above plus the blank column trick satisfies the need.
if object_id('tempdb..#tmp') is not null
drop table #tmp
create table #tmp (
Id int identity (1, 1) primary key
,PolicyNumber varchar(100) not null
,InsuredName varchar(100) not null
,EffectiveDate datetime2 not null
,Premium numeric(22, 7)
);
insert into #tmp (PolicyNumber, InsuredName, EffectiveDate, Premium)
values ('2017A-ALKJ02', 'Insured Number 1', '2017-01-01', 1000)
,('2017A-BSDSDFWEF2', 'Insured Number 2', '2017-06-01', 2000);
DECLARE #cmd NVARCHAR(MAX)='
select [outer].Id
,convert(xml, (SELECT ' +
STUFF(
(
SELECT ',[dt/#n] = ''' + c.name + '''' +
',[dt/#t] = ''' + case when t.name = 'bit' then 'b'
when t.name in ('date', 'smalldatetime', 'datetime2', 'datetime', 'datetimeoffset') then 'd'
when t.name = 'bigint' then 'g'
when t.name in ('tinyint', 'smallint', 'int', 'time', 'timestamp') then 'i'
when t.name in ('real', 'smallmoney', 'money', 'float', 'decimal', 'numeric') then 'n'
else 's'
end + '''' +
',[dt] = ' + QUOTENAME(c.name) +
','''''
FROM tempdb.sys.columns c
inner join sys.types t on c.system_type_id = t.system_type_id
where object_id = object_id('tempdb..#tmp')
FOR XML PATH('')
),1,1,'') + '
FROM #tmp [inner]
where [inner].Id = [outer].id
for xml path (''dts'')))
from #tmp [outer]'
EXEC( #cmd);

Related

Split Data and transforming them into Columns

I have an Input table as under
Id Data
1 Column1: Value1
2 Column2: Value11
3 Column3: Value111
4 Column1: Value2
5 Column2: Value22
6 Column3: Value222
I am looking for an output as under
Column1 Column2 Column3
Value1 Value11 Value111
Value2 Value22 Value222
How can I achieve so? It could have been done easily by using a WHILE LOOP and by a bit of mathematical logic, but I am looking for a more optimized one if possible by only SELECT queries (no LOOPS).
I have tried also by splitting using (':') as delimiter and then transforming ROWS to COLUMNS (PIVOT) but somewhat could not be able to proceed. (That's my thought, peoples may have more better thoughts).
My shot so far
Declare #t table(Id int identity(1,1),Data varchar(1000))
Insert into #t Values
('Column1: Value1'),('Column2: Value11'),('Column3: Value111')
,('Column1: Value2'),('Column2: Value22'),('Column3: Value222')
Select *
FROM #t
SELECT
F1.id,
F1.Data,
O.splitdata
FROM
(
SELECT *,
cast('<X>'+replace(F.Data,':','</X><X>')+'</X>' as XML) as xmlfilter from #t F
)F1
CROSS APPLY
(
SELECT fdata.D.value('.','varchar(50)') as splitdata
FROM f1.xmlfilter.nodes('X') as fdata(D)) O
This will work if you want a pure SQL solution:
Select [Column1], [Column2], [Column3] From (
Select col, val, id = ROW_NUMBER() over(partition by d.col order by d.id)
From (
Select id
, col = LEFT(Data, CHARINDEX(':', Data)-1)
, val = RIGHT(Data, LEN(DATA) - CHARINDEX(':', Data))
From #t
) as d
) as p
pivot(
MAX(val)
FOR col in([Column1], [Column2], [Column3])
) as piv
But it supposes that data for Row 1 are always before data for Row 2. There is no way to distinguish them using your sample.
If the number of column is not fixed, it has to use Dynamic SQL.
SQL Server may not be the best options for this kind of thing.
With Dynamic SQL, the above query would be like this one:
create table #t(Id int identity(1,1),Data varchar(1000))
Insert into #t Values
('Column1: Value1'),('Column2: Value11'),('Column3: Value111')
,('Column1: Value2'),('Column2: Value22'),('Column3: Value222')
Declare #sql nvarchar(max)
Select #sql = '
Select '+left(c, len(c)-1)+' From (
Select col, val, id = ROW_NUMBER() over(partition by d.col order by d.id)
From (
Select id
, col = LEFT(Data, CHARINDEX('':'', Data)-1)
, val = RIGHT(Data, LEN(DATA) - CHARINDEX('':'', Data))
From #t
) as d
) as p
pivot(
MAX(val)
FOR col in('+left(c, len(c)-1)+')
) as piv
'
From (
Select Distinct '['+LEFT(Data, CHARINDEX(':', Data)-1)+'], '
From #t
FOR XML PATH('')
) as d(c)
EXEC sp_executesql #sql
SQL Fiddle
This should work:
Declare #t table(Id int identity(1,1),Data varchar(1000))
Insert into #t Values
('Column1: Value1'),('Column2: Value11'),('Column3: Value111')
,('Column1: Value2'),('Column2: Value22'),('Column3: Value222');
WITH Splitted AS
(
SELECT *
,CAST('<X>'+REPLACE(F.Data,':','</X><X>')+'</X>' AS XML) AS xmlfilter
FROM #t AS F
)
SELECT p.*
FROM
(
SELECT ROW_NUMBER() OVER(PARTITION BY xmlfilter.value('X[1]','varchar(max)') ORDER BY Id) AS Inx
,xmlfilter.value('X[1]','varchar(max)') AS ColName
,xmlfilter.value('X[2]','varchar(max)') AS ColVal
FROM Splitted
) AS tbl
PIVOT
(
MAX(ColVal) FOR ColName IN(Column1,Column2,Column3)
) AS p

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 Pivot table without aggregate

I have a number of text files that are in a format similar to what is shown below.
ENTRY,1,000000,Widget 4000,1,,,2,,
FIELD,Type,A
FIELD,Component,Widget 4000
FIELD,Vendor,Acme
ENTRY,2,000000,PRODUCT XYZ,1,,,3,
FIELD,Type,B
FIELD,ItemAssembly,ABCD
FIELD,Component,Product XYZ - 123
FIELD,Description1,Product
FIELD,Description2,XYZ-123
FIELD,Description3,Alternate Part #440
FIELD,Vendor,Contoso
They have been imported into a table with VARCHAR(MAX) as the only field. Each ENTRY is a "new" item, and all the subsequent FIELD rows are properties of that item. The data next to the FIELD is the column name of the property. The data to the right of the property is the data I want to display.
The desired output would be:
ENTRY Type Component Vendor ItemAssembly Description1
1,000000,Widget 4000 A Widget 4000 Acme
2,000000,Product XYZ B Product XYZ-123 Contoso ABCD Product
I've got the column names using the code below (there are several tables that I have UNIONed together to list all the property names).
select #cols =
STUFF (
(select Distinct ', ' + QUOTENAME(ColName) from
(SELECT
SUBSTRING(ltrim(textFileData),CHARINDEX(',', textFileData, 1)+1,CHARINDEX(',', textFileData, CHARINDEX(',', textFileData, 1)+1)- CHARINDEX(',', textFileData, 1)-1) as ColName
FROM [MyDatabase].[dbo].[MyTextFile]
where
(LEFT(textFileData,7) LIKE #c)
UNION
....
) A
FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)'),1,1,'')
Is a Pivot table the best way to do this? No aggregation is needed. Is there a better way to accomplish this? I want to list out data next to the FIELD name in a column format.
Thanks!
Here is the solution in SQL fiddle:
http://sqlfiddle.com/#!3/8f0b0/8
Prepare raw data in format (entry, field, value), use dynamic SQL to make pivot on unknown column count.
MAX() for string is enough to simulate "without aggregate" behavior in this case.
create table t(data varchar(max))
insert into t values('ENTRY,1,000000,Widget 4000,1,,,2,,')
insert into t values('FIELD,Type,A')
insert into t values('FIELD,Component,Widget 4000')
insert into t values('FIELD,Vendor,Acme ')
insert into t values('ENTRY,2,000000,PRODUCT XYZ,1,,,3,')
insert into t values('FIELD,Type,B')
insert into t values('FIELD,ItemAssembly,ABCD')
insert into t values('FIELD,Component,Product XYZ - 123')
insert into t values('FIELD,Description1,Product ')
insert into t values('FIELD,Description2,XYZ-123 ')
insert into t values('FIELD,Description3,Alternate Part #440')
insert into t values('FIELD,Vendor,Contoso');
create type preparedtype as table (entry varchar(max), field varchar(max), value varchar(max))
declare #prepared preparedtype
;with identified as
(
select
row_number ( ) over (order by (select 1)) as id,
substring(data, 1, charindex(',', data) - 1) as type,
substring(data, charindex(',', data) + 1, len(data)) as data
from t
)
, tree as
(
select
id,
(select max(id)
from identified
where type = 'ENTRY'
and id <= i.id) as parentid,
type,
data
from identified as i
)
, pivotsrc as
(
select
p.data as entry,
substring(c.data, 1, charindex(',', c.data) - 1) as field,
substring(c.data, charindex(',', c.data) + 1, len(c.data)) as value
from tree as p
inner join tree as c on c.parentid = p.id
where p.id = p.parentid
and c.parentid <> c.id
)
insert into #prepared
select * from pivotsrc
declare #dynamicPivotQuery as nvarchar(max)
declare #columnName as nvarchar(max)
select #columnName = ISNULL(#ColumnName + ',','')
+ QUOTENAME(field)
from (select distinct field from #prepared) AS fields
set #dynamicPivotQuery = N'select * from #prepared
pivot (max(value) for field in (' + #columnName + ')) as result'
exec sp_executesql #DynamicPivotQuery, N'#prepared preparedtype readonly', #prepared
Here your are, this comes back exactly as you need it. I love tricky SQL :-). This is a real ad-hoc singel-statement call.
DECLARE #tbl TABLE(OneCol VARCHAR(MAX));
INSERT INTO #tbl
VALUES('ENTRY,1,000000,Widget 4000,1,,,2,,')
,('FIELD,Type,A')
,('FIELD,Component,Widget 4000')
,('FIELD,Vendor,Acme ')
,('ENTRY,2,000000,PRODUCT XYZ,1,,,3,')
,('FIELD,Type,B')
,('FIELD,ItemAssembly,ABCD')
,('FIELD,Component,Product XYZ - 123')
,('FIELD,Description1,Product ')
,('FIELD,Description2,XYZ-123 ')
,('FIELD,Description3,Alternate Part #440')
,('FIELD,Vendor,Contoso');
WITH OneColumn AS
(
SELECT ROW_NUMBER() OVER(ORDER BY (SELECT 1)) AS inx
,CAST('<root><r>' + REPLACE(OneCol,',','</r><r>') + '</r></root>' AS XML) AS Split
FROM #tbl AS tbl
)
,AsParts AS
(
SELECT inx
,Each.part.value('/root[1]/r[1]','varchar(max)') AS Part1
,Each.part.value('/root[1]/r[2]','varchar(max)') AS Part2
,Each.part.value('/root[1]/r[3]','varchar(max)') AS Part3
,Each.part.value('/root[1]/r[4]','varchar(max)') AS Part4
,Each.part.value('/root[1]/r[5]','varchar(max)') AS Part5
FROM OneColumn
CROSS APPLY Split.nodes('/root') AS Each(part)
)
,TheEntries AS
(
SELECT DISTINCT *
FROM AsParts
WHERE Part1='ENTRY'
)
SELECT TheEntries.Part2 + ',' + TheEntries.Part3 + ',' + TheEntries.Part4 AS [ENTRY]
,MyFields.AsXML.value('(fields[1]/field[Part2="Type"])[1]/Part3[1]','varchar(max)') AS [Type]
,MyFields.AsXML.value('(fields[1]/field[Part2="Component"])[1]/Part3[1]','varchar(max)') AS Component
,MyFields.AsXML.value('(fields[1]/field[Part2="Vendor"])[1]/Part3[1]','varchar(max)') AS Vendor
,MyFields.AsXML.value('(fields[1]/field[Part2="ItemAssembly"])[1]/Part3[1]','varchar(max)') AS ItemAssembly
,MyFields.AsXML.value('(fields[1]/field[Part2="Description1"])[1]/Part3[1]','varchar(max)') AS Description1
FROM TheEntries
CROSS APPLY
(
SELECT *
FROM AsParts AS ap
WHERE ap.Part1='FIELD' AND ap.inx>TheEntries.inx
AND ap.inx < ISNULL((SELECT TOP 1 nextEntry.inx FROM TheEntries AS nextEntry WHERE nextEntry.inx>TheEntries.inx ORDER BY nextEntry.inx DESC),10000000)
ORDER BY ap.inx
FOR XML PATH('field'), ROOT('fields'),TYPE
) AS MyFields(AsXML)

Inserting a dynamically built 'for xml' statement into a table or variable

I've got a situation where I'm trying to get a list of unfilled fields from a temp table into a comma separated statement.
So given the example data (which will always be a single row, and probably in a temp table (as the actual data will come from a multitude of source tables)):
Field1 Field2 Field3 Field4
'aaa' null '' null
And the mapping table of
FieldName Question Section
'Field1' 'Q1' 'Sec1'
'Field2' 'Q2' 'Sec1'
'Field3' 'Q3' 'Sec2'
'Field4' 'Q4' 'Sec2'
I would like the following result:
Section UnansweredQs
'Sec1' 'Q2'
'Sec2' 'Q3, Q4'
I've got as far as the comma separated list of questions by doing:
create table #testData (f1 varchar(50), f2 int, f3 varchar(50), f4 varchar(50))
create table #qlist (fieldName varchar(5), question varchar(3), section varchar(5))
insert into #qlist values ('f1', 'q1', 'sec1'), ('f2', 'q2', 'sec1'), ('f3', 'q3', 'sec2'), ('f4', 'q4', 'sec2')
insert into #testData values ('asda', null, '', null)
Then
declare #usql nvarchar(max) = ''
declare #sql nvarchar(max)
declare #xml xml
--build a gargantuan set of union statements, comparing the column value to null/'' and putting q# if it is
set #usql =
(
select 'select case when ' + c.name + ' is null or ' + c.Name + ' = '''' then ''' + q.question + ', '' else '''' end from #testData union '
from tempdb..syscolumns c
inner join #qlist q
on c.name = q.fieldName
where c.id = object_id('tempdb..#testData')
for xml path('')
);
--remove the last 'union', append for xml path to pivot the rows into a single column of concatenated rows
set #usql = left(#usql, len(#usql) - 6) + ' for xml path('''')'
print #usql
--remove final comma
--get the position of the last comma in the select statment (ie after the final unanswered question)
declare #lastComma int = charindex(',', reverse(#usql))
--add the bit before the last comma, and the bit after the last comma but skip the actual comma :)
set #usql = left(#usql, len(#usql) - #lastComma) + right(#usql, #lastComma - 2)
exec (#usql)
With this I get
XML_F52E2B61-18A1-11d1-B105-00805F49916B
----------------------------------------
q2, q3, q4
But I can't get that result set into another table or variable (via insert into #tmpresult exec (#usql) approach).
Usually with the Msg 1086, Level 15, State 1, Line 1
The FOR XML clause is invalid in views, inline functions, derived tables, and subqueries when they contain a set operator. To work around, wrap the SELECT containing a set operator using derived table syntax and apply FOR XML on top of it. error.
I've tried various things, wrapping, removing the unions, CTE's but can't get it to work.
I have a query for you:
with cte as (
select
N.Name
from Table1
cross apply (values
('Field1', Field1),
('Field2', Field2),
('Field3', Field3),
('Field4', Field4)
) as N(Name,Value)
where N.Value is null or N.Value = ''
)
select distinct
T2.Section,
stuff(
(
select ', ' + TT.Question
from Table2 as TT
inner join cte as c on c.Name = TT.FieldName
where TT.Section = T2.Section
for xml path(''), type
).value('.', 'nvarchar(max)')
, 1, 2, '') as UnansweredQs
from Table2 as T2
you can turn it into dynamic by yourself :)
sql fiddle demo
There is no need to use dynamic SQL to do this.
declare #X xml
set #X = (
select *
from #testData
for xml path('root'), elements xsinil, type
)
select section,
(
select ', '+Q2.question
from #qlist as Q2
where Q1.section = Q2.section and
#X.exist('/root/*[local-name() = sql:column("Q2.fieldName")][. = ""]') = 1
for xml path(''), type
).value('substring(text()[1], 2)', 'varchar(max)') as UnansweredQs
from #qlist as Q1
group by Q1.section
SQL Fiddle

t-sql: dynamically filter XML on multiple conditions?

I'm trying to find a way to do a accept/reject on an XML string, by joining it to a table of conditions. I have one "filter" working now, but want to write it so that it can filter 2 or more.
Here's code that matches one of the two. If either matches, it will filter the string.
What I want to do is make it so it has to match BOTH, while still leaving the option for single-condition
CREATE TABLE #filter (exclusion_type CHAR(1), excluded_value varchar(10))
INSERT INTO #filter VALUES ('B','boy')
INSERT INTO #filter VALUES ('C','cat')
DECLARE #data XML
SELECT #data = '<A><B>boy</B><C>cat</C></A>'
SELECT * FROM (SELECT CONVERT(VARCHAR(128),node.query('fn:local-name(.)')) AS NodeName, CONVERT(VARCHAR(MAX),node.query('./text()')) AS NodeValue
FROM #data.nodes(N'//*') T(node))xml_shred
IF NOT EXISTS
(SELECT * FROM (SELECT CONVERT(VARCHAR(128),node.query('fn:local-name(.)')) AS NodeName, CONVERT(VARCHAR(MAX),node.query('./text()')) AS NodeValue
FROM #data.nodes(N'//*') T(node)) xml_shred
INNER JOIN #filter
ON (nodename = exclusion_type AND nodevalue LIKE excluded_value)
)
select 'record would be inserted '
ELSE select 'record was filtered'
Here's how I currently have it to filter both. Ugly and non-expandable.
IF NOT EXISTS
(SELECT * FROM (SELECT CONVERT(VARCHAR(128),node.query('fn:local-name(.)')) AS NodeName, CONVERT(VARCHAR(MAX),node.query('./text()')) AS NodeValue
FROM #data.nodes(N'//*') T(node)) xml_shred
INNER JOIN #filter
ON (nodename = exclusion_type AND nodevalue LIKE excluded_value)
)
--combination filters don't easily work within that xml_shred
and not(
#data.value('(/A/B)[1]', 'varchar(128)') = 'boy'
AND
#data.value('(/A/C)[1]', 'varchar(128)')='cat'
)
select 'record would be inserted '
ELSE select 'record was filtered'
My only other ideas:
some sort of GUID that would link records in the #filter table together, and then inner join on a GROUP BY of #filtertable, grouping by the GUID and using the SUM to match the number of records.
use semicolons to split the #filter rows, then use a CTE or something to fake a hierarchy and work from there.
Code changes made by Mikael's suggestion
CREATE TABLE #filter
(
exclusion_set SMALLINT,
exclusion_type CHAR(1) ,
excluded_value VARCHAR(10)
)
INSERT INTO #filter
VALUES (1, 'B', 'boy')
INSERT INTO #filter
VALUES (1, 'C', 'cat')
INSERT INTO #filter
VALUES (2, 'D', 'dog' )
DECLARE #data XML
SELECT #data = '<A><B>boy</B><C>cat</C></A>'
IF NOT EXISTS(
SELECT * FROM
(
select COUNT(*) AS match_count, exclusion_set
from #filter as F
where exists (
select *
from (
select X.N.value('local-name(.)', 'varchar(128)') as NodeName,
X.N.value('./text()[1]', 'varchar(max)') as NodeValue
from #data.nodes('//*') as X(N)
) T
where T.NodeName = F.exclusion_type and
T.NodeValue like F.excluded_value
)
GROUP BY exclusion_set
) matches_per_set
INNER JOIN
(SELECT COUNT(*) AS total_count, exclusion_set FROM #filter GROUP BY exclusion_set) grouped_set
ON match_count = total_count
AND grouped_set.exclusion_set = matches_per_set.exclusion_set
)
if not exists (
select *
from #filter as F
where exists (
select *
from (
select X.N.value('local-name(.)', 'varchar(128)') as NodeName,
X.N.value('./text()[1]', 'varchar(max)') as NodeValue
from #data.nodes('//*') as X(N)
) T
where T.NodeName = F.exclusion_type and
T.NodeValue like F.excluded_value
)
having count(*) = (select count(*) from #filter)
)
select 'record would be inserted '
else
select 'record was filtered'
Since I apparently get dinged if I don't mark something as the answer, I'm including mine from above. Many thanks for the help to Mikael Eriksson. His XML shred is faster than mine, and by adding the "exclusion_set" field (char(2) to make it obvious that it wasn't an IDENTITY or primary key), I can do multiple checks. If all conditions in a set match, then the record is filtered.
CREATE TABLE #filter
(
exclusion_set CHAR(2),
exclusion_type CHAR(1) ,
excluded_value VARCHAR(10)
)
INSERT INTO #filter
VALUES ('aa', 'B', 'boy')
INSERT INTO #filter
VALUES ('aa', 'C', 'cat')
INSERT INTO #filter
VALUES ('ab', 'D', 'dog' )
DECLARE #data XML
SELECT #data = '<A><B>boy</B><C>cat</C></A>'
IF NOT EXISTS(
SELECT * FROM
(
select COUNT(*) AS match_count, exclusion_set
from #filter as F
where exists (
select *
from (
select X.N.value('local-name(.)', 'varchar(128)') as NodeName,
X.N.value('./text()[1]', 'varchar(max)') as NodeValue
from #data.nodes('//*') as X(N)
) T
where T.NodeName = F.exclusion_type and
T.NodeValue like F.excluded_value
)
GROUP BY exclusion_set
) matches_per_set
INNER JOIN
(SELECT COUNT(*) AS total_count, exclusion_set FROM #filter GROUP BY exclusion_set) grouped_set
ON match_count = total_count
AND grouped_set.exclusion_set = matches_per_set.exclusion_set
)
select 'record would be inserted '
else
select 'record was filtered'

Resources