Querying json key name in SQL Server - sql-server

Given json like this...
{"setting1":"A","setting2":"B","setting3":"C"}
I would like to see results like...
+----------+-------+
| name | value |
+----------+-------+
| setting1 | A |
| setting2 | B |
| setting3 | C |
+----------+-------+
My struggle is I'm trying to find out how to extract the key's name (i.e., "setting1", "setting2", "setting3", etc.)
I could do something like the following query, but I don't know how many settings there will be and what their names will be, so I'd like something more dynamic.
SELECT
B.name,
B.value
FROM OPENJSON(#json) WITH
(
setting1 varchar(50) '$.setting1',
setting2 varchar(50) '$.setting2',
setting3 varchar(50) '$.setting3'
) A
CROSS APPLY
(
VALUES
('setting1', A.setting1),
('setting2', A.setting2),
('setting3', A.setting3)
) B (name, value)
With XML, I could do something simple like this:
DECLARE #xml XML = '<settings><setting1>A</setting1><setting2>B</setting2><setting3>C</setting3></settings>'
SELECT
A.setting.value('local-name(.)', 'VARCHAR(50)') name,
A.setting.value('.', 'VARCHAR(50)') value
FROM #xml.nodes('settings/*') A (setting)
Any way to do something similar with SQL Server's json functionality?

Aaron Bertrand has written about json key value in Advanced JSON Techniques
SELECT x.[Key], x.[Value]
FROM OPENJSON(#Json, '$') AS x;
Return
Key Value
------------------
setting1 A
setting2 B
setting3 C

Option Using a Table
Declare #YourTable table (ID int,JSON_String varchar(max))
Insert Into #YourTable values
(1,'{"setting1":"A","setting2":"B","setting3":"C"}')
Select A.ID
,C.*
From #YourTable A
Cross Apply (values (try_convert(xml,replace(replace(replace(replace(replace(JSON_String,'"',''),'{','<row '),'}','"/>'),':','="'),',','" '))) ) B (XMLData)
Cross Apply (
Select Name = attr.value('local-name(.)','varchar(100)')
,Value = attr.value('.','varchar(max)')
From B.XMLData.nodes('/row') as C1(r)
Cross Apply C1.r.nodes('./#*') as C2(attr)
) C
Returns
ID Name Value
1 setting1 A
1 setting2 B
1 setting3 C
Option Using a String Variable
Declare #String varchar(max) = '{"setting1":"A","setting2":"B","setting3":"C"}'
Select C.*
From (values (try_convert(xml,replace(replace(replace(replace(replace(#String,'"',''),'{','<row '),'}','"/>'),':','="'),',','" '))) ) A (XMLData)
Cross Apply (
Select Name = attr.value('local-name(.)','varchar(100)')
,Value = attr.value('.','varchar(max)')
From A.XMLData.nodes('/row') as C1(r)
Cross Apply C1.r.nodes('./#*') as C2(attr)
) C
Returns
Name Value
setting1 A
setting2 B
setting3 C

If you are open to a TVF.
The following requires my Extract UDF. This function was created because I was tired of extracting string (patindex,charindex,left,right, etc). It is a modified tally parse which accepts two non-like delimiters.
Example
Declare #YourTable table (ID int,JSON_String varchar(max))
Insert Into #YourTable values
(1,'{"setting1":{"global":"A","type":"1"},"setting2":{"global":"B","type":"1"},"setting3":{"global":"C","type":"1"}} ')
Select A.ID
,B.Setting
,C.*
From #YourTable A
Cross Apply (
Select Setting = replace(replace(B1.RetVal,'"',''),'{','')
,B2.RetVal
From [dbo].[udf-Str-Extract](A.JSON_String,',',':{') B1
Join [dbo].[udf-Str-Extract](A.JSON_String,':{','}') B2
on B1.RetSeq=B2.RetSeq
) B
Cross Apply (
Select Name = C1.RetVal
,Value = C2.RetVal
From [dbo].[udf-Str-Extract](','+B.RetVal,',"','":') C1
Join [dbo].[udf-Str-Extract](B.RetVal+',',':"','",') C2
on C1.RetSeq=C2.RetSeq
) C
Returns
ID Setting Name Value
1 setting1 global A
1 setting1 type 1
1 setting2 global B
1 setting2 type 1
1 setting3 global C
1 setting3 type 1
The UDF if Interested
CREATE FUNCTION [dbo].[udf-Str-Extract] (#String varchar(max),#Delimiter1 varchar(100),#Delimiter2 varchar(100))
Returns Table
As
Return (
with cte1(N) As (Select 1 From (Values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N(N)),
cte2(N) As (Select Top (IsNull(DataLength(#String),0)) Row_Number() over (Order By (Select NULL)) From (Select N=1 From cte1 N1,cte1 N2,cte1 N3,cte1 N4,cte1 N5,cte1 N6) A ),
cte3(N) As (Select 1 Union All Select t.N+DataLength(#Delimiter1) From cte2 t Where Substring(#String,t.N,DataLength(#Delimiter1)) = #Delimiter1),
cte4(N,L) As (Select S.N,IsNull(NullIf(CharIndex(#Delimiter1,#String,s.N),0)-S.N,8000) From cte3 S)
Select RetSeq = Row_Number() over (Order By N)
,RetPos = N
,RetVal = left(RetVal,charindex(#Delimiter2,RetVal)-1)
From (
Select *,RetVal = Substring(#String, N, L)
From cte4
) A
Where charindex(#Delimiter2,RetVal)>1
)
/*
Max Length of String 1MM characters
Declare #String varchar(max) = 'Dear [[FirstName]] [[LastName]], ...'
Select * From [dbo].[udf-Str-Extract] (#String,'[[',']]')
*/

Related

Select from table only if all dynamic parameter values match

I have the following animal table:
id | action
------------------
duck | cuack
duck | fly
duck | swim
pelican | fly
pelican | swim
I want to create a stored procedure and pass a group of values into a single parameter:
EXEC GuessAnimalName 'cuack,fly,swim'
Result:
duck
So, if it cuacks, it flyes and it swims then it's a duck. But also:
EXEC GuessAnimalName 'fly,swim'
Result:
pelican
---
EXEC GuessAnimalName 'fly'
Result:
No results
Parameter's number is dynamic.
In order to guess the animal's name all actions provided must match or be found in animal table.
This is what I have so far:
DECLARE #animal AS TABLE
(
[id] nvarchar(8),
[action] nvarchar(16)
)
INSERT INTO #animal VALUES('duck','cuack')
INSERT INTO #animal VALUES('duck','fly')
INSERT INTO #animal VALUES('duck','swim')
INSERT INTO #animal VALUES('pelican','fly')
INSERT INTO #animal VALUES('pelican','swim')
-- Parameter simulation
DECLARE #params AS TABLE
(
[action] nvarchar(16)
)
INSERT INTO #params VALUES('cuack')
INSERT INTO #params VALUES('fly')
INSERT INTO #params VALUES('swim')
SELECT
a.[id]
FROM
#animal a
INNER JOIN
#params p
ON
a.[action] = p.[action]
GROUP BY
a.[id]
HAVING COUNT(a.[action]) IN (SELECT COUNT([action]) FROM #animal GROUP BY [id])
Which gives the result:
Result:
--------
duck
--------
pelican
It should return just duck.
convert this to a stored proc using RANK
declare #lookfor varchar(100) = 'swim,fly'
select id from
(select
id, rank() over (order by cnt desc) rank_ -- get rank based on the number of match where should be the same number of rows
from
(
SELECT
a.id, count(1) cnt -- identify how many matches
FROM
#animal a
INNER JOIN
#params p
ON
a.[action] = p.[action]
where charindex(p.action,#lookfor) > 0
group by a.id
having count(1) = (select count(1) from #animal x where a.id = x.id)) -- only get the animal with the same number of matches and rows
y)
z where rank_ = 1 -- display only with the most matches which should be the same number of rows matched
you don't need #params here
select id from
(select
id, rank() over (order by cnt desc) rank_
from
(
SELECT
a.id, count(1) cnt
FROM
#animal a
where charindex(a.action,#lookfor) > 0
group by a.id
having count(1) = (select count(1) from #animal x where a.id = x.id))
y)
z where rank_ = 1

Display rows based on column value in sql server 2008

i have column col1 and col2
Col1 col2
abc,def xyz,xyz
abc1,def1 xyz1,xyz1
i need output as below
Col1 col2
abc xyz,xyz
def xyz,xyz
abc1 xyz1,xyz1
def1 xyz1,xyz1
that is if col1 contains 2 value (abc,def) and col2 contains 2 value(xyz,xyz) then i need 4 rows. likewise col1 and col2 contains 2 values then i need 9 rows.
please help me to get the output in sql server
A little XML and and a CROSS APPLY
Option 1: Without a Split/Parse Function
Declare #YourTable table (Col1 varchar(25),col2 varchar(25))
Insert Into #YourTable values
('abc,def','xyz,xyz'),
('abc1,def1','xyz1,xyz1')
Select col1 = B.RetVal
,col2 = A.col2
From #YourTable A
Cross Apply (
Select RetSeq = Row_Number() over (Order By (Select null))
,RetVal = LTrim(RTrim(B.i.value('(./text())[1]', 'varchar(max)')))
From (Select x = Cast('<x>' + replace((Select replace(A.Col1,',','§§Split§§') as [*] For XML Path('')),'§§Split§§','</x><x>')+'</x>' as xml).query('.')) as X
Cross Apply x.nodes('x') AS B(i)
) B
Returns
col1 col2
abc xyz,xyz
def xyz,xyz
abc1 xyz1,xyz1
def1 xyz1,xyz1
Option 2: with a Split/Parse Function
Select col1 = B.RetVal
,col2 = A.col2
From #YourTable A
Cross Apply [dbo].[udf-Str-Parse](A.col1,',') B
The UDF if interested
CREATE FUNCTION [dbo].[udf-Str-Parse] (#String varchar(max),#Delimiter varchar(10))
Returns Table
As
Return (
Select RetSeq = Row_Number() over (Order By (Select null))
,RetVal = LTrim(RTrim(B.i.value('(./text())[1]', 'varchar(max)')))
From (Select x = Cast('<x>' + replace((Select replace(#String,#Delimiter,'§§Split§§') as [*] For XML Path('')),'§§Split§§','</x><x>')+'</x>' as xml).query('.')) as X
Cross Apply x.nodes('x') AS B(i)
);
--Thanks Shnugo for making this XML safe
--Select * from [dbo].[udf-Str-Parse]('Dog,Cat,House,Car',',')
--Select * from [dbo].[udf-Str-Parse]('John Cappelletti was here',' ')
--Select * from [dbo].[udf-Str-Parse]('this,is,<test>,for,< & >',',')
Here is another example:
;WITH A(Col1,col2)AS(
SELECT 'abc,def','xyz,xyz' UNION all
SELECT 'abc1,def1','xyz1,xyz1'
)
SELECT d.n,a.col2 FROM a
CROSS APPLY(VALUES(CONVERT(XML,'<n>'+REPLACE(col1,',','</n><n>')+'</n>'))) c(x)
CROSS APPLY(SELECT y.n.value('.','varchar(10)') FROM c.x.nodes('n') y(n)) d(n)
n col2
---------- ---------
abc xyz,xyz
def xyz,xyz
abc1 xyz1,xyz1
def1 xyz1,xyz1

Best method to write a recursive SQL view

I am looking for the cleanest SQL query to attain the following. Performance is not as important because my dataset is small.
Sample table:
Letter field holding: A, B, C, D, E,
Location field holding: UAE, CANADA, BOSTON, BAHRAIN, FRANCE
And I am looking for a result that lists every letter/location with letter in it combination, so the following result set:
A-UAE
A-CANADA
A-BAHRAIN
A-FRANCE
B-BOSTON
B-BAHRAIN
C-CANADA
C-FRANCE
D-CANADA
E-UAE
E-FRANCE
This is yet another solution:
DECLARE #Letter TABLE (
letter CHAR(1) PRIMARY KEY
);
DECLARE #Country TABLE (
name VARCHAR(100) PRIMARY KEY
);
INSERT INTO #Letter (letter)
VALUES ('A'), ('B'), ('C'), ('D'), ('E');
INSERT INTO #Country (name)
VALUES ('UAE'), ('CANADA'), ('BOSTON'), ('BAHRAIN'), ('FRANCE');
SELECT CONCAT(L.letter, ' - ', C.name)
FROM #Letter AS L
INNER JOIN #Country AS C
ON C.name LIKE '%' + L.letter + '%'
ORDER BY L.letter, C.name;
Result:
A - BAHRAIN
A - CANADA
A - FRANCE
A - UAE
B - BAHRAIN
B - BOSTON
C - CANADA
C - FRANCE
D - CANADA
E - FRANCE
E - UAE
Hopefull this outputs what you'd expect.
You can run this query on Stack Exchange Data: https://data.stackexchange.com/stackoverflow/query/622821
Alternatively, if performance becomes issue, you could create a seperate table which would store each country name and its' unique letters, so you could make a simple join instead of LIKEing to compare things:
DECLARE #CountrySplit TABLE (
letter CHAR(1)
, name VARCHAR(100)
, PRIMARY KEY (letter, name)
);
INSERT INTO #CountrySplit (letter, name)
SELECT DISTINCT SUBSTRING(C.name, v.number + 1, 1), C.name
FROM #Country AS C
INNER JOIN master..spt_values AS V
ON V.number < LEN(C.name)
WHERE V.type = 'P';
SELECT CONCAT(L.letter, ' - ', CS.name) AS Result
FROM #CountrySplit AS CS
INNER JOIN #Letter AS L
ON L.letter = CS.letter;
This is query on Stack Exchange Data:
https://data.stackexchange.com/stackoverflow/query/622841
Credits to this answer for string split: T-SQL Split Word into characters
With the help of a Parse/Split UDF and a Cross Apply.
I added an ID to demonstrate that this can be run for the entire table
Example
Declare #YourTable table (ID int,Letter varchar(50),Location varchar(50))
Insert Into #YourTable values
(1,'A, B, C, D, E,','UAE, CANADA, BOSTON, BAHRAIN, FRANCE')
Select A.ID
,B.*
From #YourTable A
Cross Apply (
Select NewValue = B1.RetVal+'-'+B2.RetVal
From [dbo].[udf-Str-Parse](A.Letter,',') B1
Join [dbo].[udf-Str-Parse](A.Location,',') B2
on charindex(B1.RetVal,B2.RetVal)>0
) B
Returns
ID NewValue
1 A-UAE
1 A-CANADA
1 A-BAHRAIN
1 A-FRANCE
1 B-BOSTON
1 B-BAHRAIN
1 C-CANADA
1 C-FRANCE
1 D-CANADA
1 E-UAE
1 E-FRANCE
The UDF if needed
CREATE FUNCTION [dbo].[udf-Str-Parse] (#String varchar(max),#Delimiter varchar(10))
Returns Table
As
Return (
Select RetSeq = Row_Number() over (Order By (Select null))
,RetVal = LTrim(RTrim(B.i.value('(./text())[1]', 'varchar(max)')))
From (Select x = Cast('<x>' + replace((Select replace(#String,#Delimiter,'§§Split§§') as [*] For XML Path('')),'§§Split§§','</x><x>')+'</x>' as xml).query('.')) as A
Cross Apply x.nodes('x') AS B(i)
);
--Thanks Shnugo for making this XML safe
--Select * from [dbo].[udf-Str-Parse]('Dog,Cat,House,Car',',')
--Select * from [dbo].[udf-Str-Parse]('John Cappelletti was here',' ')
--Select * from [dbo].[udf-Str-Parse]('this,is,<test>,for,< & >',',')
EDIT - Option without a UDF
Declare #YourTable table (ID int,Letter varchar(50),Location varchar(50))
Insert Into #YourTable values
(1,'A, B, C, D, E,','UAE, CANADA, BOSTON, BAHRAIN, FRANCE')
Select A.ID
,B.*
From #YourTable A
Cross Apply (
Select NewValue = B1.RetVal+'-'+B2.RetVal
From (
Select RetSeq = Row_Number() over (Order By (Select null))
,RetVal = LTrim(RTrim(B.i.value('(./text())[1]', 'varchar(max)')))
From (Select x = Cast('<x>' + replace((Select replace(A.Letter,',','§§Split§§') as [*] For XML Path('')),'§§Split§§','</x><x>')+'</x>' as xml).query('.')) as A
Cross Apply x.nodes('x') AS B(i)
) B1
Join (
Select RetSeq = Row_Number() over (Order By (Select null))
,RetVal = LTrim(RTrim(B.i.value('(./text())[1]', 'varchar(max)')))
From (Select x = Cast('<x>' + replace((Select replace(A.Location,',','§§Split§§') as [*] For XML Path('')),'§§Split§§','</x><x>')+'</x>' as xml).query('.')) as A
Cross Apply x.nodes('x') AS B(i)
) B2
on charindex(B1.RetVal,B2.RetVal)>0
) B

SQL replace string values

I'd like to know how can I replace multiple text values from a string in SQL?
I have a formula that I get from a table but inside that formula there are some text values with apostrophes that I need to replace for numeric values from another table, example:
Table_Values
ID| DESC |VALUE
01 | ABC | 5
02 | DEF | 10
03 | GHI | 15
TABLE_FORMULA
ID | FORMULA
01 | X='ABC'+'DEF'+'GHI'
The basic idea is to get the same formula with a result like this:
X='5'+'10'+'15'
Any idea or example would be great. Thanks.
I don't know why your data is stored like that but here is my attempt to solve your problem.
First, you need a Pattern Splitter to parse your FORMULA. Here is one taken from Dwain Camp's article.
-- PatternSplitCM will split a string based on a pattern of the form
-- supported by LIKE and PATINDEX
--
-- Created by: Chris Morris 12-Oct-2012
CREATE FUNCTION [dbo].[PatternSplitCM]
(
#List VARCHAR(8000) = NULL
,#Pattern VARCHAR(50)
) RETURNS TABLE WITH SCHEMABINDING
AS
RETURN
WITH numbers AS (
SELECT TOP(ISNULL(DATALENGTH(#List), 0))
n = ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
FROM
(VALUES (0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) d (n),
(VALUES (0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) e (n),
(VALUES (0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) f (n),
(VALUES (0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) g (n)
)
SELECT
ItemNumber = ROW_NUMBER() OVER(ORDER BY MIN(n)),
Item = SUBSTRING(#List,MIN(n),1+MAX(n)-MIN(n)),
[Matched]
FROM (
SELECT n, y.[Matched], Grouper = n - ROW_NUMBER() OVER(ORDER BY y.[Matched],n)
FROM numbers
CROSS APPLY (
SELECT [Matched] = CASE WHEN SUBSTRING(#List,n,1) LIKE #Pattern THEN 1 ELSE 0 END
) y
) d
GROUP BY [Matched], Grouper
Here is your final query. This uses a combination of string functions like CHARINDEX, LEFT, RIGHT and string concatenation using FOR XML PATH(''):
WITH Cte AS(
SELECT
f.*,
LHS = LEFT(f.FORMULA, CHARINDEX('=', f.FORMULA) - 1),
RHS = RIGHT(f.FORMULA, LEN(f.FORMULA) - CHARINDEX('=', f.FORMULA)),
s.*,
v.VALUE
FROM Table_Formula f
CROSS APPLY dbo.PatternSplitCM(RIGHT(f.FORMULA, LEN(f.FORMULA) - CHARINDEX('=', f.FORMULA)), '[+-/\*]') s
LEFT JOIN Table_Values v
ON v.[DESC] = REPLACE(s.Item, '''', '')
)
--SELECT * FROM Cte
SELECT
c.ID,
c.FORMULA,
LHS + '=' + STUFF((
SELECT ISNULL('''' + CONVERT(VARCHAR(5), VALUE) + '''', ITEM)
FROM Cte
WHERE ID = c.ID
ORDER BY ItemNumber
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)')
, 1, 0, '')
FROM Cte c
GROUP BY C.ID, c.FORMULA, c.LHS
SQL Fiddle
RESULT
| ID | FORMULA | |
|----|---------------------|-----------------|
| 1 | X='ABC'+'DEF'+'GHI' | X='5'+'10'+'15' |

Hierarchical query to get the following output from a query in sql server

i have a table called emp. The table has following data.
empid name Manager_id
1 A Null
2 B 1
3 C 2
4 D 2
5 E 4
i want the output from above table as below.
empid name Manager_id Level1 Level2 Level3 Level4
1 A null A null null null
2 B 1 A B null null
3 C 2 A B C null
4 D 2 A B D D
5 E 4 A B D E
with C as
(
select T.EmpID,
T.ManagerID,
T.Name,
cast('' as xml).query('element X { attribute V {sql:column("T.Name")}}') as LvlXML
from YourTable as T
where T.ManagerID is null
union all
select T.EmpID,
T.ManagerID,
T.Name,
C.LvlXML.query('., element X { attribute V {sql:column("T.Name")}}')
from YourTable as T
inner join C
on T.ManagerID = C.EmpID
)
select C.EmpID,
C.Name,
C.ManagerID,
C.LvlXML.value('/X[1]/#V', 'varchar(100)') as Level1,
C.LvlXML.value('/X[2]/#V', 'varchar(100)') as Level2,
C.LvlXML.value('/X[3]/#V', 'varchar(100)') as Level3,
C.LvlXML.value('/X[4]/#V', 'varchar(100)') as Level4,
C.LvlXML.value('/X[5]/#V', 'varchar(100)') as Level5
from C;
SQL Fiddle
Update:
#t-clausen.dk pointed out that performence for the query above is not what it can be so here is a faster version.
First add an index on ManagerID with Name as an included column.
create index IX_YourTable_ManagerID on YourTable(ManagerID) include(Name)
And the new query that builds the needed columns as we go in the recursion.
with C as
(
select T.EmpID,
T.ManagerID,
T.Name,
T.Name as Level1,
cast(null as varchar(100)) as Level2,
cast(null as varchar(100)) as Level3,
cast(null as varchar(100)) as Level4,
1 as Lvl
from YourTable as T
where T.ManagerID is null
union all
select T.EmpID,
T.ManagerID,
T.Name,
C.Level1,
case when C.lvl = 1 then T.Name else C.Level2 end,
case when C.lvl = 2 then T.Name else C.Level3 end,
case when C.lvl = 3 then T.Name else C.Level4 end,
C.Lvl + 1
from YourTable as T
inner join C
on T.ManagerID = C.EmpID
)
select C.EmpID,
C.Name,
C.ManagerID,
C.Level1,
C.Level2,
C.Level3,
C.Level4
from C;
That gives you this nice little query plan with an index seek both in the anchor and in the recursive part of the query:
SQL Fiddle
I would use PIVOT, I imagine this will offer the best performance.
-- testtable and data
DECLARE #t table(Empid int identity(1,1), Name char(1), Manager_id int)
INSERT #t VALUES('A',Null),('B',1),('C',2),('D',2),('E',4)
-- query
;WITH CTE as
(
SELECT
Name,
Manager_id,
Name lvlName,
Empid,
1 reverselvl
FROM #t
UNION ALL
SELECT
CTE.Name,
T.Manager_id,
T.Name lvlName,
CTE.Empid,
CTE.reverselvl + 1
FROM #t T
JOIN CTE
ON T.Empid = CTE.Manager_id
),
CTE2 as
(
SELECT Empid,
count(*) over (partition by Empid) - reverselvl lvl,
lvlName,
max(Manager_id) over (partition by Empid) Manager_id,
CTE.Name
FROM CTE
)
SELECT
Empid,
Name,
Manager_id,
[0] Level1, [1] Level2, [2] Level3, [3] Level4
FROM CTE2
PIVOT (max(lvlName) FOR [lvl] IN ([0],[1],[2],[3])) AS pvt
OPTION (maxrecursion 0)
Result:
Empid Name Manager_id Level1 Level2 Level3 Level4
1 A NULL A NULL NULL NULL
2 B 1 A B NULL NULL
3 C 2 A B C NULL
4 D 2 A B D NULL
5 E 4 A B D E

Resources