join multiple 'for XML' outputs in an efficient manner - sql-server

In the code below, I am join the xml output of two independent select clauses into one XML.
create procedure Test
as
declare #result nvarchar(max)
set #result = '<data>'
set #result = #result + (select FieldA from Table1 for xml raw('a'), root('table1'))
set #result = #result + (select FieldB from Table2 for xml raw('b'), root('table2'))
set #result = #result + '/<data>'
return #result
Output would look like:
<data>
<table1>
<a FieldA="01"/>
<a FieldA="02"/>
</table1>
<table2>
<b FieldB="aa"/>
<b FieldB="bb"/>
</table2>
</data>
I would like to know if there is a much better way to achieve the above output with greater Performance.
I believe 'for xml' output (xml data type) is streamed while the nvarchar is not. So, is it beneficial to output as XML data type? or, does Xml datatype overheads (parsing, wellformedness checks, etc) overweight the streaming benefit over nvarchar datatype?
Edit The procedure will be called by an ASP.Net application and results are read through the ADO.Net classes

It is possible to combine xml from subqueries into a bigger xml result. The key is to use the sqlserver xml type.
with foodata as (
select 1 as n
union all
select n+1 from foodata where n < 20
)
select
(select n as 'text()' from foodata where n between 10 and 13 for xml path('row'), root('query1'), type),
(select n as 'text()' from foodata where n between 15 and 18 for xml path('row'), root('query2'), type)
for xml path('root'), type
The query above will generate the following output:
<root>
<query1>
<row>10</row>
<row>11</row>
<row>12</row>
<row>13</row>
</query1>
<query2>
<row>15</row>
<row>16</row>
<row>17</row>
<row>18</row>
</query2>
</root>
I can't say which is faster though. You will just have to do some benchmarks.

Related

The xml root is repeating multiple time in sql?

