OpenJson in SQL Server for nested json data? - json

I have a json data like this:
[
{"id": 1}, {"id": 3}, {"id": 2, "children": [{"id": 4}, {"id": 5}]}
]
Please help me how to parse this data into relational data:
Column: Id Order/Index ParentId
---------------------------------------
1 1 0
3 2 0
2 3 0
4 4 2
5 5 2

There are a couple of non-trivial things in this request. First is to order the resulting rows by the document position, which is not visible when you use OPENJSON … WITH to project the columns. And the second one is that you need a hierarchical query (assuming there could be multiple levels).
Anyway, something like this:
declare #doc nvarchar(max) = N'[{"id":1},{"id":3},{"id":2,"children":[{"id":4},{"id":5}]}]';
with q as
(
select [key] nodePath,
cast(json_value(d.[value],'$.id') as int) Id,
cast(null as int) ParentId,
cast(json_query(d.[value],'$.children') as nvarchar(max)) children
from openjson(#doc) d
union all
select q.nodePath + '.' + d.[key] nodePath,
cast(json_value(d.[value],'$.id') as int) Id,
q.id ParentId,
cast(json_query(d.[value],'$.children') as nvarchar(max)) children
from q
outer apply openjson(q.children) d
where q.children is not null
)
select Id, row_number() over (order by nodePath) [Order/Index], ParentId
from q
order by [Order/Index]
outputs
Id Order/Index ParentId
----------- -------------------- -----------
1 1 NULL
3 2 NULL
2 3 NULL
4 4 2
5 5 2
(5 rows affected)

Related

Unable to get all the values from JSON_ARRAY_ELEMENTS()

Table with sample data:
create table tbl_jsdata
(
id int,
p_id int,
field_name text,
field_value text
);
insert into tbl_jsdata values
(1,101,'Name','Sam'),
(2,101,'City','Dubai'),
(3,101,'Pin','1235'),
(4,101,'Country','UAE'),
(5,102,'Name','Sam'),
(6,102,'City','Dubai'),
(7,102,'Name','Sam Jack'),
(8,102,'Name','Test'),
(9,102,'Name',null);
json_agg query:
drop table if exists tempJSData;
select p_id,
json_build_array(json_object_agg(field_name, field_value)) into tempJSData
from tbl_jsdata
group by p_id;
Getting Result:
select p_id,(json_array_elements(json_build_array)->>'Name')::text Namess
from tempJSData
p_id Namess
---------------------------------
101 Sam
102
Expected Result:
p_id Namess
---------------------------------
101 Sam
102 Sam
102 Sam Jack
102 Test
102
I think it's because you're not creating an array of Name.
If you check your query
select p_id,
json_build_array(json_object_agg(field_name, field_value))
from tbl_jsdata
group by p_id;
The result is
p_id | json_build_array
------+---------------------------------------------------------------------------------------------
101 | [{ "Name" : "Sam", "City" : "Dubai", "Pin" : "1235", "Country" : "UAE" }]
102 | [{ "Name" : "Sam", "City" : "Dubai", "Name" : "Sam Jack", "Name" : "Test", "Name" : null }]
(2 rows)
Having multiple adjacent entries of the Name field. The following json_array_elements(json_build_array)->>'Name' will fetch the first entry only. I suggest to create an array based on p_id and field_name first
with array_built as (
select p_id,field_name,
array_agg(field_value) field_value
from tbl_jsdata
group by p_id, field_name
)
select p_id,
jsonb_object_agg(field_name, field_value)
from array_built
group by p_id
;
The result can be optimised since it creates an array even if there is only one value
p_id | jsonb_object_agg
------+---------------------------------------------------------------------------
101 | {"Pin": ["1235"], "City": ["Dubai"], "Name": ["Sam"], "Country": ["UAE"]}
102 | {"City": ["Dubai"], "Name": ["Sam", "Sam Jack", "Test", null]}
(2 rows)
But now you can parse it correctly the whole query is
select p_id,
json_build_array(json_object_agg(field_name, field_value))
from tbl_jsdata
group by p_id;
select p_id,
json_build_array(json_object_agg(field_name, field_value))
from tbl_jsdata
group by p_id;
with array_built as (
select p_id,field_name,
array_agg(field_value) field_value
from tbl_jsdata
group by p_id, field_name
), agg as (
select p_id,
jsonb_object_agg(field_name, field_value) json_doc
from array_built
group by p_id
)
select p_id, jsonb_array_elements(json_doc->'Name') from agg;
;
With the expected result as
p_id | jsonb_array_elements
------+----------------------
101 | "Sam"
102 | "Sam"
102 | "Sam Jack"
102 | "Test"
102 | null
(5 rows)
You can use json_each_text to extract the values of your array and in the WHERE clause filter only the key you want:
SELECT p_id,j.value
FROM tempJSData, json_each_text(json_build_array->0) j
WHERE j.key = 'Name';
p_id | value
------+----------
101 | Sam
102 | Sam
102 | Sam Jack
102 | Test
102 |
(5 rows)
Note: this query assumes the format of your json is final. If not, consider creating an array of Name instead of an array of objects that contain names in it: name[foo,bar] instead of [name:foo,name:bar]. The answer from Ftisiot makes a pretty good point.
Demo: db<>fiddle
Your JSON aggregation is essentially invalid, as you are creating a JSON value where the same key appears more than once. If you had used the recommended jsonb data type, the duplicate keys would have been removed.
I think this aggregation makes more sense:
create table tempjsdata
as
select p_id,
jsonb_agg(jsonb_build_object(field_name, field_value)) as names
from tbl_jsdata
group by p_id
The above generates the following result:
p_id | names
-----+---------------------------------------------------------------------------------------------
101 | [{"Name": "Sam"}, {"City": "Dubai"}, {"Pin": "1235"}, {"Country": "UAE"}]
102 | [{"Name": "Sam"}, {"City": "Dubai"}, {"Name": "Sam Jack"}, {"Name": "Test"}, {"Name": null}]
Then you can use:
select p_id,
x.*
from tempjsdata
cross join lateral (
select x.item ->> 'Name'
from jsonb_array_elements(t.names) as x(item)
where x.item ? 'Name'
) x
Online example

Unpack GROUP BY query for results that lack the key?

I'm trying to run a GROUP BY query to select all the items in a table grouping them by a collection id.
| id | collection | date |
| 1 | x | ... |
| 2 | x | ... |
| 3 | y | ... |
| 4 | | ... |
| 5 | | ... |
I'd like to obtain a list like this:
[
{
collection: x,
items: [1, 2]
},
{
collection: y,
items: [3]
},
{
collection: null,
items: [4]
},
{
collection: null,
items: [5]
}
]
My query right now is the following, but I need a way to unpack items that lack the collection ID so that they all end up in a separate group, how can I do?
SELECT id, collection FROM items ORDER BY date DESC GROUP BY collection
I'm using MySQL but any SQL syntax would still be helpful.
Here I have shared two query. One with conditional group by clause and the other one is using union all. I would prefer first one.
CREATE TABLE items( id int, collection varchar(10));
insert into items values( 1 , 'x');
insert into items values( 2 , 'x');
insert into items values( 3 , 'y');
insert into items (id)values( 4 );
insert into items (id)values( 5 );
Query#1 (conditional group by clause)
SELECT collection,group_concat(id) id FROM items
GROUP BY collection,
case when collection is not null then collection else id end
Output:
collection
id
null
4
null
5
x
1,2
y
3
Query#2 (using union all)
SELECT collection,group_concat(id) id FROM items where collection is not null
GROUP BY collection
union all
select collection,id from items where collection is null
Output:
collection
id
x
1,2
y
3
null
4
null
5
db<fiddle here
Sorted by collection and id:
Query#1
SELECT collection,group_concat(id) id FROM items
GROUP BY collection,
case when collection is not null then collection else id end
order by collection,id
Output:
collection
id
null
4
null
5
x
1,2
y
3
Query#2
select collection,id from
(
SELECT collection,group_concat(id) id FROM items where collection is not null
GROUP BY collection
union all
select collection,id from items where collection is null
)t order by collection,id
Output:
collection
id
null
4
null
5
x
1,2
y
3
db<fiddle here
SELECT
CASE WHEN collection is null THEN id ELSE collection END as id,
GROUP_CONCAT(collection) as collection
FROM items
GROUP BY 1
I see, you have an ORDER BY date?

Use STUFF() on a result set coming from OPENJSON

I have a ClassTable as shown below with a Languages column in which the data is in JSON:
ID
Title
Languages
1
Class1
[{"ID": 1, "Name": "English"},{"ID": 2, "Name": "Hindi"}]
2
Class2
[{"ID": 1, "Name": "Marathi"},{"ID": 2, "Name": "Telugu"}]
and a Master table of Languages as
ID
Name
1
English
2
Hindi
3
Marathi
4
Telugu
I need output as below
ID
Title
LanguageIDs
1
TestTitle1
1,2
I am trying to achieve this with OPENJSON to get data from JSON and then I am applying STUFF() to that data so that I would get comma separated LanguageIDs
This is the query, I have written but I am not getting the expected output
SELECT
A.ID,
A.Title,
LanguageIDs = STUFF ((SELECT CONCAT(',',A.ID)
FROM Master.Languages
WHERE ID IN (A.LanguageID)
FOR XML PATH(''), TYPE).value('.', 'VARCHAR(MAX)'), 1, 1, SPACE(0))
FROM
(SELECT
X.ID,
X.Title,
X.CreatedOn,
B.ID as LanguageID
FROM
ClassTable X
CROSS APPLY
OPENJSON(Languages)
WITH (ID INT '$.ID') as B
WHERE
X.ID = 1) AS A
Can anybody tell me whats the mistake I am making? Or do I have to try a different approach for this problem?
Logically, It should work
SELECT
X.ID,
X.Title,
(
select cast(ID as varchar) +',' from OPENJSON(x.Languages)
WITH (ID INT '$.ID')
for xml path('')
) LanguageID
FROM
ClassTable X where x.id=1

SQL PIVOT QUERY ISSUE

I have a table [dbo].[RISK_DISCPLN]
RISK_DISCPLN_ID RISK_DISCPLN_NM
1 LegalRisk
2 institutional Risk
4 Market Risk
Tabbe Name [dbo].[PROJ_RISK_DISCPLN]
PROJ_ID RISK_DISCPLN_ID
1 1
1 2
1 4
2 1
2 2
2 4
3 1
3 2
3 4
Table Name [dbo].[USER]
USER_ID USER_FIRST_NM USER_LST_NM
2 saravanakumar rajkumar
3 Soosai Antony
4 Adam Allen
5 Babita Tripathy
9 stacey Davis
11 NULL NULL
I tried below query to display names into the corresponding risk, i am getting all the names as null value... Help needed please
;with cte as
(
SELECT [PROJ_RISK_DISCPLN].PROJ_ID,
[USER].USER_LST_NM + ' '+ [USER].USER_FIRST_NM as Name,
[RISK_DISCPLN].RISK_DISCPLN_NM,
[RISK_DISCPLN].RISK_DISCPLN_ID,
[USER].[USER_ID]
FROM dbo.[PROJ_RISK_DISCPLN]
left join [dbo].[USER]
on [PROJ_RISK_DISCPLN].RISK_OWN_USER_ID = [dbo].[USER].[USER_ID]
left join dbo.[RISK_DISCPLN] on
[PROJ_RISK_DISCPLN].RISK_DISCPLN_ID = [RISK_DISCPLN].RISK_DISCPLN_ID
)
select *
from
(
select c1.PROJ_ID,
c1.RISK_DISCPLN_NM,
STUFF(
(SELECT ', ' + c2.Name
FROM cte c2
where c1.PROJ_ID = c2.PROJ_ID
and c1.RISK_DISCPLN_ID = c2.RISK_DISCPLN_ID
FOR XML PATH (''))
, 1, 1, '') AS Name
from cte c1
) d
pivot
(
max(Name)
for RISK_DISCPLN_NM in ([LegalRisk Owner],[institutional Risk Owner],[Market Risk Owner])
) piv
I got answer, Instead [LegalRisk Owner],[institutional Risk Owner],[Market Risk Owner]
i have to use [LegalRisk],[institutional Risk],[Market Risk] ....

how to retrieve a field records by comparing the other fields mssql?

I have a table; let it be called table1; with the following fields and data
msgid msisdn teaserid send
1 333 1 1
2 333 1 0
3 444 2 1
4 444 2 1
5 444 3 1
I need a query that returns those msgid for which send = 1 for every single records having same msisdn, teaserid. In the above case, i want msgid: 3,4,5 as result.
How can this be done using mssql query?
This is a cute use of the window functions:
declare #t table (msgid int,msisdn int,teaserid int,send int)
insert into #t (msgid,msisdn,teaserid,send) values
(1,333,1,1),
(2,333,1,0),
(3,444,2,1),
(4,444,2,1),
(5,444,3,1)
select * from (
select *,MIN(send) OVER (PARTITION BY msisdn,teaserid) as all1
from #t
)t
where all1 = 1
Result:
msgid msisdn teaserid send all1
----------- ----------- ----------- ----------- -----------
3 444 2 1 1
4 444 2 1 1
5 444 3 1 1
By computing the MIN(send) across the msisdn,teaserid partitions, this can only be 1 if all of the send values are 1. If just one row has a 0, that will be the minimum for that partition.
You can use this query to get the result
select msgid
from table1 t
where send=1
and exists(select * from table
where send=1
and msisdn=t.msisdn
and teaserid=t.teaserid and msgid != t.msgid)