Generate XML in SQL Server - sql-server

I have a table like this -
Version itemid sampleid
--------------------------------
1 3 23
1 3 24
1 4 45
2 5 24
2 5 23
Where for each version there can be multiple itemid, and for each itemid there can be multiple sampleid.
I want to generate XML for this table in the following manner
<UserVersioningHistory>
<History>
<Version>1</Version>
<itemid>3</itemid>
<sampleid>23,24</sampleid>
</History>
<History>
<Version>1</Version>
<itemid>4</itemid>
<sampleid>45</sampleid>
</History>
<History>
<Version>2</Version>
<BusinessId>5</BusinessId>
<sampleid>24,23</sampleid>
</History>
</UserVersioningHistory>
Each node here can have only one version and itemid but can have multiple sampleid for corresponding itemid and Version Pair.
As I am not familiar in with generating XML in SQL Server, can someone give me a hint to what approach I should use?
Can I accomplish this task using while loop, or I should do this writing a subquery?

Try it out:
select * from
(SELECT
version, itemid,
STUFF(
(SELECT ',' + sampleid
FROM test
WHERE version = a.version AND itemid = a.itemid
FOR XML PATH (''))
, 1, 1, '') AS sampleid
FROM test AS a
GROUP BY version, itemid) as History
for xml auto, root ('UserVersioningHistory')

Always avoid WHILE loops unless when truly necessary.
Preferably you would have multiple tags for the SampleId section if more than one exists.
But to give you the result that you want the following would work. (I created a temp table to imitate your situation.)
I used FOR XML to do the XML formatting and had to use a second FOR XML to concatenate the SampleId sepparated by a comma.
STUFF is only used to remove the first comma in the string.
SET XACT_ABORT ON;
BEGIN TRANSACTION;
SELECT
X.Version,
X.ItemId,
X.SampleId
INTO
#Temp
FROM
(VALUES
(1, 3, 23),
(1, 3, 24),
(1, 4, 45),
(2, 5, 24),
(2, 5, 23)
) X ([Version], ItemId, SampleId)
SELECT
T.Version,
T.ItemId,
STUFF((
SELECT
',' + CONVERT(varchar(MAX), T2.SampleId)
FROM
#Temp T2
WHERE
T2.Version = T.Version AND
T2.ItemId = T.ItemId
FOR XML PATH ('')
),
1,
1,
''
) AS [SampleId]
FROM
#Temp T
GROUP BY
T.Version,
T.ItemId
FOR XML RAW ('History'), ROOT ('UserVersioningHistory'), ELEMENTS
ROLLBACK TRANSACTION;

Related

Substitute for STRING_AGG pre SQL Server 2016

I need to group a table by a set of values together with all matching row numbers/id:s for each set. This operation must be done within the boundaries of SQL Server 2016.
Let's suppose I have the following table (Places):
ID
Country
City
1
Sweden
Stockholm
2
Norway
Oslo
3
Iceland
Reykjavik
4
Sweden
Stockholm
The result that I'm after (No curly-brackets because Stack Overflow thinks it's code, preventing me from posting):
ID
Json
1,4
"Country":"Sweden","City":"Stockholm"
2
"Country":"Norway ","City":"Oslo"
3
"Country":"Iceland ","City":"Reykjavik"
In SQL Server 2017 the above result can be achieved with:
SELECT STRING_AGG(ID) ID, (SELECT Country, City FOR JSON PATH) Json
FROM Places GROUP BY Country, City
I managed to get a similar result in SQL Server 2016 with the code below. (But with my actual amount of data and columns, this solution is too slow.)
SELECT DISTINCT Country, City INTO #temp FROM Places
SELECT (SELECT ID From Places WHERE Country = P.Country AND City = P.City FOR JSON PATH) ID,
(SELECT Country, City FOR JSON Path) Json FROM #temp P
Is there any more performance-effective way of achieving the result that I'm after?
EDIT: As people suggested me to try "FOR XML Path" I tried the code below. This gives the following error "Places.ID is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause ":
SELECT stuff((select ',' + cast(ID as varchar(max)) for xml path ('')), 1, 1, '') ID,
(SELECT Country, City FOR JSON PATH) Json
FROM Places GROUP BY Country, City
Here's a solution you can try with for xml path
Basically select and group the json columns needed and using an apply, use the for xml path solution to aggregate the correlated ID values; because the outer query needs to refer to the output of the apply it needs to be aggregated also, I chose to use max
select max(x.Ids), (select country,city for json path) as [Json]
from t
outer apply (
select Stuff((select ',' + Convert(varchar(10),t2.Id)
from t t2
where t2.city=t.city and t2.country=t.country
for xml path(''),type).value('(./text())[1]','varchar(10)'),1,1,'') as Ids
)x
group by country,city
Working Fiddle
Here is another possible solution:
Declare #testTable Table (ID int, Country varchar(30), City varchar(30));
Insert Into #testTable (ID, Country, City)
Values (1, 'Sweden', 'Stockholm')
, (2, 'Normway', 'Oslo')
, (3, 'Iceland', 'Reykjavik')
, (4, 'Sweden', 'Stockholm');
Select Distinct
ID = stuff((Select concat(',', tt2.ID)
From #testTable tt2
Where tt2.City = tt.City
And tt2.Country = tt.Country
For xml path (''), Type).value('.', 'varchar(10)'), 1, 1, '')
, json = (Select Country, City For JSON PATH)
From #testTable tt;
No idea if this will perform any better though. It is essentially the same - just using DISTINCT instead of GROUP BY.

