String aggregation using JSON in SQL Server 2016 - sql

I would like to format a json string '[{"_":7},{"_":13},{"_":17}]' as '[7,13,17]'
Tried with REPLACE Method in TSQL. I have to use REPLACE method three times to get the desire result.
SELECT REPLACE(REPLACE(REPLACE('[{"_":7},{"_":13},{"_":17}]','},{"_":',', '),'{"_":',''),'}','')
is there a better way to do that?
I am using SQL Server 2016.
After some comments for this post, This my actual issue.
I have some customer data. Customer Table
CustomerId | Name
1 ABC
2 XYZ
3 EFG
each customer has some area of interest. Customer Area of Interest
CustomerAreaInterestId | FK_CustomerId | FK_AreaOfInterestId
1 1 2
2 1 3
3 1 5
4 2 1
5 2 2
6 3 3
7 3 4
Area of interest table
AreaOfInterestId | Description
1 Interest1
2 Interest2
3 Interest3
4 Interest4
5 Interest5
In the final result set, I have to include area of interest id's as an array of value
[
{
"CustomerName": "ABC",
"AreaofInterest": "[2,3,5]"
},
{
"CustomerName": "XYZ",
"AreaofInterest": "[1,2]"
},
{
"CustomerName": "EFG",
"AreaofInterest": "[3,4]"
}
]
The result consists with some other data’s as well. I have omitted for the code brevity.

