TSQL CTE hierarchy - sql-server

I am trying to create a CTE on my table to pull in a hierarchy of employees.
I have a starting point which is a "Director" and I want to find everyone that reports to each person under them.
Here is what I have so far:
;WITH EmpTable_CTE (FirstName, LastName, QID, Email) AS
(
SELECT FirstName,
LastName,
QID,
Email
FROM EmployeeTable E
WHERE QID = '12345'
UNION ALL
SELECT E.FirstName,
E.LastName,
E.QID,
E.Email
FROM EmployeeTable E
INNER JOIN EmpTable_CTE AS E2 ON E.MgrQID = E2.QID
)
SELECT * FROM EmpTable_CTE
This seems to work providing me a list of employees but there is no "hierarchy" to it.
How can I go about using FOR XML to create the hierarchy that I am looking for?
<Director>Bob Smith</Director>
<Direct>Jim Smith</Direct>
<Direct>Employee 1</direct>
<Direct>Employee 2</direct>
<Direct>Employee 3</direct>
<Direct>Bob Jones</Direct>
<Direct>Employee 1</direct>
<Direct>Employee 2</direct>
<Direct>Employee 3</direct>
<Direct>Employee A</direct>
I'm sure its just a matter of placing the FOR XML line somewhere but cant quite figure it out.
Update: Here is a SQL Fiddle of sample data:
http://sqlfiddle.com/#!6/a48f6/1
This is how I would expect the data to be from the fiddle:
<Director>Jim Jones</Director>
<Direct>Bob Jones</Direct>
<Direct>Jake Jones</Direct>
<Direct>Smith Jones</Direct>
<Direct>Carl Jones</Direct>
<Direct>Bobby Jones</Direct>
<Direct>Danny Jones</Direct>
<Direct>Billy Jones</Direct>

