SQL Inner join with For JSON hierarchy - sql

I'm trying to join two SQL tables with inner join and then return them as JSON from my procedure.
My select statement is:
SELECT
#CustomerAddressesJSON =
(SELECT
Address.AddressID, Address.CustomerID,
Address.AddressTypeID, Address.IsPrimary,
CountryID, StateID, CountyID, DistrictID,
StreetID, StreetNumber, PostalCode,
AdditionalInformation, AddressImageID,
CreatedOn, CreatedBy
FROM
[sCustomerManagement].[tCustomerAddresses] Address
INNER JOIN
[sCustomerManagement].[tAddresses] AddressDetails ON Address.AddressID = AddressDetails.AddressID
WHERE
CustomerID = #CustomerID
FOR JSON AUTO)
and the result is like this:
"customerAddressesJSON": "[ {
"AddressID": 1,
"CustomerID": 1,
"AddressTypeID": "T",
"IsPrimary": true,
"AddressDetails": [
{
"CountryID": 1,
"StateID": 1,
"CountyID": 1,
"DistrictID": 1,
"StreetID": 1,
"StreetNumber": "125",
"PostalCode": "1000",
"AdditionalInformation": "Metro Sofia",
"CreatedOn": "2017-10-24T11:46:20.1933333",
"CreatedBy": 24
}
]
}, {
"AddressID": 2,
"CustomerID": 1,
"AddressTypeID": "T",
"IsPrimary": true,
"AddressDetails": [
{
"CountryID": 1,
"StateID": 1,
"CountyID": 1,
"DistrictID": 1,
"StreetID": 1,
"StreetNumber": "125",
"PostalCode": "1000",
"AdditionalInformation": "Metro Sofia",
"CreatedOn": "2017-10-24T11:46:20.1933333",
"CreatedBy": 24
}
]
}
The problem is that I don't want the information in the array AddressDetails to be nested. Is it possible the information there to be outside, so I can receive 2 flat objects, without nested information ?
Thanks

Consider using the PATH mode with dot syntax and map all fields to Address as discussed in docs.
SELECT
#CustomerAddressesJSON =
(SELECT
a.AddressID AS 'Address.AddressID', a.CustomerID AS 'Address.CustomerID',
a.AddressTypeID AS 'Address.AddressTypeID', a,IsPrimary AS 'Address.IsPrimary',
d.CountryID AS 'Address.CountryID', d.StateID AS 'Address.StateID',
d.CountyID AS 'Address.CountyID', d.DistrictID AS 'Address.DistrictID',
d.StreetID As 'Address.StreetID', d.StreetNumber AS 'Address.StreetNumber',
d.PostalCode AS 'Address.PostalCode',
d.AdditionalInformation AS 'Address.AdditionalInformation',
d.AddressImageID AS 'Address.AddressImageID',
d.CreatedOn AS 'Address.CreatedOn', d.CreatedBy AS 'Address.CreatedBy'
FROM
[sCustomerManagement].[tCustomerAddresses] a
INNER JOIN
[sCustomerManagement].[tAddresses] d ON a.AddressID = d.AddressID
WHERE
a.CustomerID = #CustomerID
FOR JSON PATH)
Alternatively, use a derived table:
SELECT
#CustomerAddressesJSON =
(SELECT m.*
FROM
(SELECT a.AddressID, a.CustomerID, a.AddressTypeID, a,IsPrimary,
d.CountryID, d.StateID, d.CountyID, d.DistrictID,
d.StreetID, d.StreetNumber, d.PostalCode,
d.AdditionalInformation, d.AddressImageID,
d.CreatedOn, d.CreatedBy
FROM
[sCustomerManagement].[tCustomerAddresses] a
INNER JOIN
[sCustomerManagement].[tAddresses] d ON a.AddressID = d.AddressID
WHERE
a.CustomerID = #CustomerID
) AS m
FOR JSON AUTO)

Related

How to return a JSON tree format from SQL query?