I wanted the output of my xml to look like this:
<CustomConfigurationSteps>
<CustomConfigurations>
<CustomConfiguration SiteURL="abc">
<CustomElements>
<CustomElement Type="Searchtype" unit="3" price="5">
<ClassValue>AZSupreme</ClassValue>
</CustomElement>
</CustomElements>
<CustomElements>
<CustomElement Type="Searchtype" unit="3" price="5">
<ClassVaue>AZSupreme</ClassVaue>
</CustomElement>
</CustomElements>
</CustomConfiguration>
</CustomConfigurations>
</CustomConfigurationSteps>
I tried the below logic, but its not giving me the exact output.
DECLARE #xml xml
SET #xml = (SELECT
config.SiteURL AS '#SiteURL',
(SELECT
CustomConfiguration.Type AS '#Type',
CustomConfiguration.[PackageName] AS '#unit',
CustomConfiguration.[UseExecute] AS '#price',
ClassValue
FOR XML PATH('CustomElement'), TYPE) AS CustomElements
FROM
[dbo].[CustomConfigurationSteps] AS CustomConfiguration WITH (nolock)
INNER JOIN
WebSiteConfiguration AS config WITH (nolock) ON config.ConfigurationID = CustomConfiguration.[ConfigurationID]
WHERE
CustomConfiguration.[ConfigurationID] = 9
FOR XML path ('CustomConfiguration'), TYPE)
--set #xml1=( SELECT #xml FOR XML path ('CustomConfiguration') ,type)
SELECT #xml
FOR XML path ('CustomConfigurations'), ROOT ('CustomConfigurationSteps')
Try it like this (although this looks a little odd, especially the repeating <CustomElements> node.
A mockup table to simulate your issue:
DECLARE #tbl TABLE([Type] VARCHAR(100),unit int, price DECIMAL(10,4),ClassValue VARCHAR(100));
INSERT INTO #tbl VALUES('Searchtype 1',1,11,'AZSupreme 1')
,('Searchtype 2',2,22,'AZSupreme 2');
--The query
SELECT 'abc' AS [CustomConfiguration/#SiteURL]
,(
SELECT t.[Type] AS [CustomElement/#Type]
,t.unit AS [CustomElement/#unit]
,t.price AS [CustomElement/#pricee]
,t.ClassValue [CustomElement/ClassValue]
FROM #tbl t
FOR XML PATH('CustomElements'),TYPE
) AS CustomConfiguration
FOR XML PATH('CustomConfigurations'),ROOT('CustomConfigurationSteps');

How cut a thousands of line T-SQL to small stored procedures or functions?

I have some problem to reduce the size of my stored procedure, which has more than 2000 lines of code.
Here is a short example (shortly 3 outer XML, 3 inner XML).
CREATE PROCEDURE spu_Get_Rep_Data
#IdContrat INT,
#Annee INT,
#XML XML OUTPUT
AS
BEGIN
DECLARE #XML_Contrat XML;
EXEC spu_Get_Contrat #IdContrat = #IdContrat, #XML = #XML_Contrat OUTPUT;
DECLARE #XML_ProdSous_Ev XML;
EXEC spu_Get_ProdSous_Ev #IdContrat = #IdContrat, #XML = #XML_ProdSous_Ev OUTPUT;
DECLARE #XML_Ana_Bact XML;
EXEC spu_Get_Ana_Bact #IdContrat = #IdContrat, #XML = #XML_Ana_Bact OUTPUT;
SET #XML = (
SELECT #Annee AS '#Annee',
#XML_Contrat,
#XML_ProdSous_Ev,
#XML_Ana_Bact,
(SELECT
(SELECT IpLibelle AS '#Name',
(SELECT
(SELECT 'Entry' AS '#Name',
(SELECT 'Stick' AS '#Type',
'2E5576' AS '#Color'
FOR XML PATH ('SeriesConfig'), TYPE),
(SELECT p.Year AS '#Name',
ISNULL(CAST(SUM(p.Valeur) AS DECIMAL(18, 8)), '0') AS '#Val'
FROM dbo.Production AS p
WHERE p.Year = #Annee
AND p.IdContrat = #IdContrat
AND p.IpTypeCode = 407
AND p.IpId = e.IpId
GROUP BY p.Annee
ORDER BY p.Annee
FOR XML PATH ('Categorie'), TYPE)
FOR XML PATH ('Serie'), TYPE)
FOR XML PATH ('VolYear'), TYPE),
(SELECT CAST(SUM(Valeur) AS DECIMAL(18, 8)) AS '#Elec'
FROM dbo.Production AS p
WHERE p.IdContrat = #IdContrat
AND p.IpId = e.IpId
AND p.IpTypeCode = 101
FOR XML PATH ('ElecStep'), TYPE),
(SELECT Name AS '#Val1',
Value AS '#Val2'
FROM dbo.Production AS p
WHERE p.IdContrat = #IdContrat
AND p.IpId = e.IpId
AND p.IpTypeCode = 653
FOR XML PATH ('ReparationStep'), TYPE)
FROM dbo.Entite AS e
WHERE IdContrat = #IdContrat
ORDER BY e.IpLibelle
FOR XML PATH ('StepData'), TYPE)
FOR XML PATH ('StepsData'), TYPE)
FOR XML PATH ('Elements')
)
END
It retrieves the data of a "contrat", which may has several types of equipments and each type contains at least one instance, to format XML.
It would be so much better to extract the inner SELECT ... FOR XML PATH to stored procedure (which is better in performance than the user-defined function).
Expect result 1 using user-defined function:
SET #XML = (
SELECT #Annee AS '#Annee',
#XML_Contrat,
#XML_ProdSous_Ev,
#XML_Ana_Bact,
(SELECT
(SELECT IpLibelle AS '#Name',
(EXEC spu_Get_Vol_Year(e.IpId, #IdContrat, #Annee)),
(EXEC spu_Get_Elec(e.IpId, #IdContrat)),
(EXEC spu_Get_Reparation(e.IpId, #IdContrat))
FROM dbo.Entite AS e
WHERE IdContrat = #IdContrat
ORDER BY e.IpLibelle
FOR XML PATH ('StepData'), TYPE)
FOR XML PATH ('StepsData'), TYPE)
FOR XML PATH ('Elements')
)
The 3 outer stored procedure works. Since the stored procedure IS NOT allowed using in SELECT, the 3 inner stored procedures which need the parameter of IpId would not work. Considering that there must be someone who wanted to simplify any large T-SQL to pieces, I took the chance for help.
Thank you in advance for any suggestions.

For xml path returns null instead of nothing

I thought that following query suppose to return nothing, but, instead, it returns one record with a column containing null:
select *
from ( select 1 as "data"
where 0 = 1
for xml path('row') ) as fxpr(xmlcol)
If you run just the subquery - nothing is returned, but when this subquery has an outer query, performing a select on it, null is returned.
Why is that happening?
SQL Server will try to predict the type. Look at this
SELECT tbl.[IsThereAType?] + '_test'
,tbl.ThisIsINT + 100
FROM
(
SELECT NULL AS [IsThereAType?]
,3 AS ThisIsINT
UNION ALL
SELECT 'abc'
,NULL
--UNION ALL
--SELECT 1
-- ,NULL
) AS tbl;
The first column will be predicted as string type, while the second is taken as INT. That's why the + operator on top works. Try to add a number to the first or a string to the second. This will fail.
Try to uncomment the last block and it will fail too.
The prediction is done at a very early stage. Look at this, where I did include the third UNION ALL (invalid query, breaking the type):
EXEC sp_describe_first_result_set
N'SELECT *
FROM
(
SELECT NULL AS [IsThereAType?]
,3 AS ThisIsINT
UNION ALL
SELECT ''abc''
,NULL
UNION ALL
SELECT 1
,NULL
) AS tbl';
The result returns "IsThereAType?" as INT! (I'm pretty sure this is rather random and might be different on your system.)
Btw: Without this last block the type is VARCHAR(3)...
Now to your question
A naked XML is taken as NTEXT (altough this is deprecated!) and needs ,TYPE to be predicted as XML:
EXEC sp_describe_first_result_set N'SELECT ''blah'' FOR XML PATH(''blub'')';
EXEC sp_describe_first_result_set N'SELECT ''blah'' FOR XML PATH(''blub''),TYPE';
The same wrapped within a sub-select returns as NVARCHAR(MAX) resp. XML
EXEC sp_describe_first_result_set N'SELECT * FROM(SELECT ''blah'' FOR XML PATH(''blub'')) AS x(y)';
EXEC sp_describe_first_result_set N'SELECT * FROM(SELECT ''blah'' FOR XML PATH(''blub''),TYPE) AS x(y)';
Well, this is a bit weird actually...
An XML is a scalar value taken as NTEXT, NVARCHAR(MAX) or XML (depending on the way you are calling it). But it is not allowed to place a naked scalar in a sub-select:
SELECT * FROM('blah') AS x(y) --fails
While this is okay
SELECT * FROM(SELECT 'blah') AS x(y)
Conclusio:
The query parser seems to be slightly inconsistent in your special case:
Although a sub-select cannot consist of one scalar value only, the SELECT ... FOR XML (which returs a scalar actually) is not rejected. The engine seems to interpret this as a SELECT returning a scalar value. And this is perfectly okay.
This is usefull with nested sub-selects as a column (correlated sub-queries) to nest XML:
SELECT TOP 5 t.TABLE_NAME
,(
SELECT COLUMN_NAME,DATA_TYPE
FROM INFORMATION_SCHEMA.COLUMNS AS c
WHERE c.TABLE_SCHEMA=t.TABLE_SCHEMA
AND c.TABLE_NAME=t.TABLE_NAME
FOR XML PATH('Column'),ROOT('Columns'),TYPE
) AS AllTablesColumns
FROM INFORMATION_SCHEMA.TABLES AS t;
Without the FOR XML clause this would fail (...more than one value... / ...Only one column...)
Pass a generic SELECT as a parameter?
Some would say this is not possible, but you can try this:
CREATE FUNCTION dbo.TestType(#x XML)
RETURNS TABLE
AS
RETURN
SELECT #x AS BringMeBack;
GO
--The SELECT must be wrapped in paranthesis!
SELECT *
FROM dbo.TestType((SELECT TOP 5 * FROM sys.objects FOR XML PATH('x'),ROOT('y')));
GO
DROP FUNCTION dbo.TestType;
Empty XML Data is treated as NULL in SQL Server.
select *
from ( select 1 as "data"
where 0 = 1
for xml path('row') ) as fxpr(xmlcol)
The Subquery will be executed first and the result of the subquery i.e (Empty Rowset) will be converted to XML therefore, getting NULL Value.

Concatenate XML without type casting to string

I have the following XML generated from various tables in my SQL SERVER database
<XMLData>
...
<Type>1</Type>
...
</XMLData>
AND
<XMLData>
...
<Type>2</Type>
...
</XMLData>
AND
<XMLData>
...
<Type>3</Type>
...
</XMLData>
The final output I need is single combined as follows:
<AllMyData>
<XMLData>
...
<Type>1</Type>
...
</XMLData>
<XMLData>
...
<Type>2</Type>
...
</XMLData>
<XMLData>
...
<Type>3</Type>
...
</XMLData>
<AllMyData>
NOTE - all the independent elements that I am combining have the same tag name.
Thanks in advance for looking this up.
I have the following XML generated from various tables in my SQL
SERVER database
Depends on how you have it but if it is in a XML variable you can do like this.
declare #XML1 xml
declare #XML2 xml
declare #XML3 xml
set #XML1 = '<XMLData><Type>1</Type></XMLData>'
set #XML2 = '<XMLData><Type>2</Type></XMLData>'
set #XML3 = '<XMLData><Type>3</Type></XMLData>'
select #XML1, #XML2, #XML3
for xml path('AllMyData')
I can't comment but can answer so even though I think a comment is more appropriate, I'll expand on what rainabba answered above to add a bit more control. My .Net code needs to know the column name returned so I can't rely on auto-generated names but needed the very tip rainabba provided above otherwise.
This way, the xml can effectively be concatenated into a single row and the resulting column named. You could use this same approach to assign the results to an XML variable and return that from a PROC also.
SELECT (
SELECT XmlData as [*]
FROM
(
SELECT
xmlResult AS [*]
FROM
#XmlRes
WHERE
xmlResult IS NOT NULL
FOR XML PATH(''), TYPE
) as DATA(XmlData)
FOR XML PATH('')
) as [someColumnName]
If you use for xml type, you can combine the XML columns without casting them. For example:
select *
from (
select (
select 1 as Type
for xml path(''), type
)
union all
select (
select 2 as Type
for xml path(''), type
)
union all
select (
select 3 as Type
for xml path(''), type
)
) as Data(XmlData)
for xml path(''), root('AllMyData'), type
This prints:
<AllMyData>
<XmlData>
<Type>1</Type>
</XmlData>
<XmlData>
<Type>2</Type>
</XmlData>
<XmlData>
<Type>3</Type>
</XmlData>
</AllMyData>
As an addendum to Mikael Eriksson's answer - If you have a process where you need to continually add nodes and then want to group that under a single node, this is one way to do it:
declare #XML1 XML
declare #XML2 XML
declare #XML3 XML
declare #XMLSummary XML
set #XML1 = '<XMLData><Type>1</Type></XMLData>'
set #XMLSummary = (SELECT #XMLSummary, #XML1 FOR XML PATH(''))
set #XML2 = '<XMLData><Type>2</Type></XMLData>'
set #XMLSummary = (SELECT #XMLSummary, #XML2 FOR XML PATH(''))
set #XML3 = '<XMLData><Type>3</Type></XMLData>'
set #XMLSummary = (SELECT #XMLSummary, #XML3 FOR XML PATH(''))
SELECT #XMLSummary FOR XML PATH('AllMyData')
I needed to do the same but without knowing how many rows/variables were concerned and without extra schema added so here was my solution. Following this pattern, I can generate as many snippets as I want, combine them, pass them between PROCS or even return them from procs and at any point, wrap them up in containers all without modifying the data or being forced to add XML structure into my data. I use this approach with HTTP end points to provide XML Web services and with another trick that converts XML into JSON, to provide JSON WebServices.
-- SETUP A type (or use this design for a Table Variable) to temporarily store snippets into. The pattern can be repeated to pass/store snippets to build
-- larger elements and those can be further combined following the pattern.
CREATE TYPE [dbo].[XMLRes] AS TABLE(
[xmlResult] [xml] NULL
)
GO
-- Call the following as much as you like to build up all the elements you want included in the larger element
INSERT INTO #XMLRes ( xmlResult )
SELECT
(
SELECT
'foo' '#bar'
FOR XML
PATH('SomeTopLevelElement')
)
-- This is the key to "concatenating" many snippets into a larger element. At the end of this, add " ,ROOT('DocumentRoot') " to wrapp them up in another element even
-- The outer select is a time from user2503764 that controls the output column name
SELECT (
SELECT XmlData as [*]
FROM
(
SELECT
xmlResult AS [*]
FROM
#XmlRes
WHERE
xmlResult IS NOT NULL
FOR XML PATH(''), TYPE
) as DATA(XmlData)
FOR XML PATH('')
) as [someColumnName]
ALTER PROCEDURE usp_fillHDDT #Code int
AS
DECLARE #HD XML,#DT XML;
SET NOCOUNT ON;
select invhdcode, invInvoiceNO,invDate,invCusCode,InvAmount into #HD
from dbo.trnInvoiceHD where invhdcode=#Code
select invdtSlNo No,invdtitemcode ItemCode,invdtitemcode ItemName,
invDtRate Rate,invDtQty Qty,invDtAmount Amount ,'Kg' Unit into #DT from
dbo.trnInvoiceDt where invDtTrncode=#Code
set #HD = (select * from #HD HD FOR XML AUTO,ELEMENTS XSINIL);
set #DT = (select* from #DT DT FOR XML AUTO,ELEMENTS XSINIL);
SELECT CAST ('<OUTPUT>'+ CAST (ISNULL(#HD,'') AS VARCHAR(MAX))+ CAST ( ISNULL(#DT,'') AS VARCHAR(MAX))+ '</OUTPUT>' AS XML)
public String ReplaceSpecialChar(String inStr)
{
inStr = inStr.Replace("&", "&");
inStr = inStr.Replace("<", "<");
inStr = inStr.Replace(">", ">");
inStr = inStr.Replace("'", "'");
inStr = inStr.Replace("\"", """);
return inStr;
}

Query XML creating field names whithout knowing node names

If I have a SQL SERVER 2012 table containing an XML field type. The records it could contain are as follows.
I have simplified my problem to the following.
Record 1:
ID_FIELD='nn1'
XML_FIELD=
<KNOWN_NAME_1>
<UNKNOWN_NAME1>Some value</UNKNOWN_NAME1>
<UNKNOWN_NAME2>Some value</UNKNOWN_NAME2>
... Maybe more ...
</KNOWN_NAME_1>
Record 2:
ID_FIELD='nn2'
XML_FIELD=
<KNOWN_NAME_2>
<UNKNOWN_NAME1>Some value</UNKNOWN_NAME1>
<UNKNOWN_NAME2>Some value</UNKNOWN_NAME2>
... Maybe more unknown fields ...
</KNOWN_NAME_2>
I want to output non xml:
UNKNOWN_NAME1 | UNKNOWN_NAME2 | ETC
-----------------------------------
Some Value Some value
For a known root value (i.e. KNOWN_NAME_1)
I.e. If I new the node values (which I don't) I could
SELECT
XMLData.Node.value('UNKNOWN_NAME1[1]', 'varchar(100)') ,
XMLData.Node.value('UNKNOWN_NAME2[1], 'varchar(100)')
FROM FooTable
CROSS APPLY MyXmlField.nodes('//KNOWN_NAME_1') XMLData(Node)
-- WHERE SOME ID value = 'NN1' (all XML records have a separate id)
All is good however I want to do this for all the nodes (unknown quantity) without knowing the node names. The root will only contain nodes it wont get any deeper.
Is this possible in SQL?
I have looked at this but I doubt I can get enough rights to implement it.
http://architectshack.com/ClrXmlShredder.ashx
If you don't know the column names in the output you have to use dynamic SQL:
-- Source table
declare #FooTable table
(
ID_FIELD char(3),
XML_FIELD xml
)
-- Sample data
insert into #FooTable values
('nn1', '<KNOWN_NAME_1>
<UNKNOWN_NAME1>Some value1</UNKNOWN_NAME1>
<UNKNOWN_NAME2>Some value2</UNKNOWN_NAME2>
</KNOWN_NAME_1>')
-- ID to look for
declare #ID char(3) = 'nn1'
-- Element name to look for
declare #KnownName varchar(100) = 'KNOWN_NAME_1'
-- Variable to hold the XML to process
declare #XML xml
-- Get the XML
select #XML = XML_FIELD
from #FooTable
where ID_FIELD = #ID
-- Variable for dynamic SQL
declare #SQL nvarchar(max)
-- Build the query
select #SQL = 'select '+stuff(
(
select ',T.N.value('''+T.N.value('local-name(.)', 'sysname')+'[1]'', ''varchar(max)'') as '+T.N.value('local-name(.)', 'sysname')
from #XML.nodes('/*[local-name(.)=sql:variable("#KnownName")]/*') as T(N)
for xml path(''), type
).value('.', 'nvarchar(max)'), 1, 1, '')+
' from #XML.nodes(''/*[local-name(.)=sql:variable("#KnownName")]'') as T(N)'
-- Execute the query
exec sp_executesql #SQL,
N'#XML xml, #KnownName varchar(100)',
#XML = #XML,
#KnownName = #KnownName
Result:
UNKNOWN_NAME1 UNKNOWN_NAME2
--------------- ---------------
Some value1 Some value2
The dynamically generated query looks like this:
select T.N.value('UNKNOWN_NAME1[1]', 'varchar(max)') as UNKNOWN_NAME1,
T.N.value('UNKNOWN_NAME2[1]', 'varchar(max)') as UNKNOWN_NAME2
from #XML.nodes('/*[local-name(.)=sql:variable("#KnownName")]') as T(N)
SE-Data

Resources