SQL query for nested one-to-many relations, as aggregated arrays? - sql

I'm working on a kanban board using Postgres as my database. Normally I would use MongoDB for something like this, but I'm trying to practice and improve my understanding of relational databases.
So I have boards, which can have many lanes, and lanes can have many cards. Boards, lanes, and cards are 3 tables in the database. Lanes have a boardId which is a board.id value, cards have a laneId which is a lane.id value.
I want to query for a board by its id, but also have it include an array of its lanes. Then, I want each of the lanes to include an array of its cards. Like this:
{
id:123
title: 'Board title',
lanes: [{id: 0, title: 'Lane title', cards: [{id: 0, text: 'Card text'}]}, {id: 1, title: 'Lane title', cards: [{id: 1, text: 'Card text'}, {id: 2, text: 'Card text'}]}]
}
I have the first part down with a query that gets a board, then creates an array of lanes. Not sure if this is the 'right' way to do it, but here it is, with an example looking for a board with id '123':
select "boards"."id" as "boardId", "boards"."title" as "boardTitle", ARRAY_AGG(json_build_object('id', lanes.id, 'title', lanes.title)) as lanes from "boards" inner join "lanes" on "boards"."id" = "lanes"."boardId" and "boards"."id" = 123 group by "boards"."id"
But I'm not sure how I would get the cards to be included as a cards array for each element in the lanes array. My guess is that I could add another join like "cards" on "lanes"."id" = "cards"."laneId"... but then I don't know how I would include the cards for each lane in the json_build_object.

It would be best to accept json from the database in order to put rows inside of arrays.
I would build this up using CTEs to make it clear as possible:
with agg_lanes as (
select lane_id as id, jsonb_agg(cards) as "cards"
from cards
group by lane_id
), agg_boards as (
select b.id, b.title, jsonb_agg(a) as "lanes"
from agg_lanes a
join lanes l on l.id = a.id
join boards b on b.id = l.board_id
group by b.id, b.title
)
select to_json(a)
from agg_boards a
;
Working fiddle here.

Related

Return rows filtering by jsonb object in array

I have a table called houses and it has two columns id and a jsonb column called details. The details column has the following structure:
[{ kind: "flat", price: 100 }, { kind: "circle", price: 10 }]
I want to get all the houses which details column has at least one object where kind is flat.
This is what I have tried:
select *
FROM houses
WHERE "details"->>'kind' = 'flat'
You may use jsonb_array_elements
select h.* from houses h cross join lateral
jsonb_array_elements(details) as j
where j->>'kind' = 'flat'
Demo

Postgres: Convert Object Graphs to JSON

Is there a procedure that converts Postgres Object Graphs to a nested JSON tree?
Context:
An author has many books.
A book has many review.
The query below gets all reviews of all books of all authors:
select author.id, book.title, review.text
from author
left join book on author.id = book.author_id
left join review on book.id = review.book_id;
With my test data set, this returns:
id title text
1 Oathbringer obath
1 Oathbringer oath
1 The Way of Kings a
1 The Way of Kings bye
1 The Way of Kings hi
2 The Eye of the World w
3 The Fellowship of the Ring x
The tabular format is inconvenient, so I convert it to json:
select json_agg(sub2.*) as data from (
select sub.id, sub.title, json_agg(sub.text) as review from (
select author.id, book.title, review.text
from author
left join book on author.id = book.author_id
left join review on book.id = review.book_id
) as sub group by sub.id, sub.title
) as sub2;
The data is fetched as
[
{
"id":1,
"title":"Oathbringer",
"review":[
"obath",
"oath"
]
},
{
"id":1,
"title":"The Way of Kings",
"review":[
"a",
"bye",
"hi"
]
},
{
"id":2,
"title":"The Eye of the World",
"review":[
"w"
]
},
{
"id":3,
"title":"The Fellowship of the Ring",
"review":[
"x"
]
}
]
Unfortunately, the query is not correct because books for an author are not getting aggregated. Attempting to correct this, I wrote this query:
select sub2.id, json_agg((sub2.title, sub2.review)) as data from (
select sub.id, sub.title, json_agg(sub.text) as review from (
select author.id, book.title, review.text
from author
left join book on author.id = book.author_id
left join review on book.id = review.book_id
) as sub group by sub.id, sub.title
) as sub2 group by sub2.id;
But the json keys are messed up ('f1' instead of 'title', 'f2' instead of 'review').
Questions:
How would the correct query be written?
Can you modify json_agg(sub2.*) to explicitly list the columns to aggregate? The most obvious way, e.g.json_agg((sub2.title, sub2.review)) creates json objects with nonsensical keys 'f1', 'f2', etc.
Is there a better way to convert object graphs to JSON? Correctly nesting queries, and grouping and aggregating fields is tricky to get right. How would you write a program that generates a SQL query returning JSON given an object graph?

Merging columns and jsonb object array into a single jsonb column

