Expanding list from SQL column with XML datatype - sql

Given a table with an XML column, how can I expand a list stored therein, into a relational rowset?
Here is an example of a similar table.
DECLARE #test TABLE
(
id int,
strings xml
)
insert into #test (id, strings)
select 1, '<ArrayOfString><string>Alpha</string><string>Bravo</string><string>Charlie</string></ArrayOfString>' union
select 2, '<ArrayOfString><string>Bravo</string><string>Delta</string></ArrayOfString>'
I would like to obtain a rowset like this.
id string
-- ------
1 Alpha
1 Bravo
1 Charlie
2 Bravo
2 Delta
I have figured out that I can use the nodes method to output the XML value for each id like this
select id, R.strings.query('/ArrayOfString/string') as string
from #test cross apply strings.nodes('/ArrayOfString/string') as R(strings)
returning
id string
-- ------
1 <string>Alpha</string><string>Bravo</string><string>Charlie</string>
1 <string>Alpha</string><string>Bravo</string><string>Charlie</string>
1 <string>Alpha</string><string>Bravo</string><string>Charlie</string>
2 <string>Bravo</string><string>Delta</string>
2 <string>Bravo</string><string>Delta</string>
and that I can append [1] to the query to get only the first string
select id, R.strings.query('/ArrayOfString/string[1]') as string
from #test cross apply strings.nodes('/ArrayOfString/string') as R(strings)
but that still leaves me with this
id string
-- ------
1 <string>Alpha</string>
1 <string>Alpha</string>
1 <string>Alpha</string>
2 <string>Bravo</string>
2 <string>Bravo</string>
Additionally, if I change the R.strings.query to R.strings.value to get rid of the tags, I get an error; XQuery [#test.strings.value()]: 'value()' requires a singleton (or empty sequence), found operand of type 'xdt:untypedAtomic *'.
Can anyone point out what I'm missing in order to get each subsequent string value for each id on subsequent rows, and why the value() method is giving me that error?

Here is how you can do it using a cross apply and the xml methods value and nodes:
select
id,
s2.value('.', 'varchar(max)') as string
from #test
cross apply strings.nodes('/*/string') t(s2)
You were getting the error because you were missing the () around the xPath:
select id,
R.strings.value('(/ArrayOfString/string)[1]','varchar(max)') as string
from #test
cross apply strings.nodes('/ArrayOfString/string') as R(strings)
The problem with your script is that you are always selecting the first string. Using the '.' you will get the contents of the XML element string.

Maybe you could try using the string() function instead of the value() function? This should return the actual value of the node without the xml.
Here is the msdn page - http://msdn.microsoft.com/en-us/library/ms188692.aspx
Really hope that helps. :)

Related

How to get string from value

I have an Customer_value column.
The column contains values like:
DAL123245,HC.533675,ABC.01232423
HC.3425364,ABC.045367544,DAL4346456
HC.35344,ABC.03543645754,ABC.023534454,DAL.4356433
ABC.043534553,HC.3453643,ABC.05746343
What I am trying to do is get the number after the first "ABC.0" string.
For example, this is what I would like to get:
1232423
5367544
3543645754
43534553
this is what I tried:
Substring(customer_value,charindex('ABC.', customer_value) + 5, len(customer_value)) as dataneeded
The issue that I got is for 1 and 2 I got that right data as needed, but for 3 and 4, because there are multiple ABC so it gave me everything after the first ABC.
How can I get the number after the first ABC. only?
Thank you so much
Just another option is to use a bit of JSON to parse and preserve the sequence in concert with a CROSS APPLY
Note: Use OUTER APPLY to see NULL values
Example
Select NewVal = replace(Value,'ABC.0','')
From YourTable A
Cross Apply (
Select Top 1 *
From OpenJSON( '["'+replace(string_escape(customer_value,'json'),',','","')+'"]' )
Where Value like 'ABC.0%'
Order by [key]
) B
Results
NewVal
1232423
45367544
3543645754
43534553
On the assumption you are using SQL Server (given your use of charindex()/substring()/len()) you can use apply to calculate the starting position and then find the next occurence utilising the start position optional parameter of charindex, then get the substring between the values.
select Substring(customer_value, p1.v, Abs(p2.v-p1.v)) as dataneeded
from t
cross apply(values(charindex('ABC.', customer_value)+5))p1(v)
cross apply(values(charindex(',', customer_value,p1.v)))p2(v)