I currently have a table that contains a content_id, root_id, parent_id and content_level. This table is self-referencing, in which a record could have related child records. The parent records do not know about the child records but the child record know about the parents via the parent_id field.
This is the query used for fetching all the records with the root content at the top. The root content has content_level = 0, and both root_id and parent_id = NULL. For the rest of the records, the root_id field will match the content_id of root record.
SELECT *
FROM jccontent c2
WHERE c2.content_id = 138412032
UNION ALL
(
SELECT j.*
FROM jccontent AS c
INNER JOIN jccontent j on c.content_id = j.parent_id
WHERE j.root_id = 138412032
)
ORDER BY content_level ;
From here, I would like to build a JSON tree structure where it will contain the root as the top element, and then nested children elements that follows. I would like to complete this portion using purely SQL. Currently I have done it in code and it works well, but would like to see if doing it in SQL will be better.
My desired output would be something like this:
{
"content_id": 138412032,
"root_id": null,
"parent_id": null,
"content_level": 0,
"children": [
{
"content_id": 1572864000,
"root_id": 138412032,
"parent_id": 138412032,
"content_level": 1,
"children": [
{
"content_id": 1606418432,
"root_id": 138412032,
"parent_id": 1572864000,
"content_level": 2,
"children": []
},
{
"content_id": 515899393,
"root_id": 138412032,
"parent_id": 1572864000,
"content_level": 2,
"children": [
{
"content_id": 75497471,
"root_id": 138412032,
"parent_id": 515899393,
"content_level": 3,
"children": []
}
]
}
]
},
{
"content_id": 1795162113,
"root_id": 138412032,
"parent_id": 138412032,
"content_level": 1,
"children": []
}
]
}
If there is any additional information required, please let me know. I will be glad to share. Thank you.
try
WITH recursive cte AS (
SELECT content_id, parent_id, content_level
FROM jccontent
WHERE content_id = 138412032
UNION ALL
SELECT j.content_id, j.parent_id, j.content_level
FROM jccontent j
INNER JOIN cte c ON j.parent_id = c.content_id
)
SELECT JSON_OBJECT('id' VALUE cte.content_id, 'parent_id' VALUE cte.parent_id, 'level' VALUE cte.content_level)
FROM cte
ORDER BY cte.content_level;

Nested JSON object query only returns on first root