I have a table visitors(id, email, first_seen, sessions, etc.)
and another table trackings(id, visitor_id, field, value) that stores custom, user supplied data.
I want to query these and merge the visitor data columns and the trackings into a single column called data
For example, say I have two trackings
(id: 3, visitor_id: 1, field: "orders_made", value: 2)
(id: 4, visitor_id: 1, field: "city", value: 'new york')
and a visitor
(id: 1, email: 'hello#gmail.com, sessions: 5)
I want the result to be on the form of
(id: 1, data: {email: 'hello#gmail.com', sessions: 5, orders_made: 2, city: 'new york'})
What's the best way to accomplish this using Postgres 9.4?
I'll start by saying trackings is a bad idea. If you don't have many things to track, just store json instead; that's what it's made for. If you have a lot of things to track, you'll become very unhappy with the performance of trackings over time.
First you need a json object from trackings:
-- WARNING: Behavior of this with duplicate field names is undefined!
SELECT json_object(array_agg(field), array_agg(value)) FROM trackings WHERE ...
Getting json for visitors is relatively easy:
SELECT row_to_json(email, sessions) FROM visitors WHERE ...;
I recommend you do not just squash all those together. What happens if you have a field called email? Instead:
SELECT row_to_json((SELECT
(
SELECT row_to_json(email, sessions) FROM visitors WHERE ...
) AS visitor
, (
SELECT json_object(array_agg(field), array_agg(value)) FROM trackings WHERE ...
) AS trackings
));

PostgreSQL Select rows based on combination of array values

I would like to select all rows from my database where one row contains at least two terms from a set of words/array.
As an example:
I have the following array:
'{"test", "god", "safe", "name", "hello", "pray", "stay", "word", "peopl", "rain", "lord", "make", "life", "hope", "whatever", "makes", "strong", "stop", "give", "television"}'
and I got a tweet dataset stored in the database. So i would like to know which tweets (column name: tweet.content) contain at least two of the words.
My current code looks like this (but of course it only selects one word...):
CREATE OR REPLACE VIEW tweet_selection AS
SELECT tweet.id, tweet.content, tweet.username, tweet.geometry,
FROM tweet
WHERE tweet.topic_indicator > 0.15::double precision
AND string_to_array(lower(tweet.content)) = ANY(SELECT '{"test", "god", "safe", "name", "hello", "pray", "stay", "word", "peopl", "rain", "lord", "make", "life", "hope", "whatever", "makes", "strong", "stop", "give", "television"}'::text[])
so the last line needs to be adjustested somehow, but i have no idea how - maybe with a inner join?!
I have the words also stored with a unique id in a different table.
A friend of mine recommended getting a count for each row, but i have no writing access for adding an additional column in the original tables.
Background:
I am storing my tweets in a postgres database and I applied a LDA (Latent dirichlet allocation) on the dataset. Now i got the generated topics and the words associated with each topic (20 topics and 25 words).
select DISTINCT ON (tweet.id) tweet.id, tweet.content, tweet.username, tweet.geometry
from tweet
where
tweet.topic_indicator > 0.15::double precision
and (
select count(distinct word)
from
unnest(
array['test', 'god', 'safe', 'name', 'hello', 'pray', 'stay', 'word', 'peopl', 'rain', 'lord', 'make', 'life', 'hope', 'whatever', 'makes', 'strong', 'stop', 'give', 'television']::text[]
) s(word)
inner join
regexp_split_to_table(lower(tweet.content), ' ') v (word) using (word)
) >= 2

Combine results from two tables into one recordset

I have two tables items and content.
items:|ID|menu|img
table
itemcontent |ID|parent|title|content
content holds is paired to items by parent holding the title and content
i want to search all the items and also print out those records wich do not have a title present in the itemcontent table
whereby the titles will be printed as "Empty".
so printing out the output would look something like:
title: test1 and ID: items.ID=1
title: Empty and ID: items.ID=2
title: Empty and ID: items.ID=3
title: test2 and ID: items.ID=4
title: Empty and ID: items.ID=5
etc...
I tried the following and then some but to no avail:
SELECT items.*, itemcontent.title, itemcontent.content
FROM items, itemcontent
WHERE itemcontent.title LIKE '%$search%'
AND itemcontent.parent = items.ID
order by title ASC
A little help would be much appreciated
Since you want all the rows from items whether or not they have a match in itemcontent, plus a field from itemcontent when there is a match you need to use an OUTER JOIN:
SELECT items.*, COALESCE(itemcontent.title, 'empty'), itemcontent.content
FROM items LEFT OUTER JOIN itemcontent ON itemcontent.parent = items.ID
WHERE (itemcontent.title LIKE '%$search%' OR itemcontent.title IS NULL)
ORDER BY items.ID, itemcontent.title ASC
There are small differences among SQL dialects (for instance, not all versions have COALESCE) so if you want a more precise answer indicate which product you're using.
Just to be sure you might want to ORDER BY itemcontent.title and not just title or select itemcontent.title AS title? Do you have a field called title in the items table?