Query with XML Path, Elements XSINIL and other functions - sql

I am looking for guidance in understanding the logic and meaning of this script below. I have tried to break it down but I haven't been successful. I noticed that it merges the table result into one single column with the first part of the query providing the column headers only. I'm unsure though how it does cross apply when there is only one table involved. Where did the "nodes" come from when there is no node column in the table. What does fn:local-name(.)[1] mean as well as the T1.N.Nodes('/*').
Appreciate all the help. Admin here and never had developed a query before.
select top 1 (
select '^^'+T2.N.value('fn:local-name(.)[1]', 'varchar(max)')
from (
select T.*
for xml path(''), type, ELEMENTS XSINIL
) as T1(N)
cross apply T1.N.nodes('/*') as T2(N)
for xml path(''), type, ELEMENTS XSINIL
).value('substring((.)[1], 3)', 'varchar(max)') as systemname
from dbo.tablename as T with (nolock)
UNION ALL
select (
select '^^'+ISNULL(T2.N.value('./text()[1]', 'varchar(max)'),'')
from (
select T.*
for xml path(''), type, ELEMENTS XSINIL
) as T1(N)
cross apply T1.N.nodes('/*') as T2(N)
for xml path(''), type, ELEMENTS XSINIL
).value('substring(./text()[1], 3)', 'varchar(max)')
from dbo.tablename as T with (nolock)
where 1=1