SQL Server array_agg (master - detail)

How can I convert this PostgreSQL code to SQL Server ?
select
countries.title,
(select array_to_json(array_agg(row_to_json(t)))
from postcodes t
where t.country_id = countries.id) as codes
from countries
My initial problem is that I need to select complete master table and with each row all details.
Countries:
id title
1 SLO
2 AUT
PostCodes:
id country_id code title
1 1 1000 Lj
2 1 2000 Mb
3 2 22180 Vi
4 2 22484 De
Desired result:
1 SLO 1000;Lj|2000;MB
2 AUT 22180;Vi|22484;De
Not:
1 SLO 1000 Lj
1 SLO 2000 Mb
2 AUT 22180 Vi
2 AUT 22484 De
The best solution would be using FOR JSON, but unfortunately I need support for 2008 or at least 2012.
With left join all master data are duplicated for detail count, but I do not want to do this. Even worse it would be to select all countries and then call select on post_codes for every country in for loop.
select countries.title,
STUFF((select '|' + t.code + ';' + t.title
from postcodes t
where t.country_id = countries.id
FOR XML PATH('')
),1,1,'') as codes
from countries
-- CAST t.code to VARCHAR if it's Number
try this:
Select Main.COUNTRY_ID,c.title,Left(Main.POSTCODES,Len(Main.POSTCODES)-1) As "POSTCODES"
From
(
Select distinct ST2.COUNTRY_ID,
(
Select ST1.CODE+';'+ST1.TITLE + '|' AS [text()]
From dbo.POSTCODES ST1
Where ST1.COUNTRY_ID = ST2.COUNTRY_ID
ORDER BY ST1.COUNTRY_ID
For XML PATH ('')
) [POSTCODES]
From dbo.POSTCODES ST2
) [Main]
inner join countries c on c.id=main.country_id
Using XML PATH for concatenation can increase the complexity of your code. It's better to implement a CLR aggregation function. Then, you can do the following:
SELECT C.[id]
,C.[title]
,REPLACE([dbo].[Concatenate] (P.[code] + ';' + P.[title]), ',', '|')
FROM #Countries C
INNER JOIN #PostCodes P
ON C.[id] = p.[country_id]
GROUP BY C.[id]
,C.[title];
You can create your own version of the concatenate aggregate - you can specify the delimiter, the order, etc. I can show you examples if you want.
DECLARE #Countries TABLE
(
[id] TINYINT
,[title] VARCHAR(12)
);
INSERT INTO #Countries ([id], [title])
VALUES (1, 'SLO')
,(2, 'AUT');
DECLARE #PostCodes TABLE
(
[id] TINYINT
,[country_id] TINYINT
,[code] VARCHAR(12)
,[title] VARCHAR(12)
);
INSERT INTO #PostCodes ([id], [country_id], [code], [title])
VALUES (1, 1, 1000, 'Lj')
,(2, 1, 2000, 'Mb')
,(3, 2, 22180, 'Vi')
,(4, 2, 22484, 'De');
SELECT C.[id]
,C.[title]
,REPLACE([dbo].[Concatenate] (P.[code] + ';' + P.[title]), ',', '|')
FROM #Countries C
INNER JOIN #PostCodes P
ON C.[id] = p.[country_id]
GROUP BY C.[id]
,C.[title];