I have a nested JSON object database query that is used as input for a treeview component. This query works when there is 1 root in the database, but it breaks when having multiple roots. I can't really find a solution to it, could someone take a look?
Link to the dbfiddle: https://dbfiddle.uk/sjYamgm1
Query
CREATE OR REPLACE FUNCTION json_tree2() RETURNS jsonb AS $$
DECLARE
_json_output jsonb;
_temprow record;
BEGIN
SELECT
jsonb_build_object('id', id, 'label', "categoryName", 'children', array_to_json(ARRAY[]::uuid[]))
INTO _json_output
FROM "Category"
WHERE "parentCategory" IS NULL;
FOR _temprow IN
WITH RECURSIVE tree(id, ancestor, child, path, json) AS (
SELECT
t1.id,
NULL::uuid,
t2.id,
'{children}'::text[] || (row_number() OVER (PARTITION BY t1.id ORDER BY t2.id) - 1)::text,
jsonb_build_object('id', t2.id, 'label', t2."categoryName", 'children', array_to_json(ARRAY[]::uuid[]))
FROM "Category" t1
LEFT JOIN "Category" t2 ON t1.id = t2."parentCategory"
WHERE t1."parentCategory" IS NULL
UNION
SELECT
t1.id,
t1."parentCategory",
t2.id,
tree.path || '{children}' || (row_number() OVER (PARTITION BY t1.id ORDER BY t2.id) - 1)::text,
jsonb_build_object('id', t2.id, 'label', t2."categoryName", 'children', array_to_json(ARRAY[]::uuid[]))
FROM "Category" t1
LEFT JOIN "Category" t2 ON t1.id = t2."parentCategory"
INNER JOIN tree ON (t1.id = tree.child)
WHERE t1."parentCategory" = tree.id
)
SELECT
child as id, path, json
FROM tree
WHERE child IS NOT NULL ORDER BY path
LOOP
SELECT jsonb_insert(_json_output, _temprow.path, _temprow.json) INTO _json_output;
END LOOP;
RETURN _json_output;
END;
$$ LANGUAGE plpgsql;
SELECT jsonb_pretty(json_tree2())
Problem: I only get the first root.
{
    "id": "bfa3fdf8-4672-404e-baf5-0f9098a5705b",
    "label": "1",
    "children": [
        {
            "id": "9dfef3df-d67b-4afd-a591-2e9b1c0b21b1",
            "label": "2.1",
            "children": [
                {
                    "id": "903a727f-d94d-44ff-b2f6-a985fd167342",
                    "label": "1.1.1",
                    "children": [
                    ]
                },
                {
                    "id": "903a727f-d94d-44ff-b2f6-a985fd167321",
                    "label": "2.1.1",
                    "children": [
                    ]
                }
            ]
        },
        {
            "id": "9dfef3df-d67b-4afd-a591-2e9b1c0b21b7",
            "label": "1.1",
            "children": [
            ]
        }
    ]
}
Expected outcome example
[
{
"id": "bfa3fdf8-4672-404e-baf5-0f9098a5705b",
"label": "1",
"children": [
{
"id": "9dfef3df-d67b-4afd-a591-2e9b1c0b21b7",
"label": "1.1",
"children": [
{
"id": "903a727f-d94d-44ff-b2f6-a985fd167342",
"label": "1.1.1",
"children": [
]
},
]
},
{
"id": "bfa3fdf8-4672-404e-baf5-0f9098a5705e",
"label": "2",
"children": [
{
"id": "9dfef3df-d67b-4afd-a591-2e9b1c0b21b1",
"label": "2.1",
"children": [
{
"id": "903a727f-d94d-44ff-b2f6-a985fd167321",
"label": "2.1.1",
"children": [
]
},
]
}
]
Dummy data
CREATE TABLE "Category" (
id uuid,
categoryName text,
parentCategory uuid
);
INSERT INTO "Category" VALUES
('bfa3fdf8-4672-404e-baf5-0f9098a5705b', '1', NULL),
('bfa3fdf8-4672-404e-baf5-0f9098a5705e', '2', NULL),
('9dfef3df-d67b-4afd-a591-2e9b1c0b21b7', '1.1', 'bfa3fdf8-4672-404e-baf5-0f9098a5705b'),
('9dfef3df-d67b-4afd-a591-2e9b1c0b21b1', '2.1', 'bfa3fdf8-4672-404e-baf5-0f9098a5705e'),
('903a727f-d94d-44ff-b2f6-a985fd167342', '1.1.1', '9dfef3df-d67b-4afd-a591-2e9b1c0b21b7'),
('903a727f-d94d-44ff-b2f6-a985fd167321', '2.1.1', '9dfef3df-d67b-4afd-a591-2e9b1c0b21b1');
SELECT * FROM "Category";
In your recursive cte, you can build the tree from the bottom up, starting with the leaf nodes and aggregating at each iteration:
create or replace function json_tree2() returns jsonb AS $$
declare
_json_output jsonb;
_temprow record;
begin
with recursive leaf_normalized(id, cnt, p, o, r) as(
select c.id||1::text||'-NULL', 1, c.id, c.id, (select max(array_length(regexp_split_to_array(c3.categoryName, '\.'), 1)) from category c3) - array_length(regexp_split_to_array(c.categoryName, '\.'), 1) - 1
from category c
where not exists (select 1 from category c1 where c1.parentCategory = c.id) and array_length(regexp_split_to_array(c.categoryName, '\.'), 1) < (select max(array_length(regexp_split_to_array(c3.categoryName, '\.'), 1)) from category c3)
union all
select c.o||(c.cnt + 1)::text||'-NULL', c.cnt + 1, c.id, c.o, c.r - 1 from leaf_normalized c where c.r > 0
),full_normalized(id, categoryName, parentCategory) as (
select c.* from category c
union all
select c.id, '0', c.p from leaf_normalized c
),
cte(id, cname, p, js) as (
select c.id, c.categoryName, c.parentCategory, '[]'::jsonb from full_normalized c
where not exists (select 1 from full_normalized c1 where c1.parentCategory = c.id)
union all
select t.id, t.cname, t.p, jsonb_agg(t.js)
from (select c1.id, c1.categoryName cname, c1.parentCategory p,
jsonb_build_object('id', c.id, 'label', c.cname, 'children', c.js) js
from cte c join full_normalized c1 on c.p = c1.id) t
group by t.id, t.cname, t.p
),
clean(js, f) as (
select (select jsonb_agg(jsonb_build_object('id', c.id, 'label', c.cname, 'children', c.js))
from cte c where c.p is null)::text, 1
union all
select regexp_replace(c.js, '\{"id":\s"[\w\-]+\-NULL", "label"\: "\w+", "children": \[\]\}(?:,\s)*', ''), (array_length(regexp_matches(c.js, '\{"id":\s"[\w\-]+\-NULL", "label"\: "\w+", "children": \[\]\}(?:,\s)*'), 1) > 0)::int from clean c where c.f = 1
)
select (select c.js::jsonb from clean c where not exists (select 1 from regexp_matches(c.js, '\{"id":\s"[\w\-]+\-NULL", "label"\: "\w+", "children": \[\]\}(?:,\s)*')))
into _json_output;
return _json_output;
end;
$$ language plpgsql;
See fiddle.

Parent average value for children sql - recursive

I have a table. ShopUnit:
name,
price,
type (OFFER,CATEGORY),
parentID - (fk to shopUnit (self)).
I have an id. I need to return row with this id. and every children. If item or children have a type == CATEGORY. I need to set price = AVG value for children of a row.
I think of a recursive
with recursive unit_tree as (
select s1.id,
s1.price,
s1.parent_id,
s1.type,
0 as level
from shop_unit s1
where s1.id = 'a'
union all
select s2.id,
s2.price,
s2.parent_id,
s2.type,
level + 1
from shop_unit s2
join unit_tree ut on ut.id = s2.parent_id
)
select unit_tree.id,
unit_tree.parent_id,
unit_tree.type,
unit_tree.level,
unit_tree.price
from unit_tree;
but how do i count the average for every category.
here's example
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66a111",
"name": "Категория",
"type": "CATEGORY",
"parentId": null,
"date": "2022-05-28T21:12:01.516Z",
"price": 6,
"children": [
{
"name": "Оффер 1",
"id": "3fa85f64-5717-4562-b3fc-2c963f66a222",
"price": 4,
"date": "2022-05-28T21:12:01.516Z",
"type": "OFFER",
"parentId": "3fa85f64-5717-4562-b3fc-2c963f66a111"
},
{
"name": "Подкатегория",
"type": "CATEGORY",
"id": "3fa85f64-5717-4562-b3fc-2c963f66a333",
"date": "2022-05-26T21:12:01.516Z",
"parentId": "3fa85f64-5717-4562-b3fc-2c963f66a111",
"price": 8,
"children": [
{
"name": "Оффер 2",
"id": "3fa85f64-5717-4562-b3fc-2c963f66a444",
"parentId": "3fa85f64-5717-4562-b3fc-2c963f66a333",
"date": "2022-05-26T21:12:01.516Z",
"price": 8,
"type": "OFFER"
}
]
}
]
}
In order to determine if a unit is a child of another unit, you need to capture the path for each element in your recursive CTE. Then you can use a LATERAL JOIN on the unit_tree to find and average the price of the children of each unit.
WITH RECURSIVE shop_unit(id,name,price,parent_id,type) as (
(VALUES
('a','Propane',null,null,'CATEGORY'),
('b','Fuels',null,'a','CATEGORY'),
('c','HD5',5,'b','ITEM'),
('d','HD10',10,'b','ITEM'),
('e','Commercial',15,'b','ITEM'),
('f','Accessories',null,'a','CATEGORY'),
('g','Grill',100,'f','ITEM'),
('h','NFT',null,'f','CATEGORY'),
('i','bwaah.jpg',20000,'h','ITEM'),
('j','jaypeg.jpg',100000,'h','ITEM'),
('k','WD-40',2,null,'ITEM')
)
),
unit_tree as (
SELECT
s1.id,
s1.name,
s1.price,
s1.parent_id,
s1.type,
0 as level,
array[id] as path
FROM
shop_unit s1
WHERE
s1.id = 'a'
UNION ALL
SELECT
s2.id,
s2.name,
s2.price,
s2.parent_id,
s2.type,
level + 1,
ut.path || s2.id as path --generate the path for every unit so that we can check if it is a child of another element
FROM
shop_unit s2
JOIN unit_tree ut ON ut.id = s2.parent_id
)
SELECT
ut.id,
ut.name,
ut.parent_id,
ut.type,
case when ut.type = 'CATEGORY' then ap.avg_price else ut.price end as price,
ut.level,
ut.path
FROM
unit_tree ut
-- The JOIN LATERAL subquery roughly means "for each row of ut run this query"
-- Must be a LEFT JOIN LATERAL in order to keep rows of ut that have no children.
LEFT JOIN LATERAL (
SELECT
avg(ut2.price) avg_price
FROM
unit_tree ut2
WHERE
ut.level < ut2.level --is deeper level
and ut.id = any(path) --is in the path
GROUP BY
ut.id
) ap ON TRUE
ORDER BY id
You can use recursion to build the output JSON you want, as well:
with recursive top_down as (
select s.id, s.name, s.type, s."parentId", s.price, 1 as level,
array[s.id] as path, s.id as root
from shopunit s
where s."parentId" is null
union all
select c.id, c.name, c.type, c."parentId", c.price, p.level + 1 as level,
p.path||c.id as path, p.root
from shopunit c
join top_down p on p.id = c."parentId"
), category_averages as (
select p."parentId", avg(c.price) as price, p.level, p.root
from top_down p
join top_down c
on p."parentId" = any(c.path)
group by p."parentId", p.level, p.root
), fill_missing as (
select s.id, s.name, s.type, s."parentId",
coalesce(a.price, s.price)::numeric(8,2) as price,
t.level, max(t.level) over (partition by t.root) as max_depth,
row_number() over (partition by s."parentId" order by s.id) as n,
count(1) over (partition by s."parentId") as max_n,
now() as date
from shopunit s
left join category_averages a on a."parentId" = s.id
join top_down t on t.id = s.id
), build_json as (
select id, "parentId", level, max_depth, n, max_n,
to_jsonb(fill_missing) - 'level' - 'max_depth' - 'n' - 'max_n' as j
from fill_missing
where level = max_depth
and n = max_n
union all
select next.id, next."parentId", next.level, next.max_depth, next.n, next.max_n,
case
when next.level = prev.level
then '[]'::jsonb||(to_jsonb(next) - 'level' - 'max_depth' - 'n' - 'max_n')||prev.j
else
jsonb_set(
to_jsonb(next) - 'level' - 'max_depth' - 'n' - 'max_n',
'{children}', '[]'::jsonb || prev.j
)
end as j
from fill_missing next
join build_json prev
on (prev.n = 1 and prev."parentId" = next.id and next.n = next.max_n)
or (prev.n > 1 and prev."parentId" = next."parentId" and next.n = prev.n - 1)
)
select id, jsonb_pretty(j) as j
from build_json
where "parentId" is null;
Which results in:
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66a111",
"date": "2022-06-11T16:14:44.11989+01:00",
"name": "Категория",
"type": "CATEGORY",
"price": 6.00,
"children": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66a222",
"date": "2022-06-11T16:14:44.11989+01:00",
"name": "Оффер 1",
"type": "OFFER",
"price": 4.00,
"parentId": "3fa85f64-5717-4562-b3fc-2c963f66a111"
},
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66a333",
"date": "2022-06-11T16:14:44.11989+01:00",
"name": "Подкатегория",
"type": "CATEGORY",
"price": 8.00,
"children": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66a444",
"date": "2022-06-11T16:14:44.11989+01:00",
"name": "Оффер 2",
"type": "OFFER",
"price": 8.00,
"parentId": "3fa85f64-5717-4562-b3fc-2c963f66a333"
}
],
"parentId": "3fa85f64-5717-4562-b3fc-2c963f66a111"
}
],
"parentId": null
}
db<>fiddle here
(The hidden query populates the table from your example JSON)

