Querying XML colum for values - sql

I have a SQL Server table with an XML column, and it contains data something like this:
<Query>
<QueryGroup>
<QueryRule>
<Attribute>Integration</Attribute>
<RuleOperator>8</RuleOperator>
<Value />
<Grouping>OrOperator</Grouping>
</QueryRule>
<QueryRule>
<Attribute>Integration</Attribute>
<RuleOperator>5</RuleOperator>
<Value>None</Value>
<Grouping>AndOperator</Grouping>
</QueryRule>
</QueryGroup>
</Query>
Each QueryRule will only have one Attribute, but each QueryGroup can have many QueryRules. Each Query can also have many QueryGroups.
I need to be able to pull all records that have one or more QueryRule with a certain attribute and value.
SELECT *
FROM QueryBuilderQueries
WHERE [the xml contains any value=X where the attribute is either Y or Z]
I've worked out how to check a specific QueryRule, but not "any".
SELECT
Query
FROM
QueryBuilderQueries
WHERE
Query.value('(/Query/QueryGroup/QueryRule/Value)[1]', 'varchar(max)') like 'UserToFind'
AND Query.value('(/Query/QueryGroup/QueryRule/Attribute)[1]', 'varchar(max)') in ('FirstName', 'LastName')

You can use two exist(). One to check the value and one to check Attribute.
select Q.Query
from dbo.QueryBuilderQueries as Q
where Q.Query.exist('/Query/QueryGroup/QueryRule/Value/text()[. = "UserToFind"]') = 1 and
Q.Query.exist('/Query/QueryGroup/QueryRule/Attribute/text()[. = ("FirstName", "LastName")]') = 1
If you really want the like equivalence when you search for a Value you can use contains().
select Q.Query
from dbo.QueryBuilderQueries as Q
where Q.Query.exist('/Query/QueryGroup/QueryRule/Value/text()[contains(., "UserToFind")]') = 1 and
Q.Query.exist('/Query/QueryGroup/QueryRule/Attribute/text()[. = ("FirstName", "LastName")]') = 1

According to http://technet.microsoft.com/pl-pl/library/ms178030%28v=sql.110%29.aspx
"The XQuery must return at most one value"
If you are quite certain that for example your XML has let's say maximum 10 QueryRules you could maybe use WHILE to loop everything while droping your results into temporary table?
maybe below can help you anyway
CREATE TABLE #temp(
Query type)
DECLARE #i INT
SET #i = 1
WHILE #i >= 10
BEGIN
INSERT INTO #temp
SELECT
Query
FROM QueryBuilderQueries
WHERE Query.value('(/Query/QueryGroup/QueryRule/Value)[#i]', 'varchar(max)') LIKE 'UserToFind'
AND Query.value('(/Query/QueryGroup/QueryRule/Attribute)[#i]', 'varchar(max)') IN ('FirstName', 'LastName')
#i = #i + 1
END
SELECT
*
FROM #temp

It's a pity that the SQL Server (I'm using 2008) does not support some XQuery functions related to string such as fn:matches, ... If it supported such functions, we could query right inside XQuery expression to determine if there is any. However we still have another approach. That is by turning all the possible values into the corresponding SQL row to use the WHERE and LIKE features of SQL for searching/filtering. After some experiementing with the nodes() method (used on an XML data), I think it's the best choice to go:
select *
from QueryBuilderQueries
where exists( select *
from Query.nodes('//QueryRule') as v(x)
where LOWER(v.x.value('(Attribute)[1]','varchar(max)'))
in ('firstname','lastname')
and v.x.value('(Value)[1]','varchar(max)') like 'UserToFind')

Related

Apply like function on an array is SQL Server

I am getting array from front end to perform filters according that inside the SQL query.
I want to apply a LIKE filter on the array. How to add an array inside LIKE function?
I am using Angular with Html as front end and Node as back end.
Array being passed in from the front end:
[ "Sports", "Life", "Relationship", ...]
SQL query is :
SELECT *
FROM Skills
WHERE Description LIKE ('%Sports%')
SELECT *
FROM Skills
WHERE Description LIKE ('%Life%')
SELECT *
FROM Skills
WHERE Description LIKE ('%Relationship%')
But I am getting an array from the front end - how to create a query for this?
In SQL Server 2017 you can use OPENJSON to consume the JSON string as-is:
SELECT *
FROM skills
WHERE EXISTS (
SELECT 1
FROM OPENJSON('["Sports", "Life", "Relationship"]', '$') AS j
WHERE skills.description LIKE '%' + j.value + '%'
)
Demo on db<>fiddle
As an example, for SQL Server 2016+ and STRING_SPLIT():
DECLARE #Str NVARCHAR(100) = N'mast;mode'
SELECT name FROM sys.databases sd
INNER JOIN STRING_SPLIT(#Str, N';') val ON sd.name LIKE N'%' + val.value + N'%'
-- returns:
name
------
master
model
Worth to mention that input data to be strictly controlled, since such way can lead to SQL Injection attack
As the alternative and more safer and simpler approach: SQL can be generated on an app side this way:
Select * from Skills
WHERE (
Description Like '%Sports%'
OR Description Like '%Life%'
OR Description Like '%Life%'
)
A simple map()-call on the words array will allow you to generate the corresponding queries, which you can then execute (with or without joining them first into a single string).
Demo:
var words = ["Sports", "Life", "Relationship"];
var template = "Select * From Skills Where Description Like ('%{0}%')";
var queries = words.map(word => template.replace('{0}', word));
var combinedQuery = queries.join("\r\n");
console.log(queries);
console.log(combinedQuery);

Get SQL Server column names with values

I am trying to create a query that allows me to have the column name next to its column values that it finds.
As an example:
SELECT
*
FROM
INFORMATION_SCHEMA.COLUMNS
WHERE
TABLE_NAME = 'saverTbl'
The above returns all of the column names in that table like this:
id, Link_userTblID, type, environment, vendor, theGUID, theGUID, etc etc...
Now what I need is to get the output from the table name next to the value. This is my query to just get my values from the table:
SELECT
*
FROM
saverTbl
WHERE
LINK_userTblID = #val1
The above returns the row that matches the LINK_userTblID like so:
32, 1, 'Blah', 'something', 'Adobe', 546656156-45332-54616516-4515, etc etc..
Now putting that all together (which is what this question is all about):
id: 32,
LINK_userTblID: 1,
type: Blah,
environment: something,
vendor: Adobe,
theGUID: 546656156-45332-54616516-4515,
etc etc.....
Pretty much needing the output in a json format but the column name matching up with the columns value.
Try this:
SELECT * FROM saverTbl
WHERE LINK_userTblID = #val1
FOR JSON PATH
Assuming, this is meant for one single row you can try this:
WITH cte AS
(
SELECT
(SELECT TOP 1 * FROM sys.objects FOR XML PATH('row'),TYPE) AS TheXml
)
SELECT TheElement.value('local-name(.)','nvarchar(max)')
+ ': '
+ TheElement.value('text()[1]','nvarchar(max)') AS YourOutput
FROM cte
CROSS APPLY TheXml.nodes('/row/*') AS A(TheElement);
The result:
YourOutput
---------------
name: sysrscols
object_id: 3
schema_id: 4
parent_object_id: 0
type: S
type_desc: SYSTEM_TABLE
create_date: 2012-02-10T20:15:58.693
modify_date: 2012-02-10T20:15:58.700
is_ms_shipped: 1
is_published: 0
is_schema_published: 0
XML in connection with XQuery and XPath is a very mighty toolset to solve rather generic problems. The cte builds an XML which looks like this:
<row>
<name>sysrscols</name>
<object_id>3</object_id>
<schema_id>4</schema_id>
<parent_object_id>0</parent_object_id>
<type>S </type>
<type_desc>SYSTEM_TABLE</type_desc>
<create_date>2012-02-10T20:15:58.693</create_date>
<modify_date>2012-02-10T20:15:58.700</modify_date>
<is_ms_shipped>1</is_ms_shipped>
<is_published>0</is_published>
<is_schema_published>0</is_schema_published>
</row>
The call to /row/* retrieves all nodes below <row> as derived table. The rest is rather easy XQuery.

SQL Performance issue when using CASE

I have a SP that returns quite a lot of data every second and is shown in a grid. I'm now trying to reduce bandwidth and thinking of returning only columns currently shown in my grid.
This is of course simplified and minimized but basically what I had was the following SP:
SELECT
[Animals].[AnimalID] AS [AnimalID],
[Animals].[name] AS [AnimalName],
[Foods].[DisplayName] AS [Food],
[Animals].[Age] AS [AnimalAge],
[Animals].[AmountOfFood] AS [AmountOfFood]
What I’m currently trying is to pass a TVP of the names of the fields (#fields) currently shown on the grid and returning only required fields as such:
SELECT
[Animals].[AnimalID] AS [AnimalID],
[Animals].[name] AS [AnimalName],
CASE
WHEN ('Food' in (select * from #fields))
THEN [Foods].[DisplayName]
END AS [Food],
CASE
WHEN ('AnimalAge' in (select * from #fields))
THEN [Animals].[Age]
END AS [AnimalAge],
CASE
WHEN ('AmountOfFood' in (select * from #fields))
THEN [Animals].[AmountOfFood]
END AS [AmountOfFood]
The problem I'm facing is that (as could be expected) my SP went from taking ~200 ms to taking ~1 sec
Is there any way to maybe rewrite this so that it doesn’t kill us?
My kingdom for a foreach!!!
In SQL Server, you can also do this with dynamic SQL. Something like:
declare #sql nvarchar(max);
select #sql = (select ', '+
(case when FieldName = 'Food' then 'Foods.DisplayName'
when FieldName = 'AnimalAge' then 'Animals.Age'
. . .
end)
from #fields
for xml path ('')
);
select #sql = 'select [Animals].[AnimalID] AS [AnimalID], [Animals].[name] AS [AnimalName]'+#sql+RESTOFQUERY;
exec(#sql);
I'd try to convert the stored procedure into Table-Valued function, and make your grid select only required columns from it.
So your function would still select
SELECT
[Animals].[AnimalID] AS [AnimalID],
[Animals].[name] AS [AnimalName],
[Foods].[DisplayName] AS [Food],
[Animals].[Age] AS [AnimalAge],
[Animals].[AmountOfFood] AS [AmountOfFood]
If the client only selected for example select * AnimalID, Age from myfunction(..), only these columns would be transferred to the client.

Check if a value exists in a collection stored in XML data type column

I have an XML data type column called "tags".
In that, I am storing a collection, like so:
<ArrayOfString>
<string>personal</string>
<string>travel</string>
<string>gadgets</string>
<string>parenting</string>
</ArrayOfString>
I want to select all the rows, that have one of the values that I am looking for: for example, I want to select all rows in the table that have a tag "travel".
I know that this works, if I know the index of the value I am looking for:
select * from posts
where tags.value('(/ArrayOfString/string)[1]', 'nvarchar(1000)') = 'travel'
but this query works only if the tag "travel" is the 2nd item in the nodes. How do I check if a value exists, irrespective of the position it is in?
select *
from tags
where tags.exist('/ArrayOfString/string[. = "travel"]') = 1
Or like this if you want to check against a variable.
declare #Val varchar(10)
set #Val = 'travel'
select *
from tags
where tags.exist('/ArrayOfString/string[. = sql:variable("#Val")]') = 1
You can try something like this:
SELECT
*
FROM
dbo.Posts
WHERE
tags.exist('/ArrayOfString/string/text()[. = "travel"]') = 1
This will list all the rows that have "travel" in one of the strings in your XML

How can I return the content of an XML field as a recordset?

Say I've got this table (SQL Server 2005):
Id => integer
MyField => XML
Id MyField
1 < Object>< Type>AAA< /Type>< Value>10< /Value>< /Object>< Object>< Type>BBB< /Type><Value>20< /Value>< /Object>
2 < Object>< Type>AAA< /Type>< Value>15< /Value>< /Object>
3 < Object>< Type>AAA< /Type>< Value>20< /Value>< /Object>< Object>< Type>BBB< /Type>< Value>30< /Value>< /Object>
I need a TSQL query which would return something like this:
Id AAA BBB
1 10 20
2 15 NULL
3 20 30
Note that I won't know if advance how many 'Type' (eg AAA, BBB, CCC,DDD, etc.) there will be in the xml string.
You will need to use the XML querying in sql server to do that.
somethings like
select id, MyField.query('/Object/Type[.="AAA"]/Value') as AAA, MyField.query('/Object/Type[.="BBB"]/Value) AS BBB
not sure if that's 100% correct xquery syntax, but it's going to be something like that.
One possible option is to use the XMLDataDocument. Using this class you can retrieve the data as XML load it into the XmlDataDocument and then use the Dataset property to access it as if it were a standard dataset.
You'll need to use CROSS APPLY. Here's an example based on your request:
declare #y table (rowid int, xmlblock xml)
insert into #y values(1,'<Object><Type>AAA</Type><Value>10</Value></Object><Object><Type>BBB</Type><Value>20</Value></Object>')
insert into #y values(2,'<Object><Type>AAA</Type><Value>15</Value></Object>')
insert into #y values(3,'<Object><Type>AAA</Type><Value>20</Value></Object><Object><Type>BBB</Type><Value>30</Value></Object>')
select y.rowid, t.b.value('Type[1]', 'nvarchar(5)'), t.b.value('Value[1]', 'int')
from #y y CROSS APPLY XmlBlock.nodes('//Object') t(b)
Oh, and your example XML is invalid, the first row is missing the opening Value element for Type BBB.