SQL: Filter XML retrieving using xpath - sql

I have documents table, each row has a XML column Document:
<Document>
<Good GroupId="..."/>
<Good GroupId="..."/>
...
</Document>
I also have temp table with subset of GroupId values:
DECLARE #Groups TABLE (groupId VARCHAR(MAX));
Next, I wrote a select query to documents table, goal - retrieve XML from Document:
SELECT
(SELECT CAST(Document.data as XML)).query('/Document/Good') AS Goods
FROM
Documents as Document
JOIN
#Numbers n ON n.number = Document.number
WHERE
Document.type = #type
--FOR XML AUTO, ROOT('Documents')
As a result, in column Goods I got all Good items for each Document
Task:
In step 3, I need filter Good elements by GroupId attribute (using #Groups) - I need all Good for which GroupId value is not contained in #Groups
Thanks!

Do CROSS APPLY to extract the attribute values from the XML
Need to use LEFT JOIN to find Goods in XML but not in #Goods table.
Here is the SQL Fiddle: http://www.sqlfiddle.com/#!3/a6b9e/5
SELECT G.value('#GroupId', 'varchar(max)') FROM
(
SELECT
CAST(Document.data as XML) AS Goods
FROM
Documents as Document
WHERE type = 1
) T
CROSS APPLY T.Goods.nodes('Document/Good') D(G)
LEFT JOIN #Groups GS
ON G.value('#GroupId', 'varchar(max)') = GS.groupId
WHERE GS.groupId IS NULL

Related

Update a column in Table B with multiple row values from Table A using XML PATH

I have 4 columns in Table A viz., Inv_Num1, Inv_Date1, Inv_Amt1, Inv_DocNum1
I have 4 columns in Table B viz., Inv_Num2, Inv_Date2, Inv_Amt2, Inv_Status2
I would like to match the rows between Table A and Table B by using an inner join where condition on is
Invoice_Num1=Invoice_Num2 AND Invoice_Date1=Invoice_Date2 AND
Invoice_Amt1=Invoice_Amt2
When I do this matching I may get more than 1 row as a result in Table
A (Invoice_DocNum1 column)
I tried XML Path code but I dont know how to implement in Update statement
update cis2
set cis2.Inv_Status2 =
(SELECT
TypeName = STUFF((
SELECT '; ' + imd1.Inv_DocNum1
FROM [VRS].[Table_B] cis1
INNER JOIN [Table_A] imd1
ON cis1.Inv_Num1 = imd1.Inv_Num2
WHERE cis1.Inv_Num1 = imd1.Inv_Num2
AND cis1.Inv_Date1 = imd1.Inv_Date2
AND cis1.Inv_Amt1 = imd1.Inv_Amt2
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
) FROM Table_B cis2
Doing this to your database is against good practices since it violates 1NF. But you could still this if you are deadset on doing it. Something along these lines should work.
with myCte as
(
SELECT Inv_Num1
, TypeName = STUFF((
SELECT '; ' + imd1.Inv_DocNum1
FROM [VRS].[Table_B] cis1
INNER JOIN [Table_A] imd1
ON cis1.Inv_Num1 = imd1.Inv_Num2
WHERE cis1.Inv_Num1 = imd1.Inv_Num2
AND cis1.Inv_Date1 = imd1.Inv_Date2
AND cis1.Inv_Amt1 = imd1.Inv_Amt2
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
from Table_A
group by Inv_Num1
)
update tb
set Inv_Status2 = c.TypeName
from Table_B tb
join myCte c on c.Inv_Num1 = tb.Inv_Num2
The answer has two parts. First, you need to produce your comma-separated list per row. The best way to do it is STRING_AGG (https://learn.microsoft.com/en-us/sql/t-sql/functions/string-agg-transact-sql?view=sql-server-2017)
You will need to use it with the group by, like select ..., STRING_AGG(Inv_DocNum1, ',') group by ... where ... stands for your three fields forming unique key.
Second, you need to use update ... from syntax, see https://learn.microsoft.com/en-us/sql/t-sql/queries/update-transact-sql?view=sql-server-2017#l-specifying-a-table-alias-as-the-target-object. In your case, it will be from your target table joining the resultset you computed at step one.

SQL Server: How to add count to nested select

I have two tables, one containing a list of files and one containing a list of tags which are linked by a fileID.
Currently I select this as follows which works fine so far.
How do I have to amend this if I want to count the tags per file and show this in addition to the selected data ?
What I want to do is show how many tags are assigned to each file.
My SP:
SELECT C.fileTitle,
C.fileID,
(
SELECT T.fileTag
FROM Files_Tags T
WHERE T.fileID = C.fileID
ORDER BY T.fileTag
FOR XML PATH(''), ELEMENTS, TYPE
) AS tags
FROM Files C
ORDER BY C.fileTitle
FOR XML PATH('files'), ELEMENTS, TYPE, ROOT('root')
Many thanks for any help with this, Tim.
You can add a subquery:
SELECT C.fileTitle,
C.fileID,
(
SELECT COUNT(*)
FROM Files_Tags T
WHERE T.fileID = C.fileID
) AS NumTags,
(
SELECT T.fileTag
FROM Files_Tags T
WHERE T.fileID = C.fileID
ORDER BY T.fileTag
FOR XML PATH(''), ELEMENTS, TYPE
) AS tags
You could also put in a join and aggregation in the outer query. But, your query already has to use a nested select for the concatenation, so you might as well use the same structure for the count.
Can't you just do this?:
SELECT C.fileTitle,
C.fileID,
(
SELECT T.fileTag
FROM Files_Tags T
WHERE T.fileID = C.fileID
ORDER BY T.fileTag
FOR XML PATH(''), ELEMENTS, TYPE
) AS tags,
(
SELECT
COUNT(*)
FROM
Files_Tags T
WHERE
T.fileID = C.fileID
) AS NumberOfTages
FROM Files C
ORDER BY C.fileTitle
FOR XML PATH('files'), ELEMENTS, TYPE, ROOT('root')
Anding a sub query for the count

SQL select join on xml field with xpath expression

I have a following query that can return a result from an xml:
declare #xml xml
select #xml = data from files where id = 1234
select
children.p.value('./speed[1]','float')
from #xml.nodes('root/children') as children(p)
where
children.p.value('./name[1]','nvarchar(max)') = 'something'
This in my case returns a single value, for example 3141
However, I'd like to do multiple selects like this from multiple XMLs.
I can select the xml data as
select id, cast(data as xml) as xml
from files
where id in (1005,51,968,991,992,993,969,970) --for example
I imagine there must be some kind of JOIN that will apply my expression and return a single item for each xml variable in the table, but I am not sure how.
Use apply:
select
f.id, children.p.value('./speed[1]','float')
from files as f
outer apply (select cast(f.data as xml) as xml) as x
outer apply x.xml.nodes('root/children') as children(p)
where
f.id in (1005,51,968,991,992,993,969,970) and
children.p.value('./name[1]','nvarchar(max)') = 'something'

Assign Values of a column in SubQuery in SQL

I am trying to do following in SQL Server:
SELECT
PRODUCER_NAME, PRODUCER_ID,
(SELECT #X = #X + PRODUCT_NAME
FROM PRODUCT
WHERE PRODUCER_ID = PRODUCER.ID)
FROM
PRODUCER
There are two tables. Producer table is list of all producers. Product table stores product produced by producers. #x is varchar variable
Basically I want a list of all products, comma-separated by producer.
For example
Producer Products
-------- --------------------------
P1 ProductA,ProductB,ProductC
P2 ProductD,ProductE
I don't know if this is possible this way. Do anyone know how to do this without joining tables?
I don't have a way for you to assign multiple output comma-separated lists to a single varchar variable, but maybe you don't actually need that anyway. Try this:
SELECT Producer = PRODUCER.PRODUCER_NAME,
Products = STUFF(
(
SELECT N',' + PRODUCT.PRODUCT_NAME
FROM dbo.PRODUCT
WHERE PRODUCT.PRODUCER_ID = PRODUCER.ID
FOR XML PATH(''),
TYPE).value(N'./text()[1]', N'nvarchar(max)'),1,1,N'')
FROM dbo.PRODUCER;
On a large table, this kind of correlated subquery can be quite expensive. On SQL Server 2017+ we can use STRING_AGG() in a single pass:
SELECT Producer = PRODUCER.PRODUCER_NAME,
Products = STRING_AGG(PRODUCT.PRODUCT_NAME, N',')
FROM dbo.PRODUCT
INNER JOIN dbo.PRODUCER
ON PRODUCT.PRODUCER_ID = PRODUCER.ID
GROUP BY PRODUCER.PRODUCER_NAME;
Example db<>fiddle
If you want to concatenate names, you can do this:
select
P.PRODUCER_NAME, P.PRODUCER_ID,
stuff(
(
select ',' + T.PRODUCT_NAME
from PRODUCT as T
where T.PRODUCER_ID = P.PRODUCER_ID
for xml path(''), type
).value('.', 'nvarchar(max)')
, 1, 1, '') as PRODUCT_NAMES
from PRODUCER as P
Two notes:
Always use the table aliases for such a queries. To see why - delete alias name from query and drop PRODuCER_ID from PRODUCT table.
Use value method instead of implicit conversion to nvarchar to correctly work with names like 'Product & 1'.

SQL: Nested SELECT with multiple values in a single field

In my SQL 2005 DB, I have a table with values stored as IDs with relationships to other tables. So in my MyDBO.warranty table, I'm storing product_id instead of product_name in order to save space. The product_name is stored in MyDBO.products.
When the marketing department pulls the demographic information, the query selects the corresponding name for each ID from related tables (trimmed down for brevity):
SELECT w1.warranty_id AS "No.",
w1.created AS "Register Date"
w1.full_name AS "Name",
w1.purchase_date AS "Purchased",
(
SELECT p1.product_name
FROM WarrDBO.products p1 WITH(NOLOCK)
WHERE p1.product_id = i1.product_id
) AS "Product Purchased",
i1.accessories
FROM WarrDBO.warranty w1
LEFT OUTER JOIN WarrDBO.warranty_info i1
ON i1.warranty_id = w1.warranty_id
ORDER BY w1.warranty_id ASC
Now, my problem is that the "accessories" column on the warranty_info table stores several values:
No. Register Date Name Purchased Accessories
---------------------------------------------------------------------
1500 1/1/2008 Smith, John Some Product 5,7,9
1501 1/1/2008 Hancock, John Another 2,3
1502 1/1/2008 Brown, James And Another 2,9
I need to do something similar with "Accessories" that I did with "Product" and pull accessory_name from the MyDBO.accessories table using accessory_id. I'm not sure where to start, because first I'd need to extract the IDs and then somehow concatenate multiple values into a string. So each line would have "accessoryname1,accessoryname2,accessoryname3":
No. Register Date Name Purchased Accessories
---------------------------------------------------------------------
1500 1/1/2008 Smith, John Some Product Case,Bag,Padding
1501 1/1/2008 Hancock, John Another Wrap,Label
1502 1/1/2008 Brown, James And Another Wrap,Padding
How do I do this?
EDIT>> Posting my final code:
I created this function:
CREATE FUNCTION SQL_GTOInc.Split
(
#delimited varchar(50),
#delimiter varchar(1)
) RETURNS #t TABLE
(
-- Id column can be commented out, not required for sql splitting string
id INT identity(1,1), -- I use this column for numbering splitted parts
val INT
)
AS
BEGIN
declare #xml xml
set #xml = N'<root><r>' + replace(#delimited,#delimiter,'</r><r>') + '</r></root>'
insert into #t(val)
select
r.value('.','varchar(5)') as item
from #xml.nodes('//root/r') as records(r)
RETURN
END
And updated my code accordingly:
SELECT w1.warranty_id,
i1.accessories,
(
CASE
WHEN i1.accessories <> '' AND i1.accessories <> 'NULL' AND LEN(i1.accessories) > 0 THEN
STUFF(
(
SELECT ', ' + a1.accessory
FROM MyDBO.accessories a1
INNER JOIN MyDBO.Split(i1.accessories, ',') a2
ON a1.accessory_id = a2.val
FOR XML PATH('')
), 1, 1, ''
)
ELSE ''
END
) AS "Accessories"
FROM MyDBO.warranty w1
LEFT OUTER JOIN MyDBO.warranty_info i1
ON i1.warranty_id = w1.warranty_id
You could write a table valued function that simply splits comma separated string into XML and turns XML nodes to rows.
See:
http://www.kodyaz.com/articles//t-sql-convert-split-delimeted-string-as-rows-using-xml.aspx
Join to accessories through the result of function call, and stuff the result back to comma separated list of names.
Untested code:
SELECT w1.warranty_id AS "No.",
w1.created AS "Register Date"
w1.full_name AS "Name",
w1.purchase_date AS "Purchased",
(
SELECT p1.product_name
FROM WarrDBO.products p1 WITH(NOLOCK)
WHERE p1.product_id = i1.product_id
) AS "Product Purchased",
STUFF(
(
SELECT
', ' + a.name
FROM [table-valued-function](i1.accessories) acc_list
INNER JOIN accessories a ON acc_list.id = a.id
FOR XML PATH('')
), 1, 1, ''
) AS [accessories]
FROM WarrDBO.warranty w1
LEFT OUTER JOIN WarrDBO.warranty_info i1
ON i1.warranty_id = w1.warranty_id
ORDER BY w1.warranty_id ASC
Nothing to do with your question. Just a note that your original query can also be written, moving the subqery to a join, as:
SELECT w1.warranty_id AS "No.",
w1.created AS "Register Date"
w1.full_name AS "Name",
w1.purchase_date AS "Purchased",
p1.product_name AS "Product Purchased",
i1.accessories
FROM WarrDBO.warranty w1
INNER JOIN WarrDBO.products p1
ON p1.product_id = i1.product_id
LEFT OUTER JOIN WarrDBO.warranty_info i1
ON i1.warranty_id = w1.warranty_id
ORDER BY w1.warranty_id ASC
You just need to use the FOR XML feature of SQL Server to easily cat strings:
Example from the linked blog post:
SELECT
STUFF(
(
SELECT
' ' + Description
FROM dbo.Brands
FOR XML PATH('')
), 1, 1, ''
) As concatenated_string
To parse a field that has already been stored as comma delimited you will have to write a UDF that parses the field and returns a table which can then be used with an IN predicate in your WHERE clause. Look here for starters, and here.
It seem to be a work for a concatenate aggregate function.
In SQL it can be deployed using CLR
http://www.mssqltips.com/tip.asp?tip=2022