I want to aggregate the data from one table to a complex json. The elements should be grouped by id column. And nested json should be grouped by section column
I have a table with data:
-----------------------------
| id | section | subsection |
----------------------------
| 1 | s_1 | ss_1 |
----------------------------
| 1 | s_1 | ss_2 |
----------------------------
| 1 | s_2 | ss_3 |
----------------------------
| 2 | s_3 | ss_4 |
----------------------------
I want to create json like that:
[
{
"id": 1,
"sections": [
{
"section": "s_1",
"subsections": [
{
"subsection": "ss_1"
},
{
"subsection": "ss_2"
}
]
},
{
"section": "s_2",
"subsections": [
{
"subsection": "ss_3"
}
]
}
]
},
{
"id": 2,
"sections": [
{
"section": "s_3",
"subsections": [
{
"subsection": "ss_3"
}
]
}
]
}
]
I tried do it like that:
select
json_build_array(
json_build_object(
'id', a.id,
'sections', json_agg(
json_build_object(
'section', b.section,
'subsections', json_agg(
json_build_object(
'subsection', c.subsection
)
)
)
)
)
)
from table as a
inner join table as b on a.section = b.section
inner join table as c on b.subsection = c.subsection
group by a.id;
BUT there is a problem: Nested aggregate calls are not allowed
Is there any possible way to use nested aggregate calls? Or is there more elegant solution?
You'll need to use CTEs or nested queries for this. No JOINs necessary though:
SELECT json_agg(
json_build_object(
'id', a.id,
'sections', a.sections
)
)
FROM (
SELECT b.id, json_agg(
json_build_object(
'section', b.section,
'subsections', b.subsections
)
) AS sections
FROM (
SELECT c.id, c.section, json_agg(
json_build_object(
'subsection', c.subsection
)
) AS subsections
FROM table AS c
GROUP BY id, section
) AS b
GROUP BY id
) AS a;
Related
I'm using a SQL Server database. I have a table which has a field that stores the following JSON file structure:
{
"Internal":[
{
"GUID":"7c2fc2b3-7ae2-42c5-b6f0-13137f58348c",
"Type":1,
"ID":155
},
{
"GUID":"8774cbcb-e594-4c64-8ecb-b71d4f97cea4",
"Type":2,
"Link":134
}
],
"External":[
{
"GUID":"be41536b-33ea-4e12-8a11-544aa15c1edb",
"Type":2,
"Link":174
},
{
"GUID":"49383921-5bd8-4cf9-a104-67f957b7fdc7",
"Type":1,
"Link":202
}
]
}
My problem is finding the GUID value. How do I find the value of Link (202), if I know that the GUID I'm looking for is 49383921-5bd8-4cf9-a104-67f957b7fdc7?
I know the solution is to use the CROSS APPLY but I can't come to a solution. I looked through a few threads in the forum, but after a few hours I still can't get the correct query, so I am posting my question on the forum.
Here is my initial query which selects a JSON structure from the database:
SELECT m.Formulardata -- here is a JSON structure
FROM [dbo].[Vorgaenge] v
LEFT JOIN [dbo].[Vorgangsschritte] s ON v.id = s.Vorgang_ID
LEFT JOIN [dbo].[Vorgangsmassnahmen] m ON s.id = m.Vorgangsschritt_ID
WHERE m.Vorgangsmassnahme_ID = 21 AND v.id = #Vorgang_ID
Please try the following solution.
SQL #1 is a minimal reproducible example.
SQL #2 is close as much as possible to your environment.
SQL #1
-- DDL and sample data population, start
DECLARE #tbl TABLE (ID INT IDENTITY PRIMARY KEY, Formulardata NVARCHAR(MAX));
INSERT INTO #tbl (Formulardata) VALUES
(N'{
"Internal":[
{
"GUID":"7c2fc2b3-7ae2-42c5-b6f0-13137f58348c",
"Type":1,
"ID":155
},
{
"GUID":"8774cbcb-e594-4c64-8ecb-b71d4f97cea4",
"Type":2,
"Link":134
}
],
"External":[
{
"GUID":"be41536b-33ea-4e12-8a11-544aa15c1edb",
"Type":2,
"Link":174
},
{
"GUID":"49383921-5bd8-4cf9-a104-67f957b7fdc7",
"Type":1,
"Link":202
}
]
}');
-- DDL and sample data population, end
SELECT ID, u.*
FROM #tbl
CROSS APPLY OPENJSON(Formulardata) x
CROSS APPLY OPENJSON(x.Value)
WITH
(
GUID UNIQUEIDENTIFIER '$.GUID',
Type INT '$.Type',
Link INT '$.Link'
) AS u
-- WHERE u.Link = 202;
SQL #2
;WITH rs AS
(
SELECT m.Formulardata -- here is a JSON structure
FROM [dbo].[Vorgaenge] v
LEFT JOIN [dbo].[Vorgangsschritte] s ON v.id = s.Vorgang_ID
LEFT JOIN [dbo].[Vorgangsmassnahmen] m ON s.id = m.Vorgangsschritt_ID
WHERE m.Vorgangsmassnahme_ID = 21 AND v.id = #Vorgang_ID
)
SELECT ID, u.*
FROM rs
CROSS APPLY OPENJSON(Formulardata) x
CROSS APPLY OPENJSON(x.Value)
WITH
(
GUID UNIQUEIDENTIFIER '$.GUID',
Type INT '$.Type',
Link INT '$.Link'
) AS u
WHERE u.Link = 202;
Output
+----+--------------------------------------+------+------+
| ID | GUID | Type | Link |
+----+--------------------------------------+------+------+
| 1 | 7c2fc2b3-7ae2-42c5-b6f0-13137f58348c | 1 | NULL |
| 1 | 8774cbcb-e594-4c64-8ecb-b71d4f97cea4 | 2 | 134 |
| 1 | be41536b-33ea-4e12-8a11-544aa15c1edb | 2 | 174 |
| 1 | 49383921-5bd8-4cf9-a104-67f957b7fdc7 | 1 | 202 |
+----+--------------------------------------+------+------+
I do not have access right now ot a server that I could test it on, but if you know that it is in the 'external' branch then I think that this should work:
SELECT x.Link
FROM [dbo].[Vorgaenge] v
LEFT JOIN [dbo].[Vorgangsschritte] s ON v.id = s.Vorgang_ID
LEFT JOIN [dbo].[Vorgangsmassnahmen] m ON s.id = m.Vorgangsschritt_ID
CROSS APPLY OPENJSON(m.Formulardata)
WITH (
Internal NVARCHAR(MAX) '$.Internal' AS JSON,
Externals NVARCHAR(MAX) '$.External' AS JSON
) AS a
OUTER APPLY OPENJSON(a.Externals)
WITH (
Guid NVARCHAR(MAX) '$.GUID',
Type INT '$.Type',
Link INT '$.Link'
) AS x
WHERE m.Vorgangsmassnahme_ID = 21 AND v.id = #Vorgang_ID
AND ISJSON(m.Formulardata) > 0
AND x.Type = 2
AND x.GUID = '49383921-5bd8-4cf9-a104-67f957b7fdc7'
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
i have two table casetemp and medicication_master
patient has fields
id
name
age
sex
medicineid
1
xyz
23
M
1,2
2
abc
20
f
3
medicine has fields
id
medname
desc
1
crosin
fever tab
2
etzaa
shampoo
3
zaanta
painkiller
i want the mysql left join output as following :
[{
"id":"1",
"name":"xyz",
"age":"23",
"sex":"M",
"medicine_id":"1,2",
"medicine_Data":[
{
"id":"1"
"medname":"crosin",
"desc":"fever tab"
},
{
"id":"2"
"medname":"etzaa",
"desc":"shampoo"
}]
},
{
"id":"2",
"name":"abc",
"age":"20",
"sex":"F",
"medicine_id":"3",
"medicine_Data":[{
"id":"3"
"medname":"zaanta",
"desc":"pain killer"
}]
}]
the query i used is
SELECT json_object(
'id', b.id,
'name',b.name,
'age',b.age,
'sex',b.sex,
'medicine_id',b.medicine_id,
'medicine_data', json_arrayagg(json_object(
'id', pt.id,
'medname', pt.medname,
'desc',pt.desc,
))
)
FROM patient b LEFT JOIN medication_master pt
ON b.medicine_id = pt.id
where b.id=1
GROUP BY b.id;
thanks in advance
I was looking for help with a query which will produce the output in JSON that I could use to populate a Menu, thus the JSON needs to create the menu structure.
Here are the two tables that I have, and the best query I've been able to write (which does not produce the output I need) is:
SELECT
c.ID As CategoryID,
c.Name As CategoryName,
p.ProductName as ProductName
p.ProductID as ProductID
FROM Category c, Product p
WHERE
c.ID = p.CategoryID
FOR JSON PATH, ROOT('Menu')
Table: Category
ID | Name
---------------
1 | Fruit
2 | Vegetable
Table: Product
ProductID | CategoryID | ProductName
----------------------------------------
1 | 1 | Apple
2 | 1 | Orange
3 | 2 | Celery
4 | 2 | Carrot
5 | 1 | Banana
The desired output for this query would be the following
menu:
[
{name: 'Fruit', ID: '1', Items:
[
{productname: 'Apple', productid: '1'},
{productname: 'Orange', productid: '2'},
{productname: 'Banana', productid: '5'}
]
},
{name: 'Vegetable', ID: '2', Items:
[
{productname: 'Celery', productid: '3'},
{productname: 'Carrot', productid: '4'}
]
}
]
Is anyone able to explain the steps in which I could change my query to achieve this?
You may choose between the following options:
FOR JSON AUTO and an appropriate join. The output is automatically generated based on the structure of the SELECT statement.
A combination of two nested FOR JSON AUTO statements:
Table:
CREATE TABLE Category (ID int, Name varchar(50))
INSERT INTO Category (ID, Name)
VALUES
(1, 'Fruit'),
(2, 'Vegetable')
CREATE TABLE Product (ProductID int, CategoryID int, ProductName varchar(50))
INSERT INTO Product (ProductID, CategoryID, ProductName)
VALUES
(1, 1, 'Apple'),
(2, 1, 'Orange'),
(3, 2, 'Celery'),
(4, 2, 'Carrot'),
(5, 1, 'Banana')
Statement (using FOR JSON AUTO and an appropriate join and SELECT structure):
SELECT
c.Name AS name, c.ID as id,
items.ProductName AS productname, items.ProductId AS productid
FROM Category c
JOIN Product items ON (c.ID = items.CategoryId)
ORDER BY c.ID, items.ProductId
FOR JSON AUTO, ROOT ('menu')
Statement (using two FOR JSON AUTO statements):
SELECT
c.Name AS name, c.ID as id,
items = (
SELECT p.ProductName AS productname, p.ProductID AS productid
FROM Product p
WHERE p.CategoryID = c.ID
FOR JSON AUTO
)
FROM Category c
FOR JSON AUTO, ROOT ('menu')
Result:
{
"menu":[
{
"name":"Fruit",
"id":1,
"items":[
{"productname":"Apple","productid":1},
{"productname":"Orange","productid":2},
{"productname":"Banana","productid":5}
]
},
{
"name":"Vegetable",
"id":2,
"items":[
{"productname":"Celery","productid":3},
{"productname":"Carrot","productid":4}
]
}
]
}
Try this:
DECLARE #Category TABLE
(
[ID] TINYINT
,[Name] VARCHAR(12)
);
INSERT INTO #Category ([ID], [Name])
VALUES (1, 'Fruit')
,(2, 'Vegetable');
DECLARE #Product TABLE
(
[ProductID] TINYINT
,[CategoryID] TINYINT
,[ProductName] VARCHAR(12)
);
INSERT INTO #Product ([ProductID], [CategoryID], [ProductName])
VALUES (1, 1, 'Apple')
,(2, 1, 'Orange')
,(3, 2, 'Celery')
,(4, 2, 'Carrot')
,(5, 1, 'Banana');
SELECT
c.ID As CategoryID,
c.Name As CategoryName,
Items.ProductName as ProductName,
Items.ProductID as ProductID
FROM #Category c
INNER JOIN #Product Items
ON
c.ID = Items.CategoryID
FOR JSON AUTO, ROOT('Menu')
I have three tables (details omitted for brevity):
create table products (
id,
name
)
create table tags (
id,
name
)
create table product_tags (
product_id,
tag_id
)
These tables are populated as follows:
--------
products
--------
+----+------+
| id | name |
+----+------+
| 1 | Rice |
| 2 | Bean |
| 3 | Milk |
+----+------+
----
tags
----
+----+-------+
| id | name |
+----+-------+
| 1 | Eat |
| 2 | Drink |
| 3 | Seeds |
| 4 | Cow |
+----+-------+
When fetching products I want the output to be formatted as:
[{
id: 1,
name: 'Rice',
tags: [
{
id: 1,
name: 'Eat'
},
{
id: 3,
name: 'Seeds'
},
]
},
{
id: 2,
name: 'Bean',
tags: [
{
id: 1,
name: 'Eat'
},
{
id: 3,
name: 'Seeds'
},
]
},
{
id: 3,
name: 'Milk',
tags: [
{
id: 2,
name: 'Drink'
},
{
id: 4,
name: 'Cow'
},
]
}]
To accomplish this, what I'm doing is:
select
products.*,
tags.id as tag_id, tags.name as tag_name
from products
left join product_tags map on map.product_id = products.id
left join tags on map.tag_id = tags.id
The output of which is:
[{
id: 1,
name: 'Rice',
tag_id: 1,
tag_name: 'Eat',
},{
id: 1,
name: 'Rice',
tag_id: 3,
tag_name: 'Seeds',
},{
id: 2,
name: 'Bean',
tag_id: 1,
tag_name: 'Eat',
},{
id: 2,
name: 'Bean',
tag_id: 3,
tag_name: 'Seeds',
},{
id: 3,
name: 'Milk',
tag_id: 2,
tag_name: 'Drink',
},{
id: 3,
name: 'Milk',
tag_id: 4,
tag_name: 'Cow',
}]
Which I parse by hand and aggregate each product instance with an array of zero or more tag objects that it is associated with.
Question
When doing the select above, the output is 6 rows. However, there are only 3 products. Is it possible to use a limit clause that would apply to just the products?
For example, if Product.id => 1 had 10 tags associated with it, doing a LIMIT 5 would only select 5 of the tags. What I want to do is select 5 products along with all tags associated with it.
The only way I can think of achieving this is to select just the products, then do an unbounded SELECT with just the product IDs from the previous query.
Bonus question
Is there a more efficient way to do this JOIN such that the output is aggregated as above?
Use a subquery to select 5 products, then join with tags:
SELECT p.name as product, t.name AS tag
FROM (
SELECT id, name
FROM products
ORDER by name
LIMIT 5) AS p
JOIN product_tags AS pt ON pt.product_id = p.id
JOIN tags AS t ON pt.tag_id = t.id