SQL Server : select merge all values with same keys - sql-server

I have a table in SQL Server that looks like this:
id: int
key: nvarchar(max)
value: nvarchar(max)
I want to select distinct keys as first column and all values with same key joined with '<br/>' as my second column. The key values are dynamic and is not predefined - and the performance really matters - I don't want to use Linq or any UDF!
id key value
---------------------------------------
1 color red<br/>white<br/>black
4 size 15"
PS: I have searched a lot sorry if it's duplicated, currently running on SQL Server 2014 but I can move to 2019

Please try the following solution. It will work starting from SQL Server 2008 onwards.
SQL
-- DDL and sample data population, start
DECLARE #tbl TABLE (ID INT IDENTITY PRIMARY KEY, [key] NVARCHAR(MAX), [value] NVARCHAR(MAX));
INSERT INTO #tbl ([key], [value]) VALUES
(N'color', N'red'),
(N'color', N'white'),
(N'color', N'black'),
(N'size', N'15"');
-- DDL and sample data population, end
DECLARE #separator CHAR(5) = '<br/>'
, #encoded VARCHAR(20) = '<br/>';
SELECT c.[key]
, STUFF(REPLACE(
(SELECT #separator + CAST([value] AS NVARCHAR(MAX)) AS [text()]
FROM #tbl AS O
WHERE O.[key] = C.[key]
FOR XML PATH('')
), #encoded, #separator)
, 1, LEN(#separator), NULL) AS valueList
FROM #tbl AS c
GROUP BY c.[key];
Output
+-------+-------------------------+
| key | valueList |
+-------+-------------------------+
| color | red<br/>white<br/>black |
| size | 15" |
+-------+-------------------------+

Related

SQL Server extract data from XML column without tag names

I have an XML string:
<XML>
<xml_line>
<col1>1</col1>
<col2>foo 1</col2>
</xml_line>
<xml_line>
<col1>2</col1>
<col2>foo 2</col2>
</xml_line>
</XML>
I am extracting data from that string (stored in #data_xml) by storing it in SQL Server table and parsing it:
-- create temp table, insert XML string
CREATE TABLE table1 (data_xml XML)
INSERT table1
SELECT #data_xml
-- parse XML string into temp table
SELECT
N.C.value('col1[1]', 'int') col1_name,
N.C.value('col2[1]', 'varchar(31)') col2_name,
FROM
table1
CROSS APPLY
data_xml.nodes('//xml_line') N(C)
I would like to know if there is a generic way to accomplish the same without specifying column names (i.e. col1[1], col2[1])
You can use something like:
SELECT
N.C.value('let $i := . return count(//xml_line[. << $i]) + 1', 'int') as LineNumber,
Item.Node.value('local-name(.)', 'varchar(max)') name,
Item.Node.value('.', 'varchar(max)') value
FROM
table1
CROSS APPLY
data_xml.nodes('//xml_line') N(C)
CROSS APPLY
N.C.nodes('*') Item(Node)
To get:
LineNumber
name
value
1
col1
1
1
col2
foo 1
2
col1
2
2
col2
foo 2
See this db<>fiddle.
However, to spread columns horizontally, you will need to generate dynamic SQL after querying for distinct element names.
ADDENDUM: Here is an updated db<>fiddle that also shows a dynamic SQL example.
The above maps all values as VARCHAR(MAX). If you have NVARCHAR data you can make the appropriate changes. If you have a need to map specific columns to specific types, you will need to explicitly define and populate a name-to-type mapping table and incorporate that into the dynamic SQL logic. The same may be necessary if you prefer that the result columns be in a specific order.
ADDENDUM 2: This updated db<>fiddle now includes column type and ordering logic.
--------------------------------------------------
-- Extract column names
--------------------------------------------------
DECLARE #Names TABLE (name VARCHAR(100))
INSERT #Names
SELECT DISTINCT Item.Node.value('local-name(.)', 'varchar(max)')
FROM table1
CROSS APPLY data_xml.nodes('//xml_line/*') Item(Node)
--SELECT * FROM #Names
--------------------------------------------------
-- Define column-to-type mapping
--------------------------------------------------
DECLARE #ColumnTypeMap TABLE ( ColumnName SYSNAME, ColumnType SYSNAME, ColumnOrder INT)
INSERT #ColumnTypeMap
VALUES
('col1', 'int', 1),
('col2', 'varchar(10)', 2)
DECLARE #ColumnTypeDefault SYSNAME = 'varchar(max)'
--------------------------------------------------
-- Define SQL Templates
--------------------------------------------------
DECLARE #SelectItemTemplate VARCHAR(MAX) =
' , N.C.value(<colpath>, <coltype>) <colname>
'
DECLARE #SqlTemplate VARCHAR(MAX) =
'SELECT
N.C.value(''let $i := . return count(//xml_line[. << $i]) + 1'', ''int'') as LineNumber
<SelectItems>
FROM
table1
CROSS APPLY
data_xml.nodes(''//xml_line'') N(C)
'
--------------------------------------------------
-- Expand SQL templates into SQL
--------------------------------------------------
DECLARE #SelectItems VARCHAR(MAX) = (
SELECT STRING_AGG(SI.SelectItem, '')
WITHIN GROUP(ORDER BY ISNULL(T.ColumnOrder, 999), N.Name)
FROM #Names N
LEFT JOIN #ColumnTypeMap T ON T.ColumnName = N.name
CROSS APPLY (
SELECT SelectItem = REPLACE(REPLACE(REPLACE(
#SelectItemTemplate
, '<colpath>', QUOTENAME(N.name + '[1]', ''''))
, '<colname>', QUOTENAME(N.name))
, '<coltype>', QUOTENAME(ISNULL(T.ColumnType, #ColumnTypeDefault), ''''))
) SI(SelectItem)
)
DECLARE #Sql VARCHAR(MAX) = REPLACE(#SqlTemplate, '<SelectItems>', #SelectItems)
--------------------------------------------------
-- Execute
--------------------------------------------------
SELECT DynamicSql = #Sql
EXEC (#Sql)
Result (with some additional data):
LineNumber
col1
col2
bar
foo
1
1
foo 1
null
More
2
2
foo 2
Stuff
null

Using quotes in a variable to construct a string in SQL [duplicate]

This question already has answers here:
How do I escape a single quote in SQL Server?
(14 answers)
Closed 1 year ago.
I have a string pulling from XML. It is pulling a single value out of a record. the only part that changes when calling the item is the field name.
for example, the first below pulls the 'resolution' for the item,
the second below pulls the 'name' of the item:
XMLData.value('(ImportFormXml/Resolution)[1]','VARCHAR(50)') AS Resolution
XMLData.value('(ImportFormXml/Name)[1]','VARCHAR(50)') AS Name
I would like to declare a variable and use it as one of the two ways below.
WAY 1 (Preferred)
DECLARE
#Var1 Varchar(50)
SET #Var1 = 'XMLData.value('(ImportFormXml/' [BE ABLE TO INSERT NAME HERE...THIS CAN'T BE ANOTHER VARIABLE]')[1]','VARCHAR(50)')
SELECT
#Var1 INSERT 'Resolution' AS Resolution
, #Var2 INSERT 'Name' AS Name
From TableX
WAY 2
DECLARE
#Var1 Varchar(50)
#Var2 Varchar(50)
SET #Var1 = 'XMLData.value('(ImportFormXml/'
SET #Var2 = ')[1]','VARCHAR(50)')
SELECT
#Var1 + 'Resolution' + #Var2 AS Resolution
, #Var1 + 'Name' + #Var2 AS Name
From TableX
A minimal reproducible example is not provided.
So I am shooting of the hip.
SQL
-- DDL and sample data population, start
DECLARE #tbl TABLE (ID INT IDENTITY PRIMARY KEY, XMLColumn XML);
INSERT INTO #tbl (XMLColumn) VALUES
(N'<ImportFormXml>
<Resolution>Some kind of resolution</Resolution>
<Name>Just a name</Name>
</ImportFormXml>');
-- DDL and sample data population, end
SELECT ID
, c.value('local-name(*[1])','VARCHAR(50)') + ': ' +
c.value('(Resolution/text())[1]','VARCHAR(50)') AS Col1
, c.value('local-name(*[2])','VARCHAR(50)') + ': ' +
c.value('(Name/text())[1]','VARCHAR(50)') AS Col2
FROM #tbl
CROSS APPLY XMLColumn.nodes('/ImportFormXml') AS t(c);
Output
+----+-------------------------------------+-------------------+
| ID | Col1 | Col2 |
+----+-------------------------------------+-------------------+
| 1 | Resolution: Some kind of resolution | Name: Just a name |
+----+-------------------------------------+-------------------+

Multiple XML tag value into single column with comma separator

I have an XML where the XML have multiple similar tag and I want this value need to show in one column with comma separator and insert into table.
For example:
<test xmlns="http://www.google.com">
<code>a</code>
<code>b</code>
<code>c</code>
</test>
Since XML is too large and I am using OPENXML to perform operation and insert that value into particular table.
I am performing like
insert into table A
(
code
)
select Code from OPENXML(sometag)
with (
code varchar(100) 'tagvalue'
)
for XQUERY I am using something like this: 'for $i in x:Code return concat($i/text()[1], ";")' and I want same with OPENXML.
Output: I want code tag value into one column like a,b,c or a/b/c.
Since you're on SQL Server 2017 you could use STRING_AGG (Transact-SQL) to concatenate your code values, e.g.:
create table dbo.Test (
someTag xml
);
insert dbo.Test (someTag) values
('<test><code>a</code><code>b</code><code>c</code></test>'),
('<test><code>d</code><code>e</code><code>f</code></test>');
select [Code], [someTag]
from dbo.Test
outer apply (
select [Code] = string_agg([value], N',')
from (
select n1.c1.value('.', 'nvarchar(100)')
from someTag.nodes(N'/test/code') n1(c1)
) src (value)
) a1;
Which yields...
Code someTag
a,b,c <test><code>a</code><code>b</code><code>c</code></test>
d,e,f <test><code>d</code><code>e</code><code>f</code></test>
Just a small tweak to AlwaysLearning (+1)
Example
Declare #YourTable table (ID int,XMLData xml)
insert Into #YourTable values
(1,'<test><code>a</code><code>b</code><code>c</code></test>')
Select A.ID
,B.*
From #YourTable A
Cross Apply (
Select DelimString = string_agg(xAttr.value('.','varchar(max)'),',')
From A.XMLData.nodes('/test/*') xNode(xAttr)
) B
Returns
ID DelimString
1 a,b,c
And just for completeness, here is method #3 via pure XQuery and FLWOR expression.
SQL
-- DDL and sample data population, start
DECLARE #tbl TABLE (ID INT IDENTITY PRIMARY KEY, xmldata xml);
INSERT #tbl (xmldata) VALUES
('<test xmlns="http://www.google.com"><code>a</code><code>b</code><code>c</code></test>'),
('<test xmlns="http://www.google.com"><code>d</code><code>e</code><code>f</code></test>');
-- DDL and sample data population, end
DECLARE #separator CHAR(1) = ',';
-- Method #3
-- SQL Server 2005 onwards
;WITH XMLNAMESPACES (DEFAULT 'http://www.google.com')
SELECT ID
, xmldata.query('for $i in /test/code
return if ($i is (/test/code[last()])[1]) then string($i)
else concat($i, sql:variable("#separator"))')
.value('.', 'NVARCHAR(MAX)') AS [Comma_separated_list]
FROM #tbl;
Output
+----+----------------------+
| ID | Comma_separated_list |
+----+----------------------+
| 1 | a, b, c |
| 2 | d, e, f |
+----+----------------------+

Pivot and concatenate values from column in SQL Server

I have table with these columns:
ID | Name | Value
------------------
1 | Test1 | 0
2 | Test2 | 1
3 | Test3 | 0
4 | Test4 | 0
5 | Test5 | 1
And I want to have pivoted and concatenated value column as string
01001
The below code will give the expected result:
SELECT #Result = #Result + CAST(VALUE AS VARCHAR)
FROM #TmpTestingTable
Or you can use the STUFF:
SELECT STUFF(
( SELECT CAST(VALUE AS VARCHAR)
FROM #TmpTestingTable
FOR XML PATH ('')
), 1, 0, '')
For sample, I inserted the columns into the temporary table and execute the code.
CREATE TABLE #TmpTestingTable (ID INT, Name VARCHAR (20), Value INT)
INSERT INTO #TmpTestingTable (ID, Name, Value) VALUES
(1 , 'Test1' , 0),
(2 , 'Test2' , 1),
(3 , 'Test3' , 0),
(4 , 'Test4' , 0),
(5 , 'Test5' , 1)
DECLARE #Result AS VARCHAR (100) = '';
-- using variable approach
SELECT #Result = #Result + CAST(VALUE AS VARCHAR)
FROM #TmpTestingTable
SELECT #Result
-- using STUFF approach
SELECT STUFF(
( SELECT CAST(VALUE AS VARCHAR)
FROM #TmpTestingTable
FOR XML PATH ('')
), 1, 0, '')
DROP TABLE #TmpTestingTable
Use FOR XML to concatinate. It is important that you also include an ORDER BY. Otherwise you have no control of the order of the values and you risk an arbitrary order.
SELECT
(SELECT CAST([VALUE] AS CHAR(1))
FROM yourtable
ORDER BY ID
FOR XML PATH ('')
)
SELECT GROUP_CONCAT(Value SEPARATOR '') FROM Table
EDIT:
Not working on SQL Server. Have a look at Simulating group_concat MySQL function in Microsoft SQL Server 2005? to try to make it work

SQL Server CSV per row

I have Data Like:
StudentID | Course
1 | .NET
1 | SQL Server
1 | Ajax
2 | Java
2 | JSP
2 | Struts
I want the query to get the Output data Like the following.
StudentID | Course
1 | .NET, SQL Server, Ajax
2 | Java, JSP, Struts
In SQL Server 2005+, the easiest method is the to use the FOR XML trick:
SELECT StudentID, STUFF((SELECT ',' + Course
FROM table t1
WHERE t1.StudentID = t.StudentID
FOR XML PATH('')), 1, 1, '')
FROM table t
In SQLServer2000+ you can use following
create table tbl (StudentID int, course varchar(10))
insert into tbl values (1,'.NET'),(1, 'SQL Server'), (1, 'Ajax'),(2,'Java'),(2,'JSP'),(2,'Struts')
GO
CREATE FUNCTION dbo.GetCourses(#id INTEGER)
RETURNS VARCHAR(MAX)
AS
BEGIN
DECLARE #Result VARCHAR(MAX)
SET #Result = ''
SELECT #Result = #Result + [course] + ' ' FROM tbl WHERE StudentID = #id
RETURN RTRIM(#Result)
END
GO
SELECT DISTINCT StudentID, dbo.GetCourses(StudentID) FROM tbl
GO
drop table tbl
drop function dbo.GetCourses

Resources