Short Version
Cast the numeric field to text before trying to aggregate it
From the comments, it looks like the real question is how to use JSON to aggregate strings in SQL Server 2016, as shown in this answer.
SELECT
JSON_VALUE(
REPLACE(
(SELECT _ = someField FROM someTable FOR JSON PATH)
,'"},{"_":"',', '),'$[0]._'
)
or, rewritten for clarity :
SELECT
JSON_VALUE( REPLACE(
(SELECT _ = someField
FROM someTable
FOR JSON PATH)
,'"},{"_":"',', ')
,'$[0]._')
That query works only with string fields. One needs to understand what it does before it can be adopted to other types.
The inner query generates a JSON string from a field's values, eg '[{"_":"value1"},{"_":"value2"}]'.
REPLACE replaces the quotes and separators between objects, changing that array of objects to '[{"_":"value1,value2"}]'. That's a single object in an array, whose single attribute is a comma-separated string.
JSON_VALUE(...,,'$[0]._') extracts the _ attribute of that single array item.
That trick can't be used with numeric values because they don't have quotes. The solution is to cast them to text first:
SELECT
JSON_VALUE( REPLACE(
(SELECT _ = CAST(someNumber as nvarchar(20))
FROM someTable
FOR JSON PATH)
,'"},{"_":"',', ')
,'$[0]._')
Eg :
declare #t table (id int)
insert into #t
values
(7),
(13),
(17)
SELECT
JSON_VALUE( REPLACE(
(SELECT _ = cast(ID as nvarchar(20))
FROM #t
FOR JSON PATH)
,'"},{"_":"',', '),'$[0]._')
The only change from the original query is the cast clause.
This produces :
7, 13, 17
This conversion is localized so care must be taken with decimals and dates, to avoid producing unexpected results, eg 38,5, 40,1 instead of 38.5, 40.1.
PS: That's no different than the XML technique, except STUFF is used there to cut off the leading separator. That technique also needs casting numbers to text, eg :
SELECT STUFF(
( SELECT N', ' + cast(ID as nvarchar(20))
FROM #t FOR XML PATH(''),TYPE)
.value('text()[1]','nvarchar(max)'),
1,2,N'')

If you want to use only JSON functions (not string-based approach), the next example may help:
DECLARE #json nvarchar(max) = N'[{"_":7},{"_":13},{"_":17}]'
DECLARE #output nvarchar(max) = N'[]'
SELECT #output = JSON_MODIFY(#output, 'append $', j.item)
FROM OPENJSON(#json) WITH (item int '$."_"') j
SELECT #output AS [Result]
Result:
Result
[7,13,17]
Of course, the approach based on string aggregation is also a possible solution:
DECLARE #json nvarchar(max) = N'[{"_":7},{"_":13},{"_":17}]'
SELECT CONCAT(
N'[',
STUFF(
(
SELECT CONCAT(N',', j.item)
FROM OPENJSON(#json) WITH (item int '$."_"') j
FOR XML PATH('')
), 1, 1, N''
),
N']'
)

Yes you could do it with only 2 replace :
SELECT REPLACE(REPLACE('[{"_":7},{"_":13},{"_":17}]','{"_":',''),'}','')
DEMO HERE
Except if you really need a space after coma which is not what you asked to be honest.

Related

JSON Array in SQL - Extracting multiple values from JSON array

I have a JSON column [gca] that looks like this
[{
"i":"https://some.image.URL 1",
"u":"https://some.product.url",
"n":"Product 1 Name",
"q":"1",
"sk":"sku number 1",
"st":"$499.99"
},
{
"i":"https://some.image.URL 2",
"u":"https://some.product.url",
"n":"Product 2 Name",
"q":"1",
"sk":"sku number 2",
"st":"$499.99"
}]
I want to extract values specific to position. For example:
JSON_VALUE ([gca], '$[0].i') + ', ' + JSON_VALUE ([gca], '$[1].i')
So the result would be a string
image url 1, image url 2
I tried the cross apply solution from this answer, but I get this error:
JSON text is not properly formatted. Unexpected character 'h' is found at position 0
Expected results
-- https://i.stack.imgur.com/UaRjQ.png
Your statement is correct and based on the JSON data in the question, the reason for this "... JSON text is not properly formatted. Unexpected character 'h' is found at position 0' error ... " error is somewhere else.
Your JSON text is a valid JSON array, so you have two possible approaches to get your expected results:
if this JSON array has always two items, you should use JSON_VALUE() to access each item
if the count of the items is not known, you should use OPENJSON() with additional CROSS APPLY operator and string aggregation function.
Table:
CREATE TABLE #Data (
[gca] nvarchar(max)
)
INSERT INTO #Data
([gca])
VALUES
(N'[{"i":"https://some.image.URL 1","u":"https://some.product.url","n":"Product 1 Name","q":"1","sk":"sku number 1","st":"$499.99"},{"i":"https://some.image.URL 2","u":"https://some.product.url","n":"Product 2 Name","q":"1","sk":"sku number 2","st":"$499.99"}]'),
(N'[{"i":"https://some.image.URL 1","u":"https://some.product.url","n":"Product 1 Name","q":"1","sk":"sku number 1","st":"$499.99"},{"i":"https://some.image.URL 2","u":"https://some.product.url","n":"Product 2 Name","q":"1","sk":"sku number 2","st":"$499.99"}]')
Statements:
-- For fixed structure with two items
SELECT JSON_VALUE ([gca], '$[0].i') + ', ' + JSON_VALUE ([gca], '$[1].i') AS [http]
FROM #Data
-- For JSON array with multiple items and SQL Server 2017+
SELECT j.*
FROM #Data d
CROSS APPLY (
SELECT STRING_AGG([http], ',') AS [http]
FROM OPENJSON(d.[gca]) WITH ([http] varchar(max) '$.i')
) j
-- For JSON array with multiple items
SELECT STUFF(j.[http], 1, 1, N'') AS [http]
FROM #Data d
CROSS APPLY (
SELECT CONCAT(',', [http])
FROM OPENJSON(d.[gca]) WITH ([http] varchar(max) '$.i')
FOR XML PATH('')
) j([http])
Output:
-------------------------------------------------
http
-------------------------------------------------
https://some.image.URL 1,https://some.image.URL 2
https://some.image.URL 1,https://some.image.URL 2
Note, that JSON support was intoduced in SQL Server 2016 and STRING_AGG() was introduced in SQL Server 2017. For string aggregation in earlier versions use FOR XML PATH.

How to separate values with Parse name in SQL Server

I have a value (500 , 850 , 65.5) as GivenUnitPrice and I want to separate these by making separate columns by PARSENAME
I have tried like this
PARSENAME(GivenUnitPrice, 3) as DB,
PARSENAME(GivenUnitPrice, 2) as Owner,
PARSENAME(GivenUnitPrice, 1) as Object
and the result is
DB | Owner | Object
NULL | 500 , 850 , 65 | 5
Seems you want to seperate the characters by commas but Parsename() function splits by dot character( e.g. decimal number 65.5 is also splitted as if seperate integers ), so yields wrong results for your case. It's better to use replace(),substring() and charindex() together as :
with t as
(
select replace(replace('500 , 850 , 65.5',')',''),'(','') as GivenUnitPrice
), t2 as
(
select substring(GivenUnitPrice,1,charindex(',',GivenUnitPrice)-1) as db,
substring(GivenUnitPrice,charindex(',',GivenUnitPrice)+1,len(GivenUnitPrice)) as owner_object
from t
)
select db,
substring(owner_object,1,charindex(',',owner_object)-1) as owner,
substring(owner_object,charindex(',',owner_object)+1,len(owner_object)) as object
from t2;
db owner object
500 850 65.5
Demo
DECLARE #GivenUnitPrice VARCHAR(100)= '500 , 850 , 65.5'
SELECT PARSENAME(replacE(#GivenUnitPrice,',','.'),4) as DB,PARSENAME(replacE(#GivenUnitPrice,',','.'),3) as Owner,PARSENAME(replacE(#GivenUnitPrice,',','.'),2)+ '.'+PARSENAME(#GivenUnitPrice,1) AS OBJECT
You can try this....
DECLARE #UnitPrice VARCHAR(100)
SET #UnitPrice= '500 , 850 , 65.5'
SELECT PARSENAME(REPLACE(#UnitPrice,',','.'),4) as DB,
PARSENAME(REPLACE(#UnitPrice,',','.'),3) as Owner,
PARSENAME(REPLACE(#UnitPrice, ',', '.'), 2) AS OBJECT,
PARSENAME(REPLACE(#UnitPrice, '', '.'), 1) AS OBJECT
PARSENAME basically consider .(Dot) as a delimeter in the string. In your input string, there are only one .(Dot) available at the end and as a result you are getting values only at Owner and Object column. IF you want to use PARSENAME for this string, please replace commas with .(Dot) first and then apply PARSENAME as below-
DECLARE #ObjectName NVARCHAR(1000);
SET #ObjectName = '500 , 850 , 65.5';
SET #ObjectName = REPLACE(#ObjectName, ',', '.');
SELECT PARSENAME(#ObjectName, 4) AS Server,
PARSENAME(#ObjectName, 3) AS DB,
PARSENAME(#ObjectName, 2) AS Owner,
PARSENAME(#ObjectName, 1) AS Object;

find the end point of a pattern in SQL server

There is a comma separated string in a column which looks like
test=1,value=2.2,system=321
I want to extract value out from the string. I can use select PatIndex('%value=%',columnName) then use left, but this only find the beginning of the patindex.
How to identify the end of pattern value=%, so we can extract the value out?
Chain a few SUBSTRING with CHARINDEX and your PATHINDEX.
DECLARE #text VARCHAR(100) = 'test=1,value=2.21954,system=321'
SELECT
Original = #text,
Parsed = SUBSTRING( -- Get a portion of the original value
#text,
PATINDEX('%value=%',#text) + 6, -- ... starting from the 'value=' (without the 'value=')
-1 + CHARINDEX( -- ... and get as many characters until the first comma
',',
SUBSTRING( -- ... (find the comma starting from the 'value=' onwards)
#text,
PATINDEX('%value=%',#text) + 6,
100)))
Result:
Original Parsed
test=1,value=2.2,system=321 2.2
Note that the CHARINDEX will fail if there is no comma after your value=. You can filter this with a WHERE.
I strongly suggest to store your values already split on a proper table and you wont have to deal with string nightmares like this.
You can use CHARINDEX with starting position to find the first comma after the pattern. CROSS APPLY is used to keep the query easier to read:
WITH tests(str) AS (
SELECT 'test=1,value=2.2,system=321'
)
SELECT str, substring(str, pos1, pos2 - pos1) AS match
FROM tests
CROSS APPLY (SELECT PATINDEX('%value=%', str) + 6) AS ca1(pos1)
CROSS APPLY (SELECT CHARINDEX(',', str, pos1 + 1)) AS ca2(pos2)
-- 2.2
First of all, don't store denormalized data in this way, if you want to query them. SQL, the language, isn't good at string manipulation. Parsing and splitting strings can't take advantage of indexes either, which means any query that tried to find eg all records that refer to system 321 would have to scan and parse all rows.
SQL Server 2016 and JSON
SQL Server 2016 added suppor for JSON and the STRING_SPLIT function. Earlier versions already provided the XML type. It's better to store complex values as JSON or XML instead of trying to parse the string.
One option is to convert the string into a JSON object and retrieve the value contents, eg :
DECLARE #text VARCHAR(100) = 'test=1,value=2.2,system=321'
select json_value('{"' + replace(replace(#text,',','","'),'=','":"') + '"}','$.value')
This returns 2.2.
The replacements converted the original string into
{"test":"1","value":"2.2","system":"321"}
JSON_VALUE(#json,'$.') will return the value property of that object
Earlier SQL Server versions
In earlier SQL Server version, you can convert that string into an XML element the same way and use XQuery :
DECLARE #text VARCHAR(100) = 'test=1,value=2.2,system=321';
declare #xml varchar(100)='<r ' + replace(replace(#text,',','" '),'=',' ="') + '" />';
select #xml
select cast(#xml as xml).value('(/r[1]/#value)','varchar(20)')
In this case #xml contains :
<r test ="1" value ="2.2" system ="321" />
The query result is 2.2
You can try like following.
DECLARE #xml AS XML
SELECT #xml = Cast(( '<X>' + Replace(txt, ',', '</X><X>') + '</X>' ) AS XML)
FROM (VALUES ('test=1,value=2.2,system=321')) v(txt)
SELECT LEFT(value, Charindex('=', value) - 1) AS LeftPart,
RIGHT(value, Charindex('=', Reverse(value)) - 1) AS RightPart
FROM (SELECT n.value('.', 'varchar(100)') AS value
FROM #xml.nodes('X') AS T(n))T
Online Demo
Output
+----------+-----------+
| LeftPart | RightPart |
+----------+-----------+
| test | 1 |
+----------+-----------+
| value | 2.2 |
+----------+-----------+
| system | 321 |
+----------+-----------+
You can try the below query if you are using SQL Server (2016 or above)
SELECT RIGHT(Value,CHARINDEX('=',REVERSE(Value))-1) FROM YourTableName
CROSS APPLY STRING_SPLIT ( ColumnName , ',' )
WHERE Value Like 'Value=%'

Return json list of values in SQL Server 2016 using data from 2 columns

I have a database with 2 columns
A B
-- --
X 1995
Y 2005
C 1962
D 2003
I'm trying to create a SQL statement that will take a string of comma delimited values and return a json list of values in B where any value in the string is in A
so if the comma delimited string was 'X,C' the json list would be [1995,1962]
I've been using json path to try this, but I can't get it exactly like I want it and I've been spinning my wheels for too long
This is what I've tried:
Select mt.B as json_list_b_values
From [dbo].[myTable] mt
Where mt.A in (Select value From String_Split('X,C', ',')) for json path
This is the ouput:
[ {"json_list_b_values":"1995"}, {"json_list_b_values":"1962"} ]
As you're on 2016 you can't use STRING_AGG, but you can use the old tried and tested FOR XML PATH and STUFF method:
DECLARE #list varchar(8000) = 'X,C';
WITH VTE AS(
SELECT *
FROM(VALUES('X',1995),
('Y',2005),
('C',1962),
('D',2003)) V(A,B))
SELECT '[' + STUFF((SELECT CONCAT(',',V.B)
FROM VTE V
CROSS APPLY STRING_SPLIT(#list,',') SS
WHERE SS.[value] = V.A
FOR XML PATH('')),1,1,'') + ']';
I am no SQL expert. So please bear with me.
Step 1 - Handle the CSV input
You are already doing this by using the IN clause. I would store these results in a table variable
Step 2 - Convert the query results to a simple JSON array
I can think of the function STRING_AGG(). This will concatenate the rows into a flat string.
E.g. Join the FirstName column into a comma delimited string
SELECT STRING_AGG ( ISNULL(FirstName,'N/A'), ',') AS csv
FROM Person.Person;
Will produce the following
John,N/A,Mike,Peter,N/A,N/A,Alice,Bob
Code snippet based on your example
I am using a table variable #result1 to hold the result from Step 1.
SELECT '[ "' + string_agg( json_list_b_values, '", "') + '" ]' FROM #result1
MSDN reference for STRING_AGG()
https://learn.microsoft.com/en-us/sql/t-sql/functions/string-agg-transact-sql?view=sql-server-2017
CORRECTION
The function STRING_AGG() is available in SQL 2017.

SQL Server - Select column that contains query string and split values into anothers 'columns'

I need to do a select in a column that contains a query string like:
user_id=300&company_id=201503&status=WAITING OPERATION&count=1
I want to perform a select and break each value in a new column, something like:
user_id | company_id | status | count
300 | 201503 | WAITING OPERATION | 1
How can i do it in SQL Server without use procs?
I've tried a function:
CREATE FUNCTION [xpto].[SplitGriswold]
(
#List NVARCHAR(MAX),
#Delim1 NCHAR(1),
#Delim2 NCHAR(1)
)
RETURNS TABLE
AS
RETURN
(
SELECT
Val1 = PARSENAME(Value,2),
Val2 = PARSENAME(Value,1)
FROM
(
SELECT REPLACE(Value, #Delim2, '&') FROM
(
SELECT LTRIM(RTRIM(SUBSTRING(#List, [Number],
CHARINDEX(#Delim1, #List + #Delim1, [Number]) - [Number])))
FROM (SELECT Number = ROW_NUMBER() OVER (ORDER BY name)
FROM sys.all_objects) AS x
WHERE Number <= LEN(#List)
AND SUBSTRING(#Delim1 + #List, [Number], LEN(#Delim1)) = #Delim1
) AS y(Value)
) AS z(Value)
);
GO
Execution:
select QueryString
from User.Log
CROSS APPLY notifier.SplitGriswold(REPLACE(QueryString, ' ', N'ŏ'), N'ŏ', '&') AS t;
But it returns me only one column with all inside:
QueryString
user_id=300&company_id=201503&status=WAITING OPERATION&count=1
Thanks in advance.
I've had to do this many times before, and you're in luck! Since you only have 3 delimiters per string, and that number is fixed, you can use SQL Server's PARSENAME function to do it. That's far less ugly than the best alternative (using the XML parsing stuff). Try this (untested) query (replace TABLE_NAME and COLUMN_NAME with the appropriate names):
SELECT
PARSENAME(REPLACE(COLUMN_NAME,'&','.'),1) AS 'User',
PARSENAME(REPLACE(COLUMN_NAME,'&','.'),2) AS 'Company_ID',
PARSENAME(REPLACE(COLUMN_NAME,'&','.'),3) AS 'Status',
PARSENAME(REPLACE(COLUMN_NAME,'&','.'),4) AS 'Count',
FROM TABLE_NAME
That'll get you the results in the form "user_id=300", which is far and away the hard part of what you want. I'll leave it to you to do the easy part (drop the stuff before the "=" sign).
NOTE: I can't remember if PARSENAME will freak out over the illegal name character (the "=" sign). If it does, simply nest another REPLACE in there to turn it into something else, like an underscore.
You need to use SQL SUBSTRING as part of your select statement. You would first need to build the first row, then use a UNION to return the second row.