Format nested JSON object from Postgres query

I want to get a JSON object formatted similar to this:
{
"username": "USERNAME",
"teamname": "TEAMNAME",
"logs": [
{
"log": {
"log_id": 29,
"person_id": 3,
"activity_id": 3,
"shoe_id": null,
"logdate": "2016-11-29",
"distance": null,
"activitytime": null,
"sleep": null,
"heartrate": null,
"logtitle": null,
"description": null
},
"activity": "Swim",
"comments": {
"comment_id": 1,
"description": "This is a comment",
"person_id": 1,
"log_id": 29
}
}]
}
Currently I have everything formatted correctly except the comments. Here is the SQL query I am using:
SELECT p.username, t.teamname, json_agg(json_build_object('log', l.*, 'comments', c.*, 'activity', a.activity)) as logs
FROM person_tbl p
INNER JOIN log_tbl l ON p.person_id = l.person_id
INNER JOIN activity_tbl a ON l.activity_id = a.activity_id
INNER JOIN comment_tbl c ON c.log_id = l.log_id
INNER JOIN person_team_tbl pt ON p.person_id = pt.person_id
INNER JOIN team_tbl t on t.team_id = pt.team_id
WHERE t.team_id = 5
AND l.logdate > NOW()::date - 7
GROUP BY p.username, t.teamname
ORDER BY p.username
I'm having trouble getting the comments of each log. Right now, it is returning every comment and repeating the logs (they are not associated).
Also, how could I get this query to return the username and teamname when everything else is null (like when there are no logs in the past week)?
Without an SQLfiddle we do not know what your data (and structure) is so it is difficult to answer your question.
For the NULL case - please modify the WHERE clause like this (deliberately not using COALESCE)
WHERE t.team_id = 5 AND (l.logdate IS NULL OR l.logdate > NOW()::date - INTERVAL '7 day')