As mentioned, this is a pretty convoluted way of generating a flat-file.
Breaking it down step-by-step:
We start with a single row of values:
select top 1 ...
from dbo.tablename as T with (nolock)
We then make a correlated sub-query as follows:
Take all columns, and turn it into a single XML, keeping null elements and returning it as the xml type
from (
select T.*
for xml path(''), type, ELEMENTS XSINIL
) as T1(N)
Shred it into separate rows, one per node (column), the * selects all nodes. Since , type was used, T1.N is an xml data type, and we can shred it into rows with .nodes
cross apply T1.N.nodes('/*') as T2(N)
From that, select a string ^^ plus the node name, and create another XML. fn:local-name(.)[1] gets you the name of the current node, rather than the inner text value.
Since this is an unnamed column, all the nodes get lumped together. Then use .value('substring((.)[1], 3)' to get that whole string, stripping off the extra ^^. Effectively, this is the same as STRING_AGG
(
select '^^'+T2.N.value('fn:local-name(.)[1]', 'varchar(max)')
for xml path(''), type, ELEMENTS XSINIL
).value('substring((.)[1], 3)', 'varchar(max)') as systemname
Then we union that with the same thing again, this time selecting all rows, and the values rather than the node names
UNION ALL
select (
select '^^'+ISNULL(T2.N.value('./text()[1]', 'varchar(max)'),'')
from (
select T.*
for xml path(''), type, ELEMENTS XSINIL
) as T1(N)
cross apply T1.N.nodes('/*') as T2(N)
for xml path(''), type, ELEMENTS XSINIL
).value('substring(./text()[1], 3)', 'varchar(max)')
from dbo.tablename as T with (nolock)
where 1=1
The results is a big flat-file delimited by ^^
Notes:
I leave you to think about the consequences of nolock, it's generally a very bad idea.
I have no idea why where 1=1 is there, it does nothing.
ELEMENTS XSINIL was not necessary on the outer for xml, because the value is never null.
varchar(max) should be nvarchar(max) if there is any chance of other languages getting in here.
As mentioned, STRING_AGG would have been a better idea on newer versions of SQL Server
OPENJSON can do all of the above in a less convoluted fashion
And to be honest, bcp.exe and other tools are much better at this anyway

Related

SQL Server: display whole column only if substring found

Working with SQL Sever 2016. I am constrained by the fact we cannot create functions or stored procedures. I am trying to find %word% in many columns across a table (75). Right now, I have a very large clump of
and (fieldname1 like %word%
or fieldname2 like %word%
or fieldname3 like %word%) etc.
While cumbersome, this does provide me the correct results. However:
I am looking to simplify this and
in the select, I want to display the whole column if and only if it finds %word% (or even just the column name would work)
Thank you in advance for any thoughts.
--...slow...
declare #searchfor varchar(100) = '23';
select #searchfor as [thevalue],
thexml.query('for $a in (/*[contains(upper-case(.), upper-case(sql:variable("#searchfor")))])
return concat(local-name($a[1]), ",")').value('.', 'nvarchar(max)') as [appears_in_columns],
*
from
(
select *, (select o.* for xml path(''), type) as thexml
from sys.all_objects as o --table goes here
) as src
where thexml.exist('/*[contains(upper-case(.), upper-case(sql:variable("#searchfor")))]') = 1;
One option uses cross apply to unpivot the table and then search:
select v.*
from mytable t
cross apply (values
('fieldname1', fieldname1),
('fieldname2', fieldname2),
('fieldname3', fieldname3)
) v(fieldname, fieldvalue)
where v.fieldvalue like '%word%'
Note that if more than one column contains the search word, you will get several rows in the resultset. I am unsure how you want to handle this use case (there are options).
SELECT OBJECT_NAME(id) ObjectName , [Text]
FROM syscomments
WHERE TEXT LIKE '%word%'

Alternative for a WITH inside FROM

I`m trying to make a query in ssms that shows all errors and the amount of them.
The error message is in XML but in a nvarchar field, thats why I have to do a "with".
;with CastToXML as (
select CAST(tableName.XMLField as xml) as x
from tableName
group by tableName.XMLField
)
select distinct h.ep.value('(./logMessage)[1]', 'VARCHAR(max)') as log1
from CastToXML
cross apply x.nodes('xmlfield') as h(ep)
This doesn't allow me to do a group by log1.
Please help me.
If you want to add a COUNT(*) to your final results, just move your current query into another CTE and query and group by its results (after removing the DISTINCT, obviously):
;with CastToXML as (
select CAST(tableName.XMLField as xml) as x
from tableName
group by tableName.XMLField
), Errors as (
select h.ep.value('(./logMessage)[1]', 'VARCHAR(max)') as log1
from CastToXML
cross apply
x.nodes('xmlfield') as h(ep)
)
select log1,COUNT(*)
from Errors
group by log1
If I get this correctly, you want to count the messages within your XML.
Besides the fact, that you really should store your XML in a natively typed column (the repeated cast to XML is very expensive!), you can use simple XQuery to achieve the same:
DECLARE #SomeXML XML=
N'<xmlfield>
<logMessage>Message 1</logMessage>
<logMessage>Message 2</logMessage>
<logMessage>Message 3</logMessage>
<logMessage>Message 1</logMessage>
</xmlfield>';
SELECT #SomeXML.value('count(/xmlfield/logMessage)','int') AS CountOfLogMessages
,#SomeXML.value('count(distinct-values(/xmlfield/logMessage))','int') AS DistinctCountOfLogMessages
Hint You can use this in your query in the same place as you've got the other .value() call.

XML column Data into rows

I have a requirement where I have XML data column in database, which I need to pull in the form of values from rows XML data column from database. My XML is like
<ListID><ID>169346</ID><ID>289492</ID><ID>315264</ID><ID>415265</ID></ListID>
<ListID><ID>169356</ID><ID>299492</ID><ID>315264</ID><ID>415265</ID></ListID>
And I want data to be pulled up like
ID
169346
289492
315264
415265
169356
299492
315264
415265
You can use something like this:
SELECT XC.value('.', 'int')
FROM dbo.YourTableHere
CROSS APPLY XmlColumn.nodes('//ID') AS XT(XC)
This basically takes every <ID> element that exists in the XML column, and extract the values as int and shows them in a result set.
Update: from your question, mentioning I have XML data column, I assumed that your column in the SQL Server table is in fact of type XML. And it should be, if you're storing XML in it!
But if it's not - then you need to cast your column to XML first, before using the function .nodes() in my code sample:
SELECT XC.value('.', 'int')
FROM dbo.YourTableHere
CROSS APPLY CAST(YourColumn AS XML).nodes('//ID') AS XT(XC)
Here is one other way is to convert it to XML & then convert it to row :
SELECT split.a.value('.', 'varchar(max)') ID
FROM
(
SELECT CAST(ID AS XML) AS String from <table_name>
) a
CROSS APPLY String.nodes('//ID') AS split(a);
Result :
ID
169346
289492
315264
415265
169356
299492
315264
415265

SQL return list of ntext and convert it to XML

I have a query that will return and a list of ntext, and in these ntext they contain XML value.
my question is how to convert each of ntext to xml and do logic with it
Query:
select a.content
from dbo.content as a
inner join dbo.xml_collection_tbl as b on a.xml_fg_id = b.xml_collection_id
where a.inherit_from='val1' and b.collection_title='val2' and a.content_table= 'val3'
result:
what I want to do here is to check rather the Query returns contain the value that I looking for. lets say the page title = "hello World"
I tried below.But it returns many empty rows and with one correct row
select cast(a.content_html as xml).query('(//root[pagetitle/text()="AAA"])') content_html1
from dbo.content as a
inner join dbo.xml_collection_tbl as b on a.xml_fg_id = b.xml_collection_id
where a.inherit_from='val1' and b.collection_title='val2' and a.content_table= 'val3'
expected result is: return only one row where it's not empty (row 54)
First of all: NTEXT, TEXT and IMAGE are deprecated for centuries and will not be supported in future versions! Get rid of this type as soon as possible!
SQL-Server does not store the XML as the text you see, but as a hierarchically stuctured tree. This makes the handling of an XML astonishingly fast (no parsing on string level!). Your approach has to parse each and every XML over and over, which is a very expensive operation! Change your XML's storage to the native XML type and you will be very happy with the new performance!
If you have to stick with this, you can try as such:
DECLARE #t TABLE (ID INT IDENTITY, YourXML NTEXT);
INSERT INTO #t VALUES('<root><pagetitle>111</pagetitle></root>')
,('<root><pagetitle>aaa</pagetitle></root>')
,('<root><pagetitle>222</pagetitle></root>')
SELECT A.CastedXML
,B.pt.query('.')
FROM #t AS t
CROSS APPLY(SELECT CAST(YourXML AS XML) AS CastedXML) AS A
CROSS APPLY A.CastedXML.nodes('/root/pagetitle[text()="aaa"]') AS B(pt);
Demo of XQuery expression https://learn.microsoft.com/en-us/sql/xquery/xquery-language-reference-sql-server to filter data
with sd as (
select cast(content_html as xml) as col
from (
values
('<root><pagetitle>FFF</pagetitle></root>')
,('<root><pagetitle>AAA</pagetitle></root>')
) as a(content_html)
)
select t.n.value('.[1]', 'varchar(100)') as content_html1
from sd
cross apply col.nodes('root/pagetitle[text()="AAA"]') t(n)

Compare Xml data in SQL

I have two tables with same NVARCHAR field that really contains XML data.
in some cases this really-XML-field is really same as one row in other table but differs in attributes order and therefor string comparison does not return the correct result!!!
and to determining the same XML fields ,I need to have a comparison like:
cast('<root><book b="" c="" a=""/></root>' as XML)
= cast('<root><book a="" b="" c=""/></root>' as XML)
but I get this Err Msg:
The XML data type cannot be compared or sorted, except when using the
IS NULL operator.
then what is the best solution to determine the same XML without re-casting them to NVARCHAR?
Why cast it at all? Just plug them into an XML column in a temp table and run Xquery to compare them to the other table. EDIT: Included example of the comparison. There are many, many ways to run the query against the XML to get the rows that are the same - exactly how that query is written is going to depend on preference, requirements, etc. I went with a simple group by/count, but a self join could be used, WHERE EXISTS against the columns that are being searched for duplicates, you name it.
CREATE TABLE #Test (SomeXML NVARCHAR(MAX))
CREATE TABLE #XML (SomeXML XML)
INSERT #Test (SomeXML)
VALUES('<root><book b="b" c="c" a="a"/></root>')
,('<root><book a="a" b="b" c="c"/></root>')
INSERT #XML (SomeXML)
SELECT SomeXML FROM #Test;
WITH XMLCompare (a,b,c)
AS
(
SELECT
x.c.value('#a[1]','char(1)') AS a
,x.c.value('#b[1]','char(1)') AS b
,x.c.value('#c[1]','char(1)') AS c
FROM #XML
CROSS APPLY SomeXMl.nodes('/root/book') X(C)
)
SELECT
a
,b
,c
FROM XMLCompare as a
GROUP BY
a
,b
,c
HAVING COUNT(*) >1