Part of the difficulty is in the XML structure you presented - If you passed that into a parser, it would all be flat, and running the results of my process below without stuffing the First and Last name into an attribute made the nodes come out in mixed content (text with nodes on the same level).
So, I went searching and found this little gem here on SE. Adapting it to your needs, and throwing in a few fields as attributes from your table, I came up with this:
CREATE FUNCTION dbo.EmpHierarchyNode(#QID int)
RETURNS XML
WITH RETURNS NULL ON NULL INPUT
BEGIN RETURN
(SELECT QID AS "#ID", Email AS "#Email",
FirstName + ' ' + LastName AS "#Name",
CASE WHEN MgrQID = #QID
THEN dbo.EmpHierarchyNode(QID)
END
FROM dbo.EmployeeTable
WHERE MgrQID = #QID
FOR XML PATH('Direct'), TYPE)
END
SELECT QID AS "#ID", Email AS "#Email",
FirstName + ' ' + LastName AS "#Name",
dbo.EmpHierarchyNode(QID)
FROM dbo.EmployeeTable
WHERE MgrQID IS NULL
FOR XML PATH('Director'), TYPE
Essentially, this traverses down in the hierarchy, recursively calling itself. The CTE isn't sufficient if your output is targeted for XML. Using this, and what I could glean of your sample data, I got this as a result:
<Director ID="1" Email="bsmith#someCompany.com" Name="Bob Smith">
<Direct ID="2" Email="jsmith#someCompany.com" Name="Jim Smith">
<Direct ID="4" Email="e1#someCompany.com" Name="Employee 1" />
<Direct ID="5" Email="e2#someCompany.com" Name="Employee 2" />
<Direct ID="7" Email="e4#someCompany.com" Name="Employee 4" />
</Direct>
<Direct ID="3" Email="bjones#someCompany.com" Name="Bob Jones">
<Direct ID="6" Email="e3#someCompany.com" Name="Employee 3" />
<Direct ID="8" Email="e5#someCompany.com" Name="Employee 5" />
<Direct ID="9" Email="e6#someCompany.com" Name="Employee 6" />
</Direct>
</Director>
Hope this helps.
EDIT: Last Minute SQLFiddle Example.

See if it meets yours requirement:
;WITH EmpTable_CTE (FirstName, LastName, QID, Email) AS
(
SELECT FirstName,
LastName,
QID,
Email
FROM EmployeeTable E
WHERE QID = 1
UNION ALL
SELECT E.FirstName,
E.LastName,
E.QID,
E.Email
FROM EmployeeTable E
INNER JOIN EmpTable_CTE AS E2
ON E.MgrQID = E2.QID
)
SELECT LastName + ', ' + FirstName FROM EmpTable_CTE FOR XML PATH('Direct'), ROOT('Director'), TYPE

Related

How do I query repeating XML child nodes inside parent repeating nodes?

I have the following XML stored in an nText column (not my design, older database). I need to pull the PolicyNumber and the CvgCode that is a property of Coverage child node.
<efs:Request
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:efs="http://www.slsot.org/efs"
xsi:schemaLocation="http://www.slsot.org/efs
http://efs.slsot.org/efs/xsd/SlsotEfsSchema2.xsd">
<EfsVersion>2.0</EfsVersion>
<Batch BatchType="N" AgLicNo="12345" ItemCnt="69">
<EFSPolicy>
<PolicyNumber>POL12345</PolicyNumber>
<Binder>0086592YZ</Binder>
<TransType>N</TransType>
<Insured>Dummy Co LLC</Insured>
<ZipCode>75225</ZipCode>
<ClassCd>99930</ClassCd>
<PolicyFee>35.00</PolicyFee>
<TotalTax>36.62</TotalTax>
<TotalStampFee>1.13</TotalStampFee>
<TotalGross>792.75</TotalGross>
<EffectiveDate>09/17/2018</EffectiveDate>
<ExpirationDate>09/17/2019</ExpirationDate>
<IssueDate>09/20/2018</IssueDate>
<ContUntilCancl>N</ContUntilCancl>
<FedCrUnion>N</FedCrUnion>
<AORFlag>N</AORFlag>
<CustomID>043684</CustomID>
<WindStormExclusion>N</WindStormExclusion>
<CorrectionReEntry>N</CorrectionReEntry>
<Coverages>
<Coverage CvgCode="9325">720.00</Coverage>
</Coverages>
<Securities>
<Company CoNumber="80101168">100.00</Company>
</Securities>
</EFSPolicy>
<EFSPolicy>
...
</EFSPolicy>
</Batch>
</efs:Request>
And here is the SQL code I am using to extract the PolicyNumber (so far).
with cte_table(BatchID, xmlData)
AS
(SELECT BatchID, CAST(CAST(xmlData AS VARCHAR(MAX)) AS XML) from
Batches)
select
s.BatchID
,t.c.value('PolicyNumber[1]', 'varchar(max)') as PolicyNumber
from cte_table as s
cross apply s.xmlData.nodes('/*:Request/Batch/EFSPolicy') as t(c)
where BatchID in (select batchID from Batches where CreateDate between '1/1/19' and getdate())
I have tried a second CROSS APPLY on the Coverages node, but that was giving me all the Coverage values (not CvgCode property) for every batch, so my result set was 100+ times too many rows. I assume that was due to the 2nd CROSS APPLY, is there a INNER JOIN type CROSS APPLY?
You need to declare your namespaces to retrieve the data:
DECLARE #XML xml = '<efs:Request
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:efs="http://www.slsot.org/efs"
xsi:schemaLocation="http://www.slsot.org/efs
http://efs.slsot.org/efs/xsd/SlsotEfsSchema2.xsd">
<EfsVersion>2.0</EfsVersion>
<Batch BatchType="N" AgLicNo="12345" ItemCnt="69">
<EFSPolicy>
<PolicyNumber>POL12345</PolicyNumber>
<Binder>0086592YZ</Binder>
<TransType>N</TransType>
<Insured>Dummy Co LLC</Insured>
<ZipCode>75225</ZipCode>
<ClassCd>99930</ClassCd>
<PolicyFee>35.00</PolicyFee>
<TotalTax>36.62</TotalTax>
<TotalStampFee>1.13</TotalStampFee>
<TotalGross>792.75</TotalGross>
<EffectiveDate>09/17/2018</EffectiveDate>
<ExpirationDate>09/17/2019</ExpirationDate>
<IssueDate>09/20/2018</IssueDate>
<ContUntilCancl>N</ContUntilCancl>
<FedCrUnion>N</FedCrUnion>
<AORFlag>N</AORFlag>
<CustomID>043684</CustomID>
<WindStormExclusion>N</WindStormExclusion>
<CorrectionReEntry>N</CorrectionReEntry>
<Coverages>
<Coverage CvgCode="9325">720.00</Coverage>
</Coverages>
<Securities>
<Company CoNumber="80101168">100.00</Company>
</Securities>
</EFSPolicy>
<EFSPolicy>
</EFSPolicy>
</Batch>
</efs:Request>';
WITH XMLNAMESPACES('http://www.w3.org/2001/XMLSchema-instance' as xsi,
'http://www.slsot.org/efs' AS efs)
SELECT EFS.[Policy].value('(./PolicyNumber/text())[1]','varchar(25)') AS PolicyNumber,
EFS.[Policy].value('(./Coverages/Coverage/#CvgCode)[1]','int') AS CvgCode --Assumes only 1 CvgCode per policy
FROM (VALUES(#XML)) V(X)
CROSS APPLY V.X.nodes('efs:Request/Batch/EFSPolicy') EFS([Policy]);

Converting Every Child Tags in to a Single Column with multiple Delimiters -SQL Server (3)

My xml:
declare #x xml='<Detail>
<ROll ID="1">
<Exams>
<Examdetails date="2017-04-02 13:30:00">
<Exam name="ECO" Total="100">150</Exam>
<Exam name="BIO" Total="150">50</Exam>
<Exam name="MATH" Total="200">28</Exam>
</Examdetails>
<Examdetails date="2017-04-02 14:30:00">
<Exam name="ENGLISH" Total="100">150</Exam>
<Exam name="BIO" Total="200">50</Exam>
<Exam name="ZIO" Total="250">28</Exam>
</Examdetails>
</Exams>
</ROll>
<ROll ID="2">
<Exams>
<Examdetails date="2017-05-02 13:30:00">
<Exam name="HIS" Total="100">150</Exam>
<Exam name="BIO" Total="200">50</Exam>
<Exam name="THI" Total="200">89</Exam>
</Examdetails>
</Exams>
</ROll>
</Detail>'
I want to Segregate my xml based on ROLL ID's while i tried with the below query referred from here
SELECT STUFF(
(
SELECT '!' + STUFF(p.query(N'for $n in .//*
return <a>{concat("$",($n/text())[1])}</a>'
).value(N'.',N'nvarchar(max)'),1,1,'')
FROM p.nodes(N'Exams') AS A(p)
FOR XML PATH(''),TYPE).value(N'.',N'nvarchar(max)'),1,1,'')
FROM #x.nodes(N'Detail/ROll') AS A(p);
I get the result as
But i want to query it back as
2017-04-02 13:30:00$ECO$100$150!2017-04-02 13:30:00$BIO$150$50!2017-04-02 13:30:00$MATH$200$28!2017-04-02 14:30:00$ENGLISH$100$150!2017-04-02 14:30:00$BIO$200$50!2017-04-02 14:30:00$ZIO$250$28
2017-05-02 13:30:00$HIS$100$150!2017-05-02 13:30:00$BIO$200$50!2017-05-02 13:30:00$THI$200$89
Kindly help me solve this complexity
Thanks in advance ,Jayendran
In this case I'd leave the generical path and build it up like this:
SELECT r.value(N'#ID',N'int') AS ROll_ID
,STUFF((
SELECT
(
SELECT '!'+ed.value(N'#date',N'nvarchar(max)')
+'$' + e.value(N'#name','nvarchar(max)')
+'$' + e.value(N'#Total','nvarchar(max)')
+'$' + e.value(N'text()[1]','nvarchar(max)')
FROM ed.nodes(N'Exam') AS D(e)
FOR XML PATH(''),TYPE
).value(N'text()[1]','nvarchar(max)')
FROM ex.nodes(N'Examdetails') AS C(ed)
FOR XML PATH(''),TYPE
).value(N'text()[1]','nvarchar(max)'),1,1,'')
FROM #x.nodes(N'/Detail/ROll') AS A(r)
CROSS APPLY r.nodes(N'Exams') AS B(ex);
Main issues why a generic solution might not work:
it is difficult to mix attributes and element's text() generically
(important!) the attributes order is not guaranteed! Attribute values might appear not in order expected...

Convert groups of multiple key-value rows to XML

I have a table called userInfo that has data similar to the following:
Id, Field, Value
---------------------
1, FirstName, John
1, LastName, Smith
1, Age, 25
1, Gender, Male
2, FirstName, Jane
2, LastName, Smythe
2, Age, 24
2, Gender, Female
What I need is some T-SQL that will produce a single row for each Id with the following structure:
Row:1
<FieldValues>
<FirstName>John</FirstName>
<LastName>Smith</LastName>
<Age>25</Age>
<Gender>Male</Gender>
</FieldValues>
Row:2
<FieldValues>
<FirstName>Jane</FirstName>
<LastName>Smythe</LastName>
<Age>24</Age>
<Gender>Female</Gender>
</FieldValues>
I have tried a couple of things to get this but can't get figure this out.
Edit:
The list of Fields I provided here (i.e. FirstName, LastName, etc) is not a static list of fields. I will be adding and taking away from this list all the time so the query would be able to handle this automatically). Ideally I could use something like FOR XML PATH('FieldValues')
You can build your XML as a string using for xml path('') and then cast to XML.
select T.Id,
cast('<FieldValues>' + (
select '<'+T2.Field+'>'+
(select T2.Value as '*' for xml path(''))+
'</'+T2.Field+'>'
from dbo.YourTable as T2
where T.Id = T2.Id
for xml path(''), type
).value('text()[1]', 'varchar(max)') +
'</FieldValues>' as xml) as FieldValues
from dbo.YourTable as T
group by T.Id;
SQL Fiddle
This part (select T2.Value as '*' for xml path('')) is there to take care of characters that needs to be entities in the value like &.
Here is one way:
SELECT '<FieldValues>'+
'<FirstName>'+fn.Value +'</FirstName>' +
'<LastName>'+ln.Value +'</LastName>' +
'<Age>'+age.Value +'</Age>' +
'<Gender>'+gender.Value +'</Gender>' +
'</FieldValues>
FROM (SELECT DISTINCT ID FROM userInfo) t
JOIN userInfo fn ON t.ID = fn.ID and fn.Field = 'FirstName'
JOIN userInfo ln ON t.ID = ln.ID and ln.Field = 'LastName'
JOIN userInfo age ON t.ID = age.ID and age.Field = 'Age'
JOIN userInfo gender ON t.ID = gender.ID and gender.Field = 'Gender'
How this works:
First I create table of just the unique ID numbers.
SELECT DISTINCT ID FROM table
Then I use this table to join back to the main table for each field. (Each of these joins will have only one row per ID.)
JOIN table fn ON t.ID = fn.ID and fn.Field = 'FirstName'
JOIN table ln ON t.ID = ln.ID and ln.Field = 'LastName'
JOIN table age ON t.ID = age.ID and age.Field = 'Age'
JOIN table gender ON t.ID = gender.ID and gender.Field = 'Gender'
Finally I create a string formatted as you need.
'<FieldValues>'+
'<FirstName>'+fn.Value +'</FirstName>' +
'<LastName>'+ln.Value +'</LastName>' +
'<Age>'+age.Value +'</Age>' +
'<Gender>'+gender.Value +'</Gender>' +
'</FieldValues>
An additional note: It is recommended to not use Camel Case on your xml since xml is case sensitive any use of case is a pain -- most just use all lower case.

How do you select from a xml column in sql server if the column is not a singleton

I am trying to query an xml column among other...the following query is working fine...
SELECT
OrderID,
AccountNumber,
ItemID,
substring(replace(lower(s.Street),' ',''),1,8)
+ substring(replace(lower(s.City),' ',''),1,8)
+ substring(replace(lower(s.State),' ',''),1,8)
+ substring(replace(s.ZipCode,' ',''),1,5)AddressHash,
ShipName,
Street,
Street2
City,
State,
ZipCode,
OrderDate
FROM (SELECT UpwardOrderID, AccountNumber, UpwardLeagueID,
/* NOTE THAT THIS SYNTAX WORKS ONLY WORKS BECAUSE THE NODES ARE SINGLETONS. */
x.value('(./ShipTo/Name)[1]', 'VARCHAR(255)') AS ShipName,
x.value('(./ShipTo/Street1)[1]', 'VARCHAR(255)') AS Street,
x.value('(./ShipTo/Street2)[1]', 'VARCHAR(255)') AS Street2,
x.value('(./ShipTo/Subdivision1)[1]', 'VARCHAR(255)') AS City,
x.value('(./ShipTo/Subdivision2)[1]', 'VARCHAR(255)') AS State,
x.value('(./ShipTo/PostalCode)[1]', 'VARCHAR(255)') AS ZipCode,
x.value('(./Order/ClientOrderDate)[1]', 'DATETIME') AS OrderDate
--x.value('(./ShippingMethods/ShippingMethod/ID)[../Selected/text()=1]','VARCHAR(255)')
FROM av_order CROSS APPLY orderXML.nodes('/Order/ShippingInformation') t(x)
WHERE orderXML Is Not Null) s
This query is working fine except for the last column I am trying to select in the FROM subquery.
The difference is that column (ShippingMethod) is NOT a singleton. The XML contains all of the shipping methods and I want to select the ID of the SELECTED shipping method. Here is what that part of the XML looks like...
<Order>...
<ShippingInformation>
<ShipTo>
<Name>DONT SHOW</Name>
<Attention>DONT SHOW</Attention>
<Street1>DONT SHOW</Street1>
<Street2 />
<Subdivision1>DONT SHOW</Subdivision1>
<Subdivision2>IL</Subdivision2>
<PostalCode>62092</PostalCode>
<CountryCode>US</CountryCode>
<AllowEmptyShipTo>0</AllowEmptyShipTo>
<ContactInfo>DONT SHOW</ContactInfo>
</ShipTo>
<ShippingMethods>
<ShippingMethod>
<ID>UPSGROUND</ID>
<Selected>1</Selected>
<Cost>134.08</Cost>
</ShippingMethod>
<ShippingMethod>
<ID>PICKUP</ID>
<Selected>0</Selected>
<Cost>0</Cost>
</ShippingMethod>
<ShippingMethod>
<ID>UPS3DAY</ID>
<Selected>0</Selected>
<Cost>288.46</Cost>
</ShippingMethod>
<ShippingMethod>
<ID>UPS2DAY</ID>
<Selected>0</Selected>
<Cost>347.91</Cost>
</ShippingMethod>
<ShippingMethod>
<ID>UPSNEXTBUSDAY</ID>
<Selected>0</Selected>
<Cost>956.73</Cost>
</ShippingMethod>
</ShippingMethods>
</ShippingInformation>
...</Order>
What do I do to select the SELECTED shipping method ID?
Seth
You can use a predicate against Selected in the xQuery expression.
This will pick the first ShippingMethod node where Selected has the value of 1.
x.value('(./ShippingMethods/ShippingMethod[Selected = 1]/ID)[1]','VARCHAR(255)')

TSQL FOR XML EXPLICIT

Not able to to get the desired XML output
The following:
SELECT 1 as Tag,
0 as Parent,
sID as [Document!1!sID],
docID as [Document!1!docID],
null as [To!2!value]
FROM docSVsys with (nolock)
where docSVsys.sID = '57'
UNION ALL
SELECT 2 as Tag,
1 as Parent,
sID,
NULL,
value
FROM docMVtext
WHERE docMVtext.sID = '57'
ORDER BY [Document!1!sID],[To!2!value]
FOR XML EXPLICIT;
Produces:
<Document sID="57" docID="3.818919.C41P3UKK00BRICLAY0AR1ET2EBPYSU4SA">
<To value="Frank Ermis" />
<To value="Keith Holst" />
<To value="Mike Grigsby" />
</Document>
What I want is:
<Document sID="57">
<docID>3.818919.C41P3UKK00BRICLAY0AR1ET2EBPYSU4SA</docID>
<To>
<Value>Frank Ermis</Value>
<Value>Keith Holst</Value>
<Value>Mike Grigsby</Value>
</To>
</Document>
Can I get that ouput with FOR XML?
Ok I get they may be technically equivalent.
What I want and what I need are not the same.
Using xDocument for this is is SLOW.
There are millions of documents and need to XML up to 1 million at a time to XML.
The TSQL FOR XML is super fast.
I just need to get FOR XML to format.
The solution (based on accepted answer):
SELECT top 4
[sv].[sID] AS '#sID'
,[sv].[sParID] AS '#sParID'
,[sv].[docID] AS 'docID'
,[sv].addDate as 'addDate'
,(SELECT [value] AS 'value'
FROM [docMVtext] as [mv]
WHERE [mv].[sID] = [sv].[sID]
AND [mv].[fieldID] = '113'
ORDER BY [mv].[value]
FOR XML PATH (''), type
) AS "To"
,(SELECT [value] AS 'value'
FROM [docMVtext] as [mv]
WHERE [mv].[sID] = [sv].[sID]
AND [mv].[fieldID] = '130'
ORDER BY [mv].[value]
FOR XML PATH (''), type
) AS "MVtest"
FROM [docSVsys] as [sv]
WHERE [sv].[sID] >= '57'
ORDER BY
[sv].[sParID], [sv].[sID]
FOR XML PATH('Document'), root('Documents')
Produces:
<Documents>
<Document sID="57" sParID="57">
<docID>3.818919.C41P3UKK00BRICLAY0AR1ET2EBPYSU4SA</docID>
<addDate>2011-10-28T12:26:00</addDate>
<To>
<value>Frank Ermis</value>
<value>Keith Holst</value>
<value>Mike Grigsby</value>
</To>
<MVtest>
<value>MV test 01</value>
<value>MV test 02</value>
<value>MV test 03</value>
<value>MV test 04</value>
</MVtest>
</Document>
<Document sID="58" sParID="57">
<docID>3.818919.C41P3UKK00BRICLAY0AR1ET2EBPYSU4SA.1</docID>
<addDate>2011-10-28T12:26:00</addDate>
</Document>
<Document sID="59" sParID="59">
<docID>3.818920.KJKP5LYKTNIODOEI4JDOKJ2BXJI5P0BIA</docID>
<addDate>2011-10-28T12:26:00</addDate>
<To>
<value>Vladimir Gorny</value>
</To>
</Document>
<Document sID="60" sParID="59">
<docID>3.818920.KJKP5LYKTNIODOEI4JDOKJ2BXJI5P0BIA.1</docID>
<addDate>2011-10-28T12:26:00</addDate>
</Document>
</Documents>
Now what I need to do is to add a DispName attribute to the element MVtext. Attribute cannot have any spaces and I would like to include the friendly name e.g. Multi Value Text.
Try something like this (untested, since I don't have your database tables to test against...):
SELECT
sv.sID AS '#sID',
sv.docID AS 'docID',
(SELECT
value AS 'value'
FROM
dbo.docMVtext mv
WHERE
mv.sID = sv.sID
ORDER BY mv.value
FOR XML PATH (''), TYPE) AS 'To'
FROM
dbo.docSVsys sv
WHERE
sv.sID = '57'
ORDER BY
sv.sID
FOR XML PATH('Document')
Does that give you what you're looking for?? And don't you agree with John and me: this is much simpler than FOR XML EXPLICIT.....
From Examples: Using PATH Mode:
USE AdventureWorks2008R2;
GO
SELECT ProductModelID AS "#ProductModelID",
Name AS "#ProductModelName",
(SELECT ProductID AS "data()"
FROM Production.Product
WHERE Production.Product.ProductModelID =
Production.ProductModel.ProductModelID
FOR XML PATH ('')
) AS "#ProductIDs",
(
SELECT Name AS "ProductName"
FROM Production.Product
WHERE Production.Product.ProductModelID =
Production.ProductModel.ProductModelID
FOR XML PATH (''), type
) AS "ProductNames"
FROM Production.ProductModel
WHERE ProductModelID= 7 OR ProductModelID=9
FOR XML PATH('ProductModelData');

Resources