SQL query returning an extra record

I've tried both right and inner join, either I end up with an extra record or not record at all. Please suggest what to fix in this query.
Table structure
Event -> Day -> Session
session_attendee
SELECT session.id,
(SELECT count(*) from event e INNER JOIN day AS d ON e.id = d.event_id INNER JOIN session AS s ON d.id = s.day_id WHERE e.id = event.id AND d.id = day.id) AS total,
day.date, session.name, session.start, session.end, session.room,
(SELECT COUNT(distinct attendee_id) FROM session s LEFT JOIN session_attendee AS sa ON s.id = sa.session_id WHERE s.id = session.id) AS attendees
FROM event
LEFT JOIN day ON event.id = day.event_id
LEFT JOIN session ON day.id = session.day_id
LEFT JOIN session_attendee ON session.id = session_attendee.session_id
WHERE event.id = 12
GROUP BY session.id
ORDER BY day.date, session.start, event.name;
Resultset
[
{
"id": 9,
"total": 1,
"date": "2015-05-12T04:00:00.000Z",
"name": "test",
"start": "00:55:00",
"end": "00:55:00",
"room": "abc",
"attendees": 0
},
{
"id": null,
"total": 0,
"date": "2015-05-13T04:00:00.000Z",
"name": null,
"start": null,
"end": null,
"room": null,
"attendees": 0
}
]
Just try it on postgresql, you sql query will fail, because it's wrong.
You are trying to select fields without an aggregate function and why do you not have a GROUP BY?
But the main reason why it is an incorrect query, is that you must change it to something like this:
WHERE session.id in (
SELECT
session.id
FROM
event
LEFT JOIN
day ON event.id = day.event_id
LEFT JOIN
session ON day.id = session.day_id
WHERE
event.id = 12