Which concept i want use to get following output using SQL server 2012

My Table having following data's
ID | Name
--- | ---------
1 | Apple
2 | Microsoft
3 | Samsung and so on...
In my case, input is '1,2,3'.
And i need output is 'Apple,Microsoft,Samsung'.
SELECT STUFF((
SELECT ','+ Name
FROM MyTable
WHERE ID in (1, 2, 3)
FOR XML PATH('')
), 1, 1, '') AS Names
Result:
Apple,Microsoft,Samsung
You can do with XML PATH
SELECT
(
SELECT
T.Name + ', '
FROM
Tbl T
WHERE
Id in (1, 2, 3)
FOR XML PATH ('')
) DesiredOutput
Result looks like Apple, Microsoft, Samsung,
USE [Database Name]
SELECT COLUMN_NAME,*
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'YourTableName'
As SQL server doesn't support input in runtime, you can either create a stored procedure and pass the input value while executing it. or just run the following query.
SELECT NAME
FROM MyTable
WHERE ID IN
(SELECT TOP 3 ID FROM MyTable ORDER BY ID)

CTE Recursion to get tree hierarchy

I need to get an ordered hierarchy of a tree, in a specific way. The table in question looks a bit like this (all ID fields are uniqueidentifiers, I've simplified the data for sake of example):
EstimateItemID EstimateID ParentEstimateItemID ItemType
-------------- ---------- -------------------- --------
1 A NULL product
2 A 1 product
3 A 2 service
4 A NULL product
5 A 4 product
6 A 5 service
7 A 1 service
8 A 4 product
Graphical view of the tree structure (* denotes 'service'):
A
___/ \___
/ \
1 4
/ \ / \
2 7* 5 8
/ /
3* 6*
Using this query, I can get the hierarchy (just pretend 'A' is a uniqueidentifier, I know it isn't in real life):
DECLARE #EstimateID uniqueidentifier
SELECT #EstimateID = 'A'
;WITH temp as(
SELECT * FROM EstimateItem
WHERE EstimateID = #EstimateID
UNION ALL
SELECT ei.* FROM EstimateItem ei
INNER JOIN temp x ON ei.ParentEstimateItemID = x.EstimateItemID
)
SELECT * FROM temp
This gives me the children of EstimateID 'A', but in the order that it appears in the table. ie:
EstimateItemID
--------------
1
2
3
4
5
6
7
8
Unfortunately, what I need is an ordered hierarchy with a result set that follows the following constraints:
1. each branch must be grouped
2. records with ItemType 'product' and parent are the top node
3. records with ItemType 'product' and non-NULL parent grouped after top node
4. records with ItemType 'service' are bottom node of a branch
So, the order that I need the results, in this example, is:
EstimateItemID
--------------
1
2
3
7
4
5
8
6
What do I need to add to my query to accomplish this?
Try this:
;WITH items AS (
SELECT EstimateItemID, ItemType
, 0 AS Level
, CAST(EstimateItemID AS VARCHAR(255)) AS Path
FROM EstimateItem
WHERE ParentEstimateItemID IS NULL AND EstimateID = #EstimateID
UNION ALL
SELECT i.EstimateItemID, i.ItemType
, Level + 1
, CAST(Path + '.' + CAST(i.EstimateItemID AS VARCHAR(255)) AS VARCHAR(255))
FROM EstimateItem i
INNER JOIN items itms ON itms.EstimateItemID = i.ParentEstimateItemID
)
SELECT * FROM items ORDER BY Path
With Path - rows a sorted by parents nodes
If you want sort childnodes by ItemType for each level, than you can play with Level and SUBSTRING of Pathcolumn....
Here SQLFiddle with sample of data
This is an add-on to Fabio's great idea from above. Like I said in my reply to his original post. I have re-posted his idea using more common data, table name, and fields to make it easier for others to follow.
Thank you Fabio! Great name by the way.
First some data to work with:
CREATE TABLE tblLocations (ID INT IDENTITY(1,1), Code VARCHAR(1), ParentID INT, Name VARCHAR(20));
INSERT INTO tblLocations (Code, ParentID, Name) VALUES
('A', NULL, 'West'),
('A', 1, 'WA'),
('A', 2, 'Seattle'),
('A', NULL, 'East'),
('A', 4, 'NY'),
('A', 5, 'New York'),
('A', 1, 'NV'),
('A', 7, 'Las Vegas'),
('A', 2, 'Vancouver'),
('A', 4, 'FL'),
('A', 5, 'Buffalo'),
('A', 1, 'CA'),
('A', 10, 'Miami'),
('A', 12, 'Los Angeles'),
('A', 7, 'Reno'),
('A', 12, 'San Francisco'),
('A', 10, 'Orlando'),
('A', 12, 'Sacramento');
Now the recursive query:
-- Note: The 'Code' field isn't used, but you could add it to display more info.
;WITH MyCTE AS (
SELECT ID, Name, 0 AS TreeLevel, CAST(ID AS VARCHAR(255)) AS TreePath
FROM tblLocations T1
WHERE ParentID IS NULL
UNION ALL
SELECT T2.ID, T2.Name, TreeLevel + 1, CAST(TreePath + '.' + CAST(T2.ID AS VARCHAR(255)) AS VARCHAR(255)) AS TreePath
FROM tblLocations T2
INNER JOIN MyCTE itms ON itms.ID = T2.ParentID
)
-- Note: The 'replicate' function is not needed. Added it to give a visual of the results.
SELECT ID, Replicate('.', TreeLevel * 4)+Name 'Name', TreeLevel, TreePath
FROM MyCTE
ORDER BY TreePath;
I believe that you need to add the following to the results of your CTE...
BranchID = some kind of identifier that uniquely identifies the branch. Forgive me for not being more specific, but I'm not sure what identifies a branch for your needs. Your example shows a binary tree in which all branches flow back to the root.
ItemTypeID where (for example) 0 = Product and 1 = service.
Parent = identifies the parent.
If those exist in the output, I think you should be able to use the output from your query as either another CTE or as the FROM clause in a query. Order by BranchID, ItemTypeID, Parent.

How can I select from list of values in SQL Server

I have very simple problem that I can't solve. I need to do something like this:
select distinct * from (1, 1, 1, 2, 5, 1, 6).
Anybody can help??
Edit
The data comes as a text file from one of our clients. It's totally unformatted (it's a single, very long line of text), but it may be possible to do so in Excel. But it's not practical for me, because I will need to use these values in my sql query. It's not convenient to do so every time I need to run a query.
Available only on SQL Server 2008 and over is row-constructor in this form:
You could use
SELECT DISTINCT *
FROM (
VALUES (1), (1), (1), (2), (5), (1), (6)
) AS X(a)
For more information see:
MS official
http://www.sql-server-helper.com/sql-server-2008/row-value-constructor-as-derived-table.aspx
In general :
SELECT
DISTINCT
FieldName1, FieldName2, ..., FieldNameN
FROM
(
Values
( ValueForField1, ValueForField2,..., ValueForFieldN ),
( ValueForField1, ValueForField2,..., ValueForFieldN ),
( ValueForField1, ValueForField2,..., ValueForFieldN ),
( ValueForField1, ValueForField2,..., ValueForFieldN ),
( ValueForField1, ValueForField2,..., ValueForFieldN )
) AS TempTableName ( FieldName1, FieldName2, ..., FieldNameN )
In your case :
Select
distinct
TempTableName.Field1
From
(
VALUES
(1),
(1),
(1),
(2),
(5),
(1),
(6)
) AS TempTableName (Field1)
Simplest way to get the distinct values of a long list of comma delimited text would be to use a find an replace with UNION to get the distinct values.
SELECT 1
UNION SELECT 1
UNION SELECT 1
UNION SELECT 2
UNION SELECT 5
UNION SELECT 1
UNION SELECT 6
Applied to your long line of comma delimited text
Find and replace every comma with UNION SELECT
Add a SELECT in front of the statement
You now should have a working query
Have you tried using the following syntax?
select * from (values (1), (2), (3), (4), (5)) numbers(number)
If you want to select only certain values from a single table you can try this
select distinct(*) from table_name where table_field in (1,1,2,3,4,5)
eg:
select first_name,phone_number from telephone_list where district id in (1,2,5,7,8,9)
if you want to select from multiple tables then you must go for UNION.
If you just want to select the values 1, 1, 1, 2, 5, 1, 6 then you must do this
select 1
union select 1
union select 1
union select 2
union select 5
union select 1
union select 6
PostgreSQL gives you 2 ways of doing this:
SELECT DISTINCT * FROM (VALUES('a'),('b'),('a'),('v')) AS tbl(col1)
or
SELECT DISTINCT * FROM (select unnest(array['a','b', 'a','v'])) AS tbl(col1)
using array approach you can also do something like this:
SELECT DISTINCT * FROM (select unnest(string_to_array('a;b;c;d;e;f;a;b;d', ';'))) AS tbl(col1)
I know this is a pretty old thread, but I was searching for something similar and came up with this.
Given that you had a comma-separated string, you could use string_split
select distinct value from string_split('1, 1, 1, 2, 5, 1, 6',',')
This should return
1
2
5
6
String split takes two parameters, the string input, and the separator character.
you can add an optional where statement using value as the column name
select distinct value from string_split('1, 1, 1, 2, 5, 1, 6',',')
where value > 1
produces
2
5
6
This works on SQL Server 2005 and if there is maximal number:
SELECT *
FROM
(SELECT ROW_NUMBER() OVER(ORDER BY a.id) NUMBER
FROM syscomments a
CROSS JOIN syscomments b) c
WHERE c.NUMBER IN (1,4,6,7,9)
Using GROUP BY gives you better performance than DISTINCT:
SELECT *
FROM
(
VALUES
(1),
(1),
(1),
(2),
(5),
(1),
(6)
) AS A (nums)
GROUP BY A.nums;
If you need an array, separate the array columns with a comma:
SELECT * FROM (VALUES('WOMENS'),('MENS'),('CHILDRENS')) as X([Attribute])
,(VALUES(742),(318)) AS z([StoreID])
Another way that you can use is a query like this:
SELECT DISTINCT
LTRIM(m.n.value('.[1]','varchar(8000)')) as columnName
FROM
(SELECT CAST('<XMLRoot><RowData>' + REPLACE(t.val,',','</RowData><RowData>') + '</RowData></XMLRoot>' AS XML) AS x
FROM (SELECT '1, 1, 1, 2, 5, 1, 6') AS t(val)
) dt
CROSS APPLY
x.nodes('/XMLRoot/RowData') m(n);
If it is a list of parameters from existing SQL table, for example ID list from existing Table1, then you can try this:
select distinct ID
FROM Table1
where
ID in (1, 1, 1, 2, 5, 1, 6)
ORDER BY ID;
Or, if you need List of parameters as a SQL Table constant(variable), try this:
WITH Id_list AS (
select ID
FROM Table1
where
ID in (1, 1, 1, 2, 5, 1, 6)
)
SELECT distinct * FROM Id_list
ORDER BY ID;
I create a function on most SQL DB I work on to do just this.
CREATE OR ALTER FUNCTION [dbo].[UTIL_SplitList](#parList Varchar(MAX),#splitChar Varchar(1)=',')
Returns #t table (Column_Value varchar(MAX))
as
Begin
Declare #pos integer
set #pos = CharIndex(#splitChar, #parList)
while #pos > 0
Begin
Insert Into #t (Column_Value) VALUES (Left(#parList, #pos-1))
set #parList = Right(#parList, Len(#parList) - #pos)
set #pos = CharIndex(#splitChar, #parList)
End
Insert Into #t (Column_Value) VALUES (#parList)
Return
End
Once the function exists, it is as easy as
SELECT DISTINCT
*
FROM
[dbo].[UTIL_SplitList]('1,1,1,2,5,1,6',',')
Select user id from list of user id:
SELECT * FROM my_table WHERE user_id IN (1,3,5,7,9,4);
A technique that has worked for me is to query a table that you know has a large amount of records in it, including just the Row_Number field in your result
Select Top 10000 Row_Number() OVER (Order by fieldintable) As 'recnum' From largetable
will return a result set of 10000 records from 1 to 10000, use this within another query to give you the desired results
Use the SQL In function
Something like this:
SELECT * FROM mytable WHERE:
"VALUE" In (1,2,3,7,90,500)
Works a treat in ArcGIS

Resources