How to fetch only a part of string

I have a column which has inconsistent data. The column named ID and it can have values such as
0897546321
ABC,0876455321
ABC,XYZ,0873647773
ABC,
99756
test only
The SQL query should fetch only Ids which are of 10 digit in length, should begin with a 08 , should be not null and should not contain all characters. And for those values, which have both digits and characters such as ABC,XYZ,0873647773, it should only fetch the 0873647773 . In these kind of values, nothing is fixed, in place of ABC, XYZ , it can be anything and can be of any length.
The column Id is of varchar type.
My try: I tried the following query
select id
from table
where id is not null
and id not like '%[^0-9]%'
and id like '[08]%[0-9]'
and len(id)=10
I am still not sure how should I deal with values like ABC,XYZ,0873647773
P.S - I have no control over the database. I can't change its values.
SQL Server generally has poor support regular expressions, but in this case a judicious use of PATINDEX is viable:
SELECT SUBSTRING(id, PATINDEX('%,08[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9],%', ',' + id + ','), 10) AS number
FROM yourTable
WHERE ',' + id + ',' LIKE '%,08[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9],%';
Demo
If you normalise your data, and split the delimited data into parts, you can achieve this some what more easily:
SELECT SS.value
FROM dbo.YourTable YT
CROSS APPLY STRING_SPLIT(YT.YourColumn,',') SS
WHERE LEN(SS.value) = 10
AND SS.value NOT LIKE '%[^0-9]%';
If you're on an older version of SQL Server, you'll have to use an alternative String Splitter method (such as a XML splitter or user defined inline table-value function); there are plenty of examples on these already on Stack Overflow.
db<>fiddle

How to get maximum value of a specific part of strings?

I have below records
Id Title
500006 FS/97/98/037
500007 FS/97/04/035
500008 FS/97/01/036
500009 FS/97/104/040
I should split Title field and get 4th part of text and return maximum value. In this example my query should return 040 or 40.
select max(cast(right(Title, charindex('/', reverse(Title) + '/') - 1) as int))
from your_table
SQLFiddle demo
You can use PARSENAME function since you always have 4 parts(confirmed in comments section)
select max(cast(parsename(replace(Title,'/','.'),1) as int))
from yourtable
If you want to split the data in the Title column and get the part from the splitted text by position, you may try with one JSON-based approach with a simple string transformation. You need to transform the data in the Title column into a valid JSON array (FS/97/98/037 into ["FS","97","08","037"]) and after that to parse thе data with OPENJSON(). The result from OPENJSON() (using default schema and parsing JSON array) is a table with columns key, value and type, and the key column holds the index of the items in the JSON array:
Note, that using STRING_SPLIT() is not an option here, because the order of the returned rows is not guaranteed.
Table:
CREATE TABLE Data (
Id varchar(6),
Title varchar(50)
)
INSERT INTO Data
(Id, Title)
VALUES
('500006', 'FS/97/98/037'),
('500007', 'FS/97/04/035'),
('500008', 'FS/97/01/036'),
('500009', 'FS/97/104/040')
Statement:
SELECT MAX(j.[value])
FROM Data d
CROSS APPLY OPENJSON(CONCAT('["', REPLACE(d.Title, '/', '","'), '"]')) j
WHERE (j.[key] + 1) = 4
If you data has fixed format with 4 parts, even this approach may help:
SELECT MAX(PARSENAME(REPLACE(Title, '/', '.'), 1))
FROM Data
You can also try the below query.
SELECT Top 1
CAST('<x>' + REPLACE(Title,'/','</x><x>') + '</x>' AS XML).value('/x[4]','int') as Value
from Data
order by 1 desc
You can find the live demo Here.

how to convert the output of sub query into numeric

select rptName
from RptTable
where rpt_id in (
select LEFT(Reports, NULLIF(LEN(Reports)-1,-1))
from repoAccess1
where uid = 'VIKRAM'
)
this is my sql query In which i have use the sub query to access selected field
in this sub query returns
select LEFT(Reports, NULLIF(LEN(Reports)-1,-1))
from repoAccess1
where uid = 'VIKRAM'
Returns
1,2
that means the query should be like
select rptName
from RptTable where rpt_id in (1,2)
But i m getting this error
Msg 8114, Level 16, State 5, Line 1
Error converting data type nvarchar to numeric.
could anyone tell me ow to modify to get exact ans
It's a little hard to tell without the concrete table definitions, but I'm pretty sure you're trying to compare different data types to each other. If this is the case you can make use of the CAST or the CONVERT function, for example:
SELECT
[rptName]
FROM [RptTable]
WHERE [rpt_id] IN
(
SELECT
CONVERT(int, LEFT([Reports], NULLIF(LEN([Reports]) - 1, -1)))
FROM [repoAccess1]
WHERE [uid] = 'VIKRAM'
)
UPDATE: Since you have updated your question: The LEFT function returns results of either varchar or nvarchar data type. So the resulting query would be
SELECT
[rptName]
FROM [RptTable]
WHERE [rpt_id] IN('1', '2')
Please note the apostrophes (is this the correct term?) around the values. Since [rpt_id] seems to be of data type int the values cannot implicitly be converted. And that's where the aforementioned CAST or CONVERT come into play.
If I understand correctly, the subquery is returning a single row with a value of '1,2'. This is not a number, hence the error.
Before continuing, let me emphasize that storing values in comma delimited string is not the SQL-way of doing things. You should have one row per id, with proper types and foreign keys defined.
That said, sometimes we are stuck with other people's really bad design decisions. If this is the case, you can use LIKE:
select rptName
from RptTable r
where exists (select 1
from repoAccess1 a
where a.uid = 'VIKRAM' and
',' + a.reports + ',' like '%,' + cast(r.rpt_id as varchar(255)) + ',%'
);
select rptName
from RptTable
where rpt_id in (
select CAST(LEFT(Reports, NULLIF(LEN(Reports)-1,-1)) AS INT) as Val
from repoAccess1
where uid = 'VIKRAM'
)
Your query would work fine when (LEFT(Reports, NULLIF(LEN(Reports)-1,-1)) ) returns either 1 or 2 since SQL Server implicitly converts the varchar value to numeric.
It seems there might be a data issue. One of the data returned by LEFT function is non-numeric. In order to find that particular record you can use isnumeric function. Try like this,
SELECT rptName
FROM RptTable
WHERE rpt_id IN (
SELECT LEFT(Reports, NULLIF(LEN(Reports) - 1, - 1))
FROM repoAccess1
WHERE uid = 'VIKRAM'
AND ISNUMERIC(LEFT(Reports, NULLIF(LEN(Reports) - 1, - 1))) = 1
)

Return string of description given string of IDs (separated by commas)

I have one table A that has a column C and a lookup table (lookup) that provides a description given an ID.
Here the setup:
table A with column C values:
1,2,3
2,3,4
table lookup:
1, 'This'
2, 'is'
3, 'tricky'
4, 'SQL'
Provide a SQL (SQL Server 2005) statement that returns the following strings:
Input: 1,2,3 Output: 'This','Is','tricky'
Input: 2,3,4 Output: 'Is','tricky','SQL'
basically turning the string of IDs (from an input table A) into a string of descriptions
The Samples that come with SQL Server 2005 include a CLR function called Split(). It's the best way of splitting comma-separated lists like this by far.
Suppose you have a table called inputs, with a column called input.
I forget what the particular outputs of dbo.Split() are... so work with me here. Let's call the fields id and val, where id tells us which entry it is in the list.
WITH
separated AS (
SELECT i.input, s.id, s.val
FROM
dbo.inputs AS i
CROSS APPLY
dbo.Split(i.input) AS s
)
, converted AS (
SELECT s.input, s.id, m.string
FROM
separated AS s
JOIN
dbo.mapping AS m
ON m.number = CAST(s.val AS varchar(5))
)
SELECT c.input, (SELECT string + ' ' FROM converted AS c2 WHERE c2.input = c.input ORDER BY id FOR XML PATH('')) AS converted_string
FROM converted AS c
GROUP